dsh-memory-graph-chiyo 0.0.0-stage → 0.7.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/CHANGELOG.md +106 -0
- package/LICENSE +21 -0
- package/README.md +297 -2
- package/bin/bench.mjs +58 -0
- package/bin/cli.mjs +103 -0
- package/cordis.patch.yml +11 -0
- package/docs/API.md +124 -0
- package/index.d.ts +337 -0
- package/package.json +69 -3
- package/plugin.js +143 -0
- package/src/anchor.js +63 -0
- package/src/assemble.js +99 -0
- package/src/compact.js +237 -0
- package/src/conflicts.js +77 -0
- package/src/consolidate.js +132 -0
- package/src/core.js +409 -0
- package/src/extractor.js +91 -0
- package/src/health.js +94 -0
- package/src/index.js +122 -0
- package/src/instant.js +111 -0
- package/src/lifecycle.js +36 -0
- package/src/person.js +115 -0
- package/src/persona.js +114 -0
- package/src/repair.js +64 -0
- package/src/rules.js +70 -0
- package/src/sensitive.js +48 -0
- package/src/shared.js +113 -0
- package/src/snapshot.js +59 -0
- package/src/store.js +139 -0
- package/src/timeline.js +56 -0
- package/src/viz.js +116 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0 — 2026-11-07
|
|
4
|
+
|
|
5
|
+
从「库」变成「既能 import、也能装进 DSH 的插件」。
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- `plugin.js`:DSH 插件入口,`apply(ctx, config)` 注册 5 个工具
|
|
9
|
+
- `memory_write` 写记忆(带锚点)
|
|
10
|
+
- `memory_recall` 召回
|
|
11
|
+
- `memory_health` 体检
|
|
12
|
+
- `memory_nightly` 夜间整理(默认 dry-run)
|
|
13
|
+
- `memory_shared` 共享记忆(list/propose/approve/revoke)
|
|
14
|
+
- `cordis.patch.yml`:bundle patch,profile 列出包名即挂载
|
|
15
|
+
- `package.json` 增加 `dsh.bundle.patch`、`exports["./plugin"]`、`engines.dsh`
|
|
16
|
+
- 数据目录默认 `~/.dsh/memory-graph`,默认 scope `agent:memory`
|
|
17
|
+
|
|
18
|
+
### 兼容
|
|
19
|
+
- 没有 `dsh-tools` 时用内置 `defineTool` 兜底(参数映射自动转 JSON Schema)
|
|
20
|
+
- 单个工具注册失败不影响其它工具
|
|
21
|
+
|
|
22
|
+
### 测试
|
|
23
|
+
47 → **51**(新增插件注册、schema、体检/夜间、共享流程)
|
|
24
|
+
|
|
25
|
+
## 0.6.0 — 2026-11-07
|
|
26
|
+
|
|
27
|
+
收口这一版,目标是"别人敢用"。
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- **矛盾检测**(`conflicts.js`)
|
|
32
|
+
- 同一类别下"喜欢 / 不喜欢"这类极性翻转 → 旧的标 `superseded`,召回默认只给新的
|
|
33
|
+
- `scanConflicts()` 批量扫描历史遗留矛盾
|
|
34
|
+
- `includeSuperseded` 可以显式把旧说法也召回
|
|
35
|
+
- **多进程写锁**:`store.append` 走目录级 `.lock`(mkdir 原子),等待超时 3s,10s 的陈旧锁自动清理
|
|
36
|
+
- **损坏修复**(`repair.js`):`scanStore` 只检查,`repairStore({apply:true})` 删坏行并备份原文件
|
|
37
|
+
- **TypeScript 声明**:手写 `index.d.ts`,覆盖全部主要 API
|
|
38
|
+
- **性能优化**
|
|
39
|
+
- 批量写:`appendMany` 一次锁一次写
|
|
40
|
+
- 缓存就地更新:写入后不再让整个 scope 缓存失效
|
|
41
|
+
- bigram 缓存:召回时不再重复切词
|
|
42
|
+
- 召回回写节流:30 秒内同一节点不重复累加 `recallCount`
|
|
43
|
+
- **基准** `bin/bench.mjs`:2000 节点实测写入 ~1400 条/s、纯召回 0.42ms/次
|
|
44
|
+
- **CLI**:`scan` / `repair` 命令
|
|
45
|
+
|
|
46
|
+
### 测试
|
|
47
|
+
|
|
48
|
+
39 → **47**
|
|
49
|
+
|
|
50
|
+
## 0.5.0 — 2026-11-07
|
|
51
|
+
|
|
52
|
+
把「记忆存储」补成「能接进 agent 的记忆系统」,同时补工程化。
|
|
53
|
+
|
|
54
|
+
### 新增
|
|
55
|
+
|
|
56
|
+
- `extractor.js`:从对话自动抽记忆
|
|
57
|
+
- `heuristicExtract(messages)`:规则 + 事实启发式,零模型
|
|
58
|
+
- `extractAndWrite(memory, scope, messages)`:过滤敏感 → 校验锚点 → 幂等去重 → 落盘
|
|
59
|
+
- `buildExtractionPrompt(messages)`:给大模型抽取用的提示词
|
|
60
|
+
- `assemble.js`:按 **token 预算**把召回结果拼成提示词块(事实 / 在办的事 / 印象 / 共享记忆分区,带 id)
|
|
61
|
+
- `snapshot.js`:整个数据目录导出成**单个 JSON 包**,可导入还原(换机器、误删恢复)
|
|
62
|
+
- `SharedMemory`
|
|
63
|
+
- 每日提名配额(默认 3 条/天)
|
|
64
|
+
- `dependsOn` 依赖 + 撤销时**级联撤销**下游
|
|
65
|
+
- 审计日志(`propose` / `approve` / `revoke`,独立 `shared:audit` 作用域)
|
|
66
|
+
- `JsonlStore` 内存缓存:按文件 mtime 失效,写入后自动失效,避免每次召回全量解析
|
|
67
|
+
- 关系边强度:`relations[].weight` + `decayRelations()`(默认半年半衰期,低于阈值摘边)
|
|
68
|
+
- 人物关联:`relations[].type === 'person'` 可指向人物层记录(`options.personIds` 白名单)
|
|
69
|
+
- GitHub Actions:Node 20/22/24 三版本跑测试
|
|
70
|
+
|
|
71
|
+
### 测试
|
|
72
|
+
|
|
73
|
+
31 → **39** 个
|
|
74
|
+
|
|
75
|
+
## 0.4.0 — 2026-11-07
|
|
76
|
+
|
|
77
|
+
- 规则抽取(主人规矩 / 她自己的承诺)
|
|
78
|
+
- 敏感内容过滤(路径 / QQ号 / 凭据 / 令牌)
|
|
79
|
+
- 锚点强校验(引文必须真的在原文里)
|
|
80
|
+
- 幂等键去重(`idempotencyKey` / `mentionKey`)
|
|
81
|
+
- 人物层(登记 / 别名 / 合并审批)
|
|
82
|
+
- 时段轨迹 + 漏检扫描
|
|
83
|
+
- 六项体检
|
|
84
|
+
- 人格旋钮(六旋钮 + 三重刹车,角色扮演层)
|
|
85
|
+
- 星图可视化(SVG + Mermaid)
|
|
86
|
+
|
|
87
|
+
## 0.3.0 — 2026-11-07
|
|
88
|
+
|
|
89
|
+
- 带锚点的上下文压缩(prune / compact / stats,`expand()` 可取回原文)
|
|
90
|
+
- 保证「压缩必须变小」的保护
|
|
91
|
+
|
|
92
|
+
## 0.2.0 — 2026-11-07
|
|
93
|
+
|
|
94
|
+
- 跨 scope 召回(合并打分、带来源)
|
|
95
|
+
- 共享记忆(propose → approve → revoke)
|
|
96
|
+
- plan 生命周期(`validTo` 过期)
|
|
97
|
+
- 关系边(6 种类型,召回可沿边扩展)
|
|
98
|
+
- 夜间入口 `nightly()`
|
|
99
|
+
- 命令行 `bin/cli.mjs`
|
|
100
|
+
|
|
101
|
+
## 0.1.0 — 2026-11-07
|
|
102
|
+
|
|
103
|
+
- 图记忆内核:锚点强制、四层衰减、写入去重、重要度、召回
|
|
104
|
+
- 即时记忆(6h 醒 / 6h 睡 / 墓碑 / 复活升级)
|
|
105
|
+
- 夜间整理 + 体检
|
|
106
|
+
- JSONL 存储(last-wins、按月分片)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 浮水梦华
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,298 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-memory-graph-chiyo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 可回溯、会遗忘、会睡觉整理的记忆内核。给 AI agent 用的长期记忆图。
|
|
4
|
+
|
|
5
|
+
[](https://github.com/fushui-menghua/dsh-memory-graph-chiyo)
|
|
6
|
+
[](https://github.com/fushui-menghua/dsh-memory-graph-chiyo/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## 为什么又一个记忆库
|
|
9
|
+
|
|
10
|
+
现在的开源记忆方案大致五条路(2026 年现状):
|
|
11
|
+
|
|
12
|
+
| 路线 | 代表 | 记忆形态 | 检索 | 短板 |
|
|
13
|
+
|---|---|---|---|---|
|
|
14
|
+
| A. 向量 RAG | 传统向量库 | 文本块 | 向量相似 | 谁说的、什么时候、还算不算数,全丢 |
|
|
15
|
+
| B. 抽取 + 混合检索 | Mem0 | 结构化事实卡 | 语义+关键词+实体 | 事实是模型重写的,**原文不可回溯** |
|
|
16
|
+
| C. 时序知识图谱 | Zep / Graphiti | 实体关系图 + 时间区间 | 图遍历 + 时间过滤 | 状态变化处理最强,但重 |
|
|
17
|
+
| D. OS 分层记忆 | Letta / MemGPT | 内存页 + 归档 | 模型自己搬页 | 依赖模型自律,容易漂 |
|
|
18
|
+
| E. 本体图谱 | Cognee | 本体 + 图 | 图查询 | 要设计 schema,接入成本高 |
|
|
19
|
+
|
|
20
|
+
**本项目走 B+C 的混合,但多三件事:**
|
|
21
|
+
|
|
22
|
+
1. **锚点强制(anchors)** —— fact / plan 必须能指回原话片段,否则拒收。
|
|
23
|
+
2. **四层衰减** —— instant → short → long → cold;不是只增不减的仓库。
|
|
24
|
+
3. **夜间整理(consolidation)** —— 归档重复与墓碑,把提过多次、规则承诺类升层。
|
|
25
|
+
|
|
26
|
+
一句话:**它记得住,也忘得掉,而且每晚自己整理。**
|
|
27
|
+
|
|
28
|
+
## 能力清单
|
|
29
|
+
|
|
30
|
+
| 能力 | 说明 |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 锚点强制 | `fact`/`plan` 必须带原话;`impression` 可无 |
|
|
33
|
+
| 写入即去重 | 规范化文本相同 → 合并 `mentionCount` |
|
|
34
|
+
| 四层衰减 | `short`→`long`(提 3 次满 2 天);长期不用 →`cold` |
|
|
35
|
+
| 重要度 | 提及 + 召回 + 锚点 + 类别 + 新鲜度加权,召回即反哺 |
|
|
36
|
+
| 召回 | 字符 bigram + 关键词 + 重要度 + 新鲜度;过滤冷层/归档/过期 |
|
|
37
|
+
| **跨 scope 召回** | 多 scope 合并打分,结果带 `scope` 来源 |
|
|
38
|
+
| **关系边** | `person`/`relationship`/`because`/`derivedFrom`/`supports`/`contradicts`;召回可沿边扩展一跳(进出边都算) |
|
|
39
|
+
| **plan 生命周期** | 有 `validTo`,过期自动 `expired`,过期后不再召回 |
|
|
40
|
+
| 即时记忆 | 醒 6h → 睡 6h → 墓碑;精确复活;复活 3 次升级成 fact |
|
|
41
|
+
| **共享记忆** | propose → approve → 跨会话可见 → revoke;未批准不可召回 |
|
|
42
|
+
| 夜间整理 | 归档重复/墓碑/极小陈旧;强化规则承诺类 |
|
|
43
|
+
| 体检 | 分层、类别、锚点覆盖率、90 天陈旧数 |
|
|
44
|
+
| **上下文压缩(带锚点)** | 超阈值折叠旧轮次 → 摘要 + 锚点 + 原文可 `expand()` 取回;工具输出先掐头去尾 |
|
|
45
|
+
| **规则抽取** | 从原话认出「主人定的规矩」和「她自己的承诺」,自动生成带锚点的规则节点 |
|
|
46
|
+
| **敏感过滤** | 路径 / QQ号 / 凭据 / 令牌写入前拒收,可逐类放行 |
|
|
47
|
+
| **锚点强校验** | 传了原始消息时,引文必须真的在里面,防模型编造 |
|
|
48
|
+
| **幂等去重** | `idempotencyKey` / `mentionKey`,同一件事换说法也只记一次 |
|
|
49
|
+
| **人物层** | 登记、别名解析、合并提议与审批 |
|
|
50
|
+
| **时段轨迹 / 漏检扫描** | 回看某段时间发生了什么;找出没被任何锚点引用过的话 |
|
|
51
|
+
| **人格旋钮(可选)** | 六个性格旋钮,带单步/每日/漂移三重刹车(角色扮演层,非记忆) |
|
|
52
|
+
| **星图可视化** | 零依赖 SVG + Mermaid 输出,可直接贴 README |
|
|
53
|
+
| **自动抽取管道** | `extractAndWrite`:从对话抽规则/事实 → 敏感过滤 → 锚点校验 → 幂等去重 → 落盘 |
|
|
54
|
+
| **提示词装配** | `assembleContext`:按 token 预算分区拼装(记得的事/在办的事/印象/共享) |
|
|
55
|
+
| **快照导入导出** | 整个数据目录导成单个 JSON,可还原(换机器、误删恢复) |
|
|
56
|
+
| **关系强度衰减** | 边有 weight,默认半年半衰期,低于阈值自动摘边 |
|
|
57
|
+
| **共享审计与配额** | 每日提名上限、依赖级联撤销、独立审计日志 |
|
|
58
|
+
| **store 缓存** | 按文件 mtime 失效,写入即失效,召回不必全量解析 |
|
|
59
|
+
| **矛盾检测** | 喜欢/不喜欢这类翻转 → 旧的标 `superseded`,只召回新的 |
|
|
60
|
+
| **多进程写锁** | 目录级锁 + 超时 + 陈旧锁清理 |
|
|
61
|
+
| **损坏修复** | `scanStore` / `repairStore`,坏行剔除并备份原文件 |
|
|
62
|
+
| **TypeScript 类型** | 内置 `index.d.ts` |
|
|
63
|
+
| **性能基准** | `npm run bench`:2000 节点写入 ~1400 条/s、召回 0.42ms |
|
|
64
|
+
| 命令行 | `memory-graph health/recall/recall-all/nightly/shared-*/scan/repair` |
|
|
65
|
+
| 零依赖 | 只用 Node 内置模块 |
|
|
66
|
+
|
|
67
|
+
## 安装
|
|
68
|
+
|
|
69
|
+
**作为库(推荐给开发者)**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm i dsh-memory-graph-chiyo
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**作为 DSH 插件(推荐给 DSH 用户)**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dsh plugin --profile web add dsh-memory-graph-chiyo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
装上后 DSH 会注册 5 个工具:`memory_write` / `memory_recall` / `memory_health` / `memory_nightly` / `memory_shared`。
|
|
82
|
+
数据默认在 `~/.dsh/memory-graph`,默认 scope `agent:memory`(可在 plugin 配置里改)。
|
|
83
|
+
|
|
84
|
+
> 插件与库是同一个包:`import` 用 `./src/index.js`,DSH 走 `./plugin.js` 的 `apply(ctx)`。
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm i dsh-memory-graph-chiyo
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
要求 Node ≥ 20。
|
|
92
|
+
|
|
93
|
+
## 快速开始
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { createMemory } from 'dsh-memory-graph-chiyo';
|
|
97
|
+
|
|
98
|
+
const mem = createMemory({ dir: './memory-data' });
|
|
99
|
+
|
|
100
|
+
// 1) 写:fact 必须带锚点
|
|
101
|
+
mem.write('user:alice', {
|
|
102
|
+
nodes: [
|
|
103
|
+
{ kind: 'fact', text: '她喜欢下雨天', anchors: [{ quote: '我最喜欢下雨了' }] },
|
|
104
|
+
{ kind: 'fact', text: '她怕黑', anchors: [{ quote: '我怕黑' }] },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 2) 建立关系边(目标必须已存在)
|
|
109
|
+
const fear = mem.load('user:alice').find((n) => /怕黑/.test(n.text));
|
|
110
|
+
mem.write('user:alice', {
|
|
111
|
+
nodes: [{ kind: 'fact', text: '睡觉要留一盏小灯', anchors: [{ quote: '留个灯' }], relations: [{ type: 'because', targetId: fear.id }] }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 3) 召回(hops=1 沿关系扩展)
|
|
115
|
+
const hits = mem.recall('user:alice', { query: '怕黑', hops: 1 });
|
|
116
|
+
|
|
117
|
+
// 4) 跨 scope 召回
|
|
118
|
+
mem.recallAcross(['user:alice', 'agent:chiyo'], { query: '下雨' });
|
|
119
|
+
|
|
120
|
+
// 5) 计划(3 天后自动过期)
|
|
121
|
+
mem.write('user:alice', { nodes: [{ kind: 'plan', text: '周末去看海', anchors: [{ quote: '周末去海边' }], validDays: 3 }] });
|
|
122
|
+
|
|
123
|
+
// 6) 共享记忆:提名 → 批准 → 可召回
|
|
124
|
+
const s = mem.proposeShared({ text: '雨天适合听老唱片', sourceScope: 'group:demo' });
|
|
125
|
+
mem.approveShared(s.id, { by: 'owner' });
|
|
126
|
+
mem.recallShared({ query: '雨天' });
|
|
127
|
+
|
|
128
|
+
// 7) 睡一觉:推进层/即时/计划 + 整理 + 体检
|
|
129
|
+
const report = mem.nightly(['user:alice', 'agent:chiyo'], { apply: true });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 上下文压缩(带锚点)
|
|
133
|
+
|
|
134
|
+
普通压缩的毛病:摘要写完,原话就找不回来了。这里给摘要挂 **anchors**(被折叠事件的 id),
|
|
135
|
+
被折叠的原文默认从 `shadowed` 返回,随时能取回。
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { createCompactor } from 'dsh-memory-graph-chiyo';
|
|
139
|
+
|
|
140
|
+
const comp = createCompactor({
|
|
141
|
+
windowTokens: 128000, // 模型上下文窗口
|
|
142
|
+
thresholdRatio: 0.3, // 超过 30% 才压
|
|
143
|
+
keepTurns: 10, // 保留最近 10 轮
|
|
144
|
+
pruner: { thresholdChars: 8000, headChars: 6000, tailChars: 1500 },
|
|
145
|
+
// summarizer: async (events) => '用你的模型写摘要', // 可选,默认是确定性抽取
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const r = await comp.compact(events);
|
|
149
|
+
// r.compacted 是否真的压了
|
|
150
|
+
// r.events 新上下文:摘要块 + 最近 N 轮
|
|
151
|
+
// r.anchors 被折叠事件的 id 列表(摘要里也写了一份)
|
|
152
|
+
// r.shadowed 被折叠的原文,存哪儿由你决定
|
|
153
|
+
// r.beforeTokens / r.afterTokens
|
|
154
|
+
|
|
155
|
+
// 想核对摘要对不对:
|
|
156
|
+
for (const e of comp.expand(r)) console.log(e.id, e.text);
|
|
157
|
+
|
|
158
|
+
// 审计每次压缩:
|
|
159
|
+
console.log(comp.stats());
|
|
160
|
+
// [{ at, beforeK, afterK, freedK, folded, keptTurns }]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
三层策略(跟她实际在用的那套一致):
|
|
164
|
+
|
|
165
|
+
| 层 | 做什么 | 什么时候 |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| prune | 工具输出掐头去尾 | 单条超过 `thresholdChars` |
|
|
168
|
+
| compact | 旧轮次折叠成摘要 | 总占用超过 `windowTokens × thresholdRatio` |
|
|
169
|
+
| stats | 记录放掉多少、留几轮 | 每次压缩后 |
|
|
170
|
+
|
|
171
|
+
**保证不变大的保护**:如果摘要比原文还长(窗口很小时会这样),会先截短;实在压不下去就跳过并返回 `note`,不会假装压过。
|
|
172
|
+
|
|
173
|
+
## 命令行
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# 体检
|
|
177
|
+
npx memory-graph health --dir ./data --scope user:alice
|
|
178
|
+
|
|
179
|
+
# 召回
|
|
180
|
+
npx memory-graph recall --dir ./data --scope user:alice --query 下雨
|
|
181
|
+
|
|
182
|
+
# 跨 scope
|
|
183
|
+
npx memory-graph recall-all --dir ./data --scopes user:alice,agent:chiyo --query 下雨
|
|
184
|
+
|
|
185
|
+
# 夜间整理(dry-run / --apply)
|
|
186
|
+
npx memory-graph nightly --dir ./data --scopes user:alice --apply
|
|
187
|
+
|
|
188
|
+
# 共享记忆
|
|
189
|
+
npx memory-graph shared-list --dir ./data
|
|
190
|
+
npx memory-graph shared-approve --dir ./data --id s_xxx --by owner
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
配合系统计划任务,每天凌晨自动整理:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Linux / macOS
|
|
197
|
+
0 3 * * * cd /path/to/app && npx memory-graph nightly --dir ./data --scopes user:alice --apply
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 存储
|
|
201
|
+
|
|
202
|
+
JSONL,按 scope 和月份分文件:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
memory-data/
|
|
206
|
+
├─ user_alice/2026-11.jsonl
|
|
207
|
+
├─ agent_chiyo/2026-11.jsonl
|
|
208
|
+
└─ shared_global/2026-11.jsonl ← 共享记忆独立存放
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- **last-wins**:同 id 以最后一条为准,改状态不改历史
|
|
212
|
+
- **可 grep**:你的记忆是你自己的
|
|
213
|
+
- **崩溃安全**:最多丢最后一行
|
|
214
|
+
|
|
215
|
+
## 核心概念
|
|
216
|
+
|
|
217
|
+
| 概念 | 说明 |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `scope` | `user:alice` / `agent:chiyo` / `group:demo`,隔离单位 |
|
|
220
|
+
| `kind` | `fact`(有锚点)/ `impression`(主观)/ `plan`(有失效期) |
|
|
221
|
+
| `layer` | `instant` / `short` / `long` / `cold` / `permanent` |
|
|
222
|
+
| `anchors` | 原话片段,用于回溯与验证 |
|
|
223
|
+
| `relations` | 节点间的边,召回可扩散 |
|
|
224
|
+
| `status` | plan 专用:`active` / `done` / `abandoned` / `expired` |
|
|
225
|
+
|
|
226
|
+
## 规则抽取 / 人物 / 时间线 / 体检 / 星图
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
// 规则:从原话里认出规矩与承诺
|
|
230
|
+
const rules = mem.extractRules('以后不要在深夜给我发消息', { speaker: 'owner' });
|
|
231
|
+
// → [{ kind:'fact', text:'以后不要在深夜给我发消息', cluster:'rule-owner', anchors:[...] }]
|
|
232
|
+
mem.write('user:alice', { nodes: rules });
|
|
233
|
+
|
|
234
|
+
// 人物:登记 → 别名解析 → 合并审批
|
|
235
|
+
mem.person.upsert({ name: '阿岚', aliases: ['小岚'] });
|
|
236
|
+
const p = mem.person.proposeMerge('阿岚', '岚岚');
|
|
237
|
+
mem.person.approveMerge(p.id, { by: 'owner' });
|
|
238
|
+
|
|
239
|
+
// 时间线:这段时间发生了什么 + 哪些话没进记忆
|
|
240
|
+
mem.episode({ scope: 'user:alice', from: Date.now() - 86400_000 });
|
|
241
|
+
mem.missedScan({ scope: 'user:alice', messages });
|
|
242
|
+
|
|
243
|
+
// 体检:六项指标
|
|
244
|
+
mem.healthReport({ scope: 'user:alice' });
|
|
245
|
+
// { checks: [锚点覆盖率, 重复率, 长期占比, 即时占比, 陈旧度, 归档与共享], ok, failed }
|
|
246
|
+
|
|
247
|
+
// 星图:SVG 或 Mermaid
|
|
248
|
+
const svg = mem.starMap(mem.load('user:alice'), { title: '她的记忆星图' });
|
|
249
|
+
const mmd = mem.mermaid(mem.load('user:alice'));
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 人格旋钮(可选,跟记忆无关)
|
|
253
|
+
|
|
254
|
+
> ⚠️ 这一层是**角色扮演**,不是记忆。记忆回答"发生过什么",人格回答"用什么性格说话"。
|
|
255
|
+
> 不需要的话完全不调用即可,不影响其它能力。
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
mem.persona.get(); // { tsundere, playfulness, initiative, softness, snark, moodSwing }
|
|
259
|
+
mem.persona.tune('initiative', 0.05, { by: 'owner', reason: '希望更主动' });
|
|
260
|
+
mem.persona.summary(); // 「傲娇 中等;爱玩 偏高;主动 很高…」
|
|
261
|
+
mem.persona.revert();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
三重刹车(防止人格一夜之间漂走):
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
单步 ≤ 0.05 · 每天最多 1 次 · 距基线累计偏移 ≤ 0.15
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 范围说明
|
|
271
|
+
|
|
272
|
+
- **长期记忆**:跨会话的节点、衰减、关系、整理(本项目的核心)
|
|
273
|
+
- **上下文压缩**:单次对话太长时腾出窗口,摘要带锚点可回溯(v0.3.0 起内置)
|
|
274
|
+
|
|
275
|
+
两者互补:压缩管"这一次对话装不下",整理管"跨天该忘什么"。
|
|
276
|
+
|
|
277
|
+
## 测试
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npm test
|
|
281
|
+
# 47 tests: 锚点 / 召回 / 去重 / 即时记忆 / 整理 / 层推进 / 体检
|
|
282
|
+
跨 scope / 共享审批 / plan 过期 / 关系边扩展 / nightly 全流程
|
|
283
|
+
压缩 / 规则抽取 / 敏感过滤 / 锚点强校验 / 幂等键 / 人物合并 / 时间线 / 漏检
|
|
284
|
+
自动抽取 / 提示词装配 / 快照往返 / 关系衰减 / 共享审计配额 / store 缓存
|
|
285
|
+
矛盾检测 / 矛盾扫描 / 写锁超时 / 陈旧锁 / 损坏修复 / d.ts 完整 / 召回性能护栏
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 设计取舍
|
|
289
|
+
|
|
290
|
+
- **不做嵌入向量**:默认字符 bigram + 关键词 + 重要度,零依赖、可解释;
|
|
291
|
+
要语义检索可自行替换 `recall`(纯函数,好接 embedding)。
|
|
292
|
+
- **不自动摘要**:摘要会丢掉原话,而原话是这套系统的地基。
|
|
293
|
+
- **不删除**:归档只是 `archived: true`,永远可恢复。
|
|
294
|
+
- **共享要审批**:默认只能提名,批准后才跨会话可见。
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT © 浮水梦华
|
package/bin/bench.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bench.mjs —— 性能基准:生成 N 条记忆,测写入/召回/整理耗时
|
|
4
|
+
*
|
|
5
|
+
* node bin/bench.mjs 10000
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { createMemory } from '../src/index.js';
|
|
11
|
+
|
|
12
|
+
const N = Number(process.argv[2]) || 10000;
|
|
13
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'chiyo-bench-'));
|
|
14
|
+
const mem = createMemory({ dir });
|
|
15
|
+
|
|
16
|
+
const words = ['下雨', '猫', '深夜', '海边', '唱片', '咖啡', '书', '散步', '旧照片', '风'];
|
|
17
|
+
const t0 = Date.now();
|
|
18
|
+
let batch = [];
|
|
19
|
+
let writeErrors = 0;
|
|
20
|
+
for (let i = 0; i < N; i += 1) {
|
|
21
|
+
const w = words[i % words.length];
|
|
22
|
+
batch.push({ kind: i % 7 === 0 ? 'impression' : 'fact', text: `第 ${i} 条:她${w}相关的事 ${i % 97}`, anchors: [{ quote: `${w} ${i}` }], category: 'preference' });
|
|
23
|
+
// 单次最多写 5 条(options.maxNodesPerWrite),批量按 5 切
|
|
24
|
+
if (batch.length === 5) {
|
|
25
|
+
try { mem.write('user:bench', { nodes: batch, allowSensitive: true }); } catch (e) { writeErrors += 1; }
|
|
26
|
+
batch = [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (batch.length) mem.write('user:bench', { nodes: batch, allowSensitive: true });
|
|
30
|
+
const writeMs = Date.now() - t0;
|
|
31
|
+
|
|
32
|
+
const t1 = Date.now();
|
|
33
|
+
let hits = 0;
|
|
34
|
+
for (let i = 0; i < 200; i += 1) hits += mem.recall('user:bench', { query: words[i % words.length], limit: 20, track: false }).length;
|
|
35
|
+
const recallMs = Date.now() - t1;
|
|
36
|
+
const t1b = Date.now();
|
|
37
|
+
for (let i = 0; i < 50; i += 1) mem.recall('user:bench', { query: words[i % words.length], limit: 20 });
|
|
38
|
+
const recallTrackedMs = Date.now() - t1b;
|
|
39
|
+
|
|
40
|
+
const t2 = Date.now();
|
|
41
|
+
const cons = mem.consolidate('user:bench', { apply: false });
|
|
42
|
+
const consMs = Date.now() - t2;
|
|
43
|
+
|
|
44
|
+
const live = mem.load('user:bench').filter((n) => !n.archived).length;
|
|
45
|
+
console.log(JSON.stringify({
|
|
46
|
+
nodes: live,
|
|
47
|
+
writeMs,
|
|
48
|
+
writePerSec: Math.round((live / Math.max(1, writeMs)) * 1000),
|
|
49
|
+
writeErrors,
|
|
50
|
+
recall200PureMs: recallMs,
|
|
51
|
+
recallPureAvgMs: Number((recallMs / 200).toFixed(2)),
|
|
52
|
+
recall50TrackedMs: recallTrackedMs,
|
|
53
|
+
recallTrackedAvgMs: Number((recallTrackedMs / 50).toFixed(2)),
|
|
54
|
+
hits,
|
|
55
|
+
consolidateMs: consMs,
|
|
56
|
+
candidates: { archive: cons.archived, reinforce: cons.reinforced },
|
|
57
|
+
dir,
|
|
58
|
+
}, null, 2));
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli.mjs —— 命令行入口
|
|
4
|
+
*
|
|
5
|
+
* node bin/cli.mjs health --dir ./data --scope user:alice
|
|
6
|
+
* node bin/cli.mjs recall --dir ./data --scope user:alice --query 下雨
|
|
7
|
+
* node bin/cli.mjs nightly --dir ./data --scopes user:alice,agent:chiyo [--apply]
|
|
8
|
+
* node bin/cli.mjs shared-list --dir ./data
|
|
9
|
+
* node bin/cli.mjs shared-approve --dir ./data --id s_xxx --by owner
|
|
10
|
+
*/
|
|
11
|
+
import { createMemory } from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const out = { _: [] };
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a.startsWith('--')) {
|
|
18
|
+
const key = a.slice(2);
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (next === undefined || next.startsWith('--')) out[key] = true;
|
|
21
|
+
else { out[key] = next; i += 1; }
|
|
22
|
+
} else out._.push(a);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = parseArgs(process.argv.slice(2));
|
|
28
|
+
const cmd = args._[0];
|
|
29
|
+
const dir = String(args.dir ?? './memory-data');
|
|
30
|
+
const memory = createMemory({ dir });
|
|
31
|
+
const scope = args.scope ? String(args.scope) : null;
|
|
32
|
+
const scopes = args.scopes ? String(args.scopes).split(',').map((s) => s.trim()).filter(Boolean) : (scope ? [scope] : []);
|
|
33
|
+
|
|
34
|
+
if (!cmd) {
|
|
35
|
+
console.log('用法: cli.mjs <health|recall|recall-all|tick|consolidate|nightly|shared-list|shared-approve|shared-revoke> [--dir ...] [--scope ...] [--query ...] [--apply]');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
if (!scope && !['nightly', 'shared-list', 'shared-approve', 'shared-revoke', 'scan', 'repair'].includes(cmd)) {
|
|
39
|
+
console.error('缺少 --scope(或命令不适用)');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (cmd) {
|
|
44
|
+
case 'health': {
|
|
45
|
+
console.log(JSON.stringify(memory.health(scope), null, 2));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'recall': {
|
|
49
|
+
const hits = memory.recall(scope, { query: String(args.query ?? ''), limit: Number(args.limit ?? 10), includeCold: args.cold === true });
|
|
50
|
+
for (const h of hits) console.log(`[${h.kind}/${h.layer}] ${h.text} (score ${h.score})`);
|
|
51
|
+
if (!hits.length) console.log('(没有命中)');
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'recall-all': {
|
|
55
|
+
const hits = memory.recallAcross(scopes.length ? scopes : [scope], { query: String(args.query ?? ''), limit: Number(args.limit ?? 15) });
|
|
56
|
+
for (const h of hits) console.log(`[${h.scope}] [${h.kind}/${h.layer}] ${h.text} (score ${h.score})`);
|
|
57
|
+
if (!hits.length) console.log('(没有命中)');
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'tick': {
|
|
61
|
+
const changes = memory.tickLayers(scope, { apply: args.apply === true });
|
|
62
|
+
console.log(`层变化 ${changes.length} 条${args.apply ? '(已写入)' : '(dry-run)'}`);
|
|
63
|
+
for (const c of changes) console.log(` ${c.from} → ${c.to} ${c.text}`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'consolidate': {
|
|
67
|
+
const r = memory.consolidate(scope, { apply: args.apply === true });
|
|
68
|
+
console.log(JSON.stringify({ scope: r.scope, total: r.total, archived: r.archived, reinforced: r.reinforced, apply: r.apply }, null, 2));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'scan': {
|
|
72
|
+
console.log(JSON.stringify(memory.scanStore({ dir }), null, 2));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'repair': {
|
|
76
|
+
console.log(JSON.stringify(memory.repairStore({ dir, apply: args.apply === true }), null, 2));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'nightly': {
|
|
80
|
+
const r = memory.nightly(scopes, { apply: args.apply === true });
|
|
81
|
+
console.log(JSON.stringify(r, null, 2));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'shared-list': {
|
|
85
|
+
const rows = memory.listShared({ status: String(args.status ?? '') });
|
|
86
|
+
for (const e of rows) console.log(`[${e.status}] ${e.id} ${e.text.slice(0, 60)} (from ${e.sourceScope})`);
|
|
87
|
+
if (!rows.length) console.log('(没有共享记忆)');
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'shared-approve': {
|
|
91
|
+
const e = memory.approveShared(String(args.id), { by: String(args.by ?? 'owner') });
|
|
92
|
+
console.log(`已批准:${e.id} ${e.text.slice(0, 60)}`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'shared-revoke': {
|
|
96
|
+
const e = memory.revokeShared(String(args.id), { by: String(args.by ?? 'owner'), reason: String(args.reason ?? '') });
|
|
97
|
+
console.log(`已撤销:${e.id}`);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
console.error(`未知命令:${cmd}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# dsh-memory-graph-chiyo —— DSH bundle patch
|
|
3
|
+
# 装上之后,profile 在 dsh.profile.bundles 里列出本包名即生效:
|
|
4
|
+
# dsh plugin --profile web add dsh-memory-graph-chiyo
|
|
5
|
+
# ============================================================================
|
|
6
|
+
- insert:
|
|
7
|
+
- id: memory-graph
|
|
8
|
+
name: dsh-memory-graph-chiyo
|
|
9
|
+
config:
|
|
10
|
+
# 数据目录默认为 ~/.dsh/memory-graph,scope 默认为 agent:memory
|
|
11
|
+
scope: agent:memory
|