Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
31d9f95
Fix: 添加了未提取记忆补偿功能
TriDefender Aug 10, 2026
3df3614
Feat: 添加遗忘曲线门控
TriDefender Aug 11, 2026
d00440e
Merge branch 'adoresever:desktop-2.0' into desktop-2.0
TriDefender Aug 11, 2026
2f7fa58
Fix: Fixed tests
TriDefender Aug 11, 2026
dcfad1d
Fix: 确保跨插件重启可以继承状态
TriDefender Aug 12, 2026
8ccfb79
移植主干改动1fdec04,避免全量更新communities
TriDefender Aug 15, 2026
492923c
Update integration.graph.test.ts
TriDefender Aug 15, 2026
499cb76
Added finegrain memory control on cron
TriDefender Aug 15, 2026
02c4bb2
Fix: fixed incoherent tests
TriDefender Aug 15, 2026
d1a7604
Fix: 修了一些阻塞问题
TriDefender Aug 18, 2026
5101f7c
Fix: 进行代码审计后捉了几个bug
TriDefender Aug 18, 2026
bb124b7
Fix 修复代码审计发现的并发、数据损坏与性能缺陷
TriDefender Aug 23, 2026
6009c28
chore: updated docs
TriDefender Aug 31, 2026
c3f6748
feat: add LLM failure cooldown guard (persistent config errors only)
TriDefender Aug 31, 2026
0afefdc
fix: guard duplicate register() and accept lowercase llm.baseUrl
TriDefender Aug 31, 2026
bba42eb
feat: add opt-in bounded raw-message retention (messageRetention)
TriDefender Aug 31, 2026
d247624
Fix: 按照Openclaw 规范声明 transcript fencing 语义
TriDefender Sep 1, 2026
fcfec61
Fix: Test synced passwords
TriDefender Sep 1, 2026
6bd0d7e
Fix: Context engine "graph-memory-pro" factory returned null instead …
TriDefender Sep 1, 2026
42b18e0
chore: remove dead code — legacy store APIs bypassed by Neo4j version…
TriDefender Sep 2, 2026
b5a18b1
Removed plugin residuals
TriDefender Sep 2, 2026
ff3e787
Merge branch 'desktop-2.0' of https://github.com/TriDefender/graph-me…
TriDefender Sep 2, 2026
db20f0a
refactor: dedupe shared implementations (Fixes #2)
TriDefender Sep 2, 2026
0ca2a11
Merge pull request #3 from TriDefender/refactor/dedupe-issue-2
TriDefender Sep 2, 2026
0575239
feat: reembed pipeline — void stale vectors + batch rebuild (graph-me…
TriDefender Sep 2, 2026
580a4e6
Feat: 重做Deprecate+Delete业务逻辑
TriDefender Sep 3, 2026
be142d8
Update integration.neo4j.test.ts
TriDefender Sep 3, 2026
0c9b85a
fix: deprecate lifecycle — autoDeprecateNodes status guard + legacy d…
TriDefender Sep 3, 2026
c548f2b
feat: 提取/召回 LLM 与 embedding 成本优化套件
TriDefender Sep 3, 2026
5d2bc33
chore: Performed code quality review
TriDefender Sep 3, 2026
d38d972
chore: local code audit results
TriDefender Sep 3, 2026
82cf3c5
chore: 代码质量提高
TriDefender Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ jobs:
run: npm test
env:
NEO4J_INTEGRATION: "1"
# 集成测试(尤其删除类的 retention 测试)要求显式指定 URI;
# runner 上的 service container 是一次性实例,7687 安全
NEO4J_TEST_URI: bolt://localhost:7687
# 与 service 的 NEO4J_AUTH 保持同一来源,供可配置凭据的测试使用。
NEO4J_TEST_USER: neo4j
NEO4J_TEST_PASSWORD: graphmemory
CI: "true"

shellcheck:
Expand Down
103 changes: 98 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The installer configures Neo4j to start at boot with a 3-tier no-sudo fallback:

## Manual Configuration

Install the local plugin, then make it the OpenClaw context engine:
Install the local plugin, then make it the OpenClaw context engine (**restart the gateway after changing config**: the plugin guards against duplicate `register()` — a second registration without `dispose()` reuses the active engine, so new config is never hot-reloaded):

```json
{
Expand Down Expand Up @@ -108,6 +108,99 @@ Anthropic direct (Claude) — drop `baseURL`, switch `provider`:

`embedding` is optional. When present, `dimensions` must match the Neo4j vector index dimension. For a fresh database, the plugin creates matching indexes during startup. If you change dimensions later, recreate the vector indexes or the Neo4j database.

### Memory decay (forgetting curve)

Each maintenance cycle scores every active node with a three-factor weighted model (recency + frequency + intrinsic) and bidirectionally transitions nodes across three tiers: `core` / `working` / `peripheral`. Two lifecycle stages sit on top of the forgetting curve (both on by default, configurable via `decay`): **auto-deprecation** — a `peripheral` node whose composite stays below `peripheralCompositeThreshold` and which has not been accessed for `autoDeprecateAfterDays` (30) days gets its edges severed and is marked `deprecated`; if the same knowledge is extracted or edited again, the node automatically revives to `active`. **purge** — any `deprecated` node (manual deprecation and merge losers included) is hard-deleted (`DETACH DELETE`, vectors included) after `purgeAfterDays` (60) days to reclaim storage.

The full formula, field mapping from the reference implementation, default-value rationale, and tuning guide live in **[`docs/decay.md`](docs/decay.md)**.

Minimal config (all fields optional, defaults shown):

```json
"decay": { "enabled": true }
```

Common overrides — for fuller control see `docs/decay.md` §4:

```json
"decay": {
"enabled": true,
"recencyHalfLifeDays": 30,
"peripheralCompositeThreshold": 0.15,
"workingAccessThreshold": 3
}
```

### Cron sessions

Sessions created by OpenClaw scheduled tasks can be configured independently of normal sessions. The host places the cron marker on the **sessionKey** (`sessionId` is a random UUID); real shapes are `cron:<jobId>`, `agent:<agentId>:cron:<jobId>`, or `agent:<agentId>:cron:<jobId>:run:<runId>`:

```json
"cron": {
"enabled": true,
"extract": true,
"finalizeAndMaintain": true
}
```

| Option | Default | Description |
| --- | --- | --- |
| `enabled` | `true` | Enable graph functionality inside cron sessions (recall injection + message buffering). When `false`, cron sessions skip automatic recall and message persistence; the `gm_*` tools remain available for explicit calls (manual escape hatch). |
| `extract` | `true` | Trigger knowledge extraction (LLM triples) in cron sessions via `afterTurn` / `compact`. When `false`, messages are still buffered and can be backfilled later with `openclaw graph-memory extract`. |
| `finalizeAndMaintain` | `true` | Run finalize (EVENT→SKILL promotion) and graph maintenance (decay / PageRank / communities) when a cron session ends. Disable when frequent cron runs make end-of-session global maintenance too costly. |

All three options default to **`true`**: cron sessions behave like normal sessions (recall, buffering, extraction, and end-of-session maintenance all enabled) unless explicitly disabled. `enabled: false` is the master switch — even with `extract` / `finalizeAndMaintain` set to `true`, nothing runs. Non-cron sessions are never affected by these options.

All three sub-options are optional; omitted fields keep the default `true` (e.g. with `"cron": { "extract": false }` only extraction is disabled — recall, buffering, and end-of-session maintenance stay on).

Caveat: when a cron job sets an explicit custom `sessionKey`, the host does not append the `cron` segment — such sessions cannot be detected and are treated as normal sessions.

### Raw message retention (messageRetention, opt-in)

Context compaction only changes what the model sees — it is **not authorization to delete persistent evidence**. The default `keep=all` never deletes any raw message (GmMessage) at zero overhead. To bound database growth, opt into bounded pruning; it runs at the tail of the graph maintenance chain and processes at most `batchSize` rows per cycle:

```json
"messageRetention": {
"keep": "referenced",
"batchSize": 500,
"dryRun": false
}
```

| Option | Default | Description |
| --- | --- | --- |
| `keep` | `"all"` | `all` = keep everything (default, zero behavior change); `referenced` = delete only messages that were extracted **and** actually produced knowledge; `recent` = `referenced` plus a time-window guard (requires at least one window option). |
| `recentTurns` | `0` | `keep=recent`: keep the newest N real user turns per session (that turn and everything after it). Sessions with no user messages are fully protected. |
| `retentionDays` | `0` | `keep=recent`: keep messages ingested within the last N days. |
| `batchSize` | `500` | Maximum rows processed per maintenance cycle (1–10000), keeping the chain bounded. |
| `dryRun` | `false` | `true` reports the candidate set without deleting — **run one `dryRun` cycle to validate candidates before enabling pruning**. |

Deletion semantics (fail-closed):

- Only messages with `extracted=true` **and** `producedKnowledge=true` (the extraction actually produced nodes/edges) enter the candidate set.
- Turns where the LLM returned zero nodes and zero edges are marked `producedKnowledge=false` — the raw evidence stays in the database. Such turns are never re-extracted automatically; to re-mine them with a better prompt/model, manually reset their `extracted` flag to `false` and re-run `openclaw graph-memory extract`.
- Unextracted messages and legacy rows (extracted before this flag existed, no `producedKnowledge` property) are never deleted.
- A failure in the retention step itself (invalid policy fails closed with zero deletions / Neo4j error) never invalidates the other maintenance steps; it retries next cycle.

### Re-embedding after switching embedding models

Embedding vectors are model-specific — vectors produced by the old model are not comparable
(different dimensions break dedup and vector search outright), and vectors are write-once with
no automatic migration. After changing `embedding.model` / `embedding.dimensions`, run:

```bash
openclaw graph-memory reembed --dry-run # report dimension match + vector coverage, no writes
openclaw graph-memory reembed # void all vectors and rebuild them in batches
```

The command voids every `MemoryNode.embedding` (and its `contentHash`, so the runtime
`syncEmbed` hash guard cannot short-circuit), then re-embeds all active nodes and community
summaries with the current model using batched requests (`--batch <n>`, default 32; failed
batches automatically fall back to per-item requests). If the vector index dimensions no longer
match the configured model, the run aborts — add `--recreate-index` to drop and recreate
`gm_node_embedding` / `gm_community_embedding` with the new dimensions. Items that fail stay
vectorless and are picked up by a re-run.

### OAuth login (experimental)

```bash
Expand All @@ -127,7 +220,7 @@ conversation messages -> GmMessage nodes -> LLM triple extraction
-> embeddings -> vector recall + community expansion + GDS PPR
-> XML context injection

session end -> dedup -> global PageRank -> communities -> summaries
session end -> decay (forgetting curve) -> dedup -> global PageRank -> communities -> summaries
```

## Verify
Expand Down Expand Up @@ -158,18 +251,18 @@ Inspect the graph with the bundled Cypher shell:
| --- | --- |
| `gm_search` | Recall graph knowledge for a query |
| `gm_record` | Add a knowledge node manually |
| `gm_update` | Update, delete, or deprecate an existing node by exact name. `mode=update` (default) refines description/content; `mode=delete` hard-deletes the node and all its relationships; `mode=deprecate` marks `[DEPRECATED]` and removes all relationships while keeping the node (throws if not found) |
| `gm_update` | Update or deprecate an existing node by exact name. `mode=update` (default) refines description/content; `mode=deprecate` marks `[DEPRECATED]`, removes all relationships, and makes the node unreachable from recall (equivalent to deletion; physically purged by maintenance after `purgeAfterDays`). `mode=delete` was removed — deprecate replaces it (throws if not found) |
| `gm_link` | Manually create or refine an edge between two existing nodes (validates type + direction against the whitelist; idempotent on from+to+type) |
| `gm_unlink` | Remove edges between two nodes by name; optional `type` filter, otherwise all from→to edges |
| `gm_merge` | Merge two same-type duplicate nodes: keep absorbs content/validatedCount/sessions + dedup-aware edge migration; merge is soft-deleted |
| `gm_stats` | Show node, relationship, community, and PageRank statistics |
| `gm_maintain` | Run deduplication, PageRank, and community maintenance |
| `gm_maintain` | Run decay scoring, deduplication, PageRank, and community maintenance |

## Development

```bash
npm install
npm run build # tsc --noEmit
npm run build # compiles to dist/
npm test # unit tests only (no Neo4j required)
```

Expand Down
107 changes: 103 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ bash setup-graph-memory-pro.sh --uninstall

## 手动配置

安装插件后,在 `~/.openclaw/openclaw.json` 中配置:
安装插件后,在 `~/.openclaw/openclaw.json` 中配置(**修改配置后需重启网关生效**:插件内置防重复注册守卫,宿主未 dispose 的二次 `register()` 会复用现有引擎,新配置不会热加载)

```json
{
Expand All @@ -62,6 +62,7 @@ bash setup-graph-memory-pro.sh --uninstall
"password": "你的 Neo4j 密码"
},
"llm": {
"provider": "openai",
"apiKey": "你的 LLM API Key",
"baseURL": "https://api.openai.com/v1",
"model": "gpt-4o-mini"
Expand All @@ -79,8 +80,106 @@ bash setup-graph-memory-pro.sh --uninstall
}
```

Anthropic 直连(Claude)——去掉 `baseURL`,切换 `provider`:

```json
"llm": {
"provider": "anthropic",
"apiKey": "sk-ant-...",
"model": "claude-3-5-sonnet-20241022"
}
```

`embedding` 可选。设置时,`dimensions` 必须与 Neo4j 向量索引维度一致。新数据库会在插件启动时按配置创建索引;更换维度后需要重建向量索引或 Neo4j 数据库。

### 记忆衰减(遗忘曲线)

每个维护周期对全部 active 节点做三因子加权评分(recency + frequency + intrinsic),并在三个 tier 之间双向转换:`core` / `working` / `peripheral`。遗忘曲线之上还有两段生命周期(默认开启,可通过 `decay` 配置):**自动弃用**——`peripheral` 层且 composite 持续低于 `peripheralCompositeThreshold`、超过 `autoDeprecateAfterDays`(30)天未访问的节点会被切断所有边并标记 `deprecated`;若同名知识之后被重新提取或编辑,节点自动复活回 `active`。**到期清理**——所有 `deprecated` 节点(含手动弃用与合并产物)自弃用起超过 `purgeAfterDays`(60)天后硬删(`DETACH DELETE`,向量随节点移除)以释放存储。

完整公式、字段映射、默认值依据与调参指南见 **[`docs/decay.md`](docs/decay.md)**。

最小配置(所有字段可选,默认值如下):

```json
"decay": { "enabled": true }
```

常用覆盖——完整参数见 `docs/decay.md` §4:

```json
"decay": {
"enabled": true,
"recencyHalfLifeDays": 30,
"peripheralCompositeThreshold": 0.15,
"workingAccessThreshold": 3
}
```

### cron 会话行为控制

OpenClaw 定时任务创建的会话可以独立配置图谱行为。host 把 cron 标记放在 **sessionKey** 上(`sessionId` 是随机 UUID),实际形状为 `cron:<jobId>`、`agent:<agentId>:cron:<jobId>` 或 `agent:<agentId>:cron:<jobId>:run:<runId>`:

```json
"cron": {
"enabled": true,
"extract": true,
"finalizeAndMaintain": true
}
```

| 选项 | 默认 | 说明 |
| --- | --- | --- |
| `enabled` | `true` | 是否在 cron 会话内启用图谱功能(召回注入 + 消息入库)。关闭后 cron 会话不自动召回、不自动入库;`gm_*` 工具仍可手动调用(作为显式逃生通道)。 |
| `extract` | `true` | 是否在 cron 会话内触发知识提取(afterTurn / compact 的 LLM 三元组提取)。关闭后消息仍入库缓冲,之后可用 `openclaw graph-memory extract` 手动回填。 |
| `finalizeAndMaintain` | `true` | cron 会话结束时是否执行 finalize(EVENT→SKILL 晋升)和图维护(decay / PageRank / 社区检测)。定时任务频繁时可关闭,避免每次会话结束都跑全局维护。 |

三个选项**默认全部开启**:cron 会话默认使用图谱,需按需显式关闭。`enabled=false` 是总开关:即使 `extract`/`finalizeAndMaintain` 设为 `true` 也不生效。非 cron 会话不受这些选项影响。三个子项均可省略,未写的字段取默认值 `true`。

注意:若 cron 任务显式设置了自定义 `sessionKey`,host 不再附加 `cron` 段,此类会话无法被识别,将按普通会话处理。

### 原始消息保留(messageRetention,opt-in)

上下文压缩只改变模型可见面,**不构成删除持久证据的授权**——默认 `keep=all` 永不删除任何原始消息(GmMessage),零开销。需要控制库体积时可显式开启有界清理,挂在图维护链尾部,每个维护周期最多处理 `batchSize` 行:

```json
"messageRetention": {
"keep": "referenced",
"batchSize": 500,
"dryRun": false
}
```

| 选项 | 默认 | 说明 |
| --- | --- | --- |
| `keep` | `"all"` | `all`=全部保留(默认,零行为变化);`referenced`=只删"已提取完成且实际产出知识"的消息;`recent`=在 `referenced` 基础上叠加时间窗保护(需至少配一个窗口参数)。 |
| `recentTurns` | `0` | `keep=recent`:每 session 保留最近 N 轮真实用户发言(该轮及其后的全部消息保留)。无 user 消息的 session 完全保护。 |
| `retentionDays` | `0` | `keep=recent`:保留最近 N 天内入库的消息。 |
| `batchSize` | `500` | 单个维护周期最多处理的行数(1~10000),保证维护链工作有界。 |
| `dryRun` | `false` | `true` 时只报告候选集不删除——**启用清理前建议先跑一轮 `dryRun` 验证候选集**。 |

删除语义(fail-closed):

- 只有 `extracted=true` **且** `producedKnowledge=true`(该轮 LLM 提取实际产出节点/边)的消息才会进入候选。
- LLM 空提取(成功返回零节点零边)的轮次标记 `producedKnowledge=false`,原始证据保留在库中。此类轮次不会自动重提;如需用更好的 prompt/模型重挖,需手动将这些行的 `extracted` 重置为 `false`,再运行 `openclaw graph-memory extract` 回填。
- 未提取消息、以及标记机制上线前的遗留行(无 `producedKnowledge` 属性)一律不删。
- 保留步骤自身失败(非法策略 fail-closed 不删 / Neo4j 故障)不影响维护链的其他步骤,下一周期自动重试。

### 更换 embedding 模型后的重嵌入

embedding 向量与模型绑定——旧模型产出的向量与新模型不可比(维度不同时去重与向量搜索直接失效),
且向量只写不删、没有自动迁移。更改 `embedding.model` / `embedding.dimensions` 后请执行:

```bash
openclaw graph-memory reembed --dry-run # 只报告维度对照与向量覆盖情况,不写入
openclaw graph-memory reembed # 清空全部向量并按批重建
```

该命令清空所有 `MemoryNode.embedding`(连同 `contentHash`,避免运行时 `syncEmbed`
被旧文本 hash 短路),然后用当前模型按批重建全部活跃节点与社区摘要向量(`--batch <n>`,
默认 32;批失败自动退化为逐条请求)。若向量索引维度与当前模型不符,命令会中止——加
`--recreate-index` 删除并按新维度重建 `gm_node_embedding` / `gm_community_embedding`。
失败条目保持无向量,修复端点后重跑本命令即可增量补齐。

### OAuth 登录(实验性)

```bash
Expand All @@ -100,7 +199,7 @@ openclaw graph-memory auth login
-> embedding -> 向量召回 + 社区扩展 + GDS PPR
-> XML 上下文注入

会话结束 -> 去重 -> 全局 PageRank -> 社区 -> 社区摘要
会话结束 -> 衰减(遗忘曲线)-> 去重 -> 全局 PageRank -> 社区 -> 社区摘要
```

## 验证
Expand Down Expand Up @@ -129,12 +228,12 @@ openclaw gateway --verbose
| --- | --- |
| `gm_search` | 按查询召回图谱知识 |
| `gm_record` | 手动记录知识节点 |
| `gm_update` | 按精确节点名称更新 / 删除 / 弃用已有节点(不存在则报错)。`mode=update`(默认)refine description/content;`mode=delete` 硬删除节点及其所有关系;`mode=deprecate` 标记 `[DEPRECATED]` 并删除所有关系(节点本身保留但被隔离) |
| `gm_update` | 按精确节点名称更新 / 弃用已有节点(不存在则报错)。`mode=update`(默认)refine description/content;`mode=deprecate` 标记 `[DEPRECATED]` 并切断所有关系,节点不再可被召回(等效删除;维护链在 `purgeAfterDays` 天后物理清理)。`mode=delete` 已移除,由 deprecate 取代 |
| `gm_link` | 手动在两个已存在节点之间建立或细化关系边(按白名单校验类型+方向;from+to+type 已存在时仅更新 instruction) |
| `gm_unlink` | 按名称删除两节点之间的关系边;可选 type 过滤,不传则删除 from→to 之间所有边 |
| `gm_merge` | 合并两个同类型重复节点:keep 吸收 content/validatedCount/sessions + 去重边迁移;merge 节点被软删除(deprecated) |
| `gm_stats` | 查看节点、关系、社区和 PageRank 统计 |
| `gm_maintain` | 执行去重、PageRank 和社区维护 |
| `gm_maintain` | 执行衰减评分、去重、PageRank 和社区维护 |

## 开发

Expand Down
Loading