# Graph Memory

<p align="center">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/brand/graph-memory-hosts-banner.png" alt="Graph Memory 原生适配 DeepSeek Harness，并兼容 OpenClaw" width="100%">
</p>

<p align="center">
  <strong>限制上下文，让记忆继续生长。</strong><br>
  Graph Memory 原生接管 DeepSeek Harness 的模型可见历史：保留最近对话，把旧历史变成可检索图记忆，并在需要时召回精确来源。
</p>

<p align="center">
  <a href="README.md">English</a> ·
  <a href="https://www.dsh.so/zh/artifact/graph-memory">dsh.so</a> ·
  <a href="benchmarks/dsh-context-takeover/README.md">20 轮实测</a> ·
  <a href="docs/TURN_MEMORY_NAVIGATION_UPGRADE_CN.md">升级与移植指南</a>
</p>

<p align="center">
  <a href="https://www.dsh.so/zh/artifact/graph-memory"><img src="https://www.dsh.so/badge/graph-memory.svg" alt="dsh.so 安全徽章"></a>
  <a href="https://www.dsh.so/zh/artifact/graph-memory"><img src="https://www.dsh.so/badge/install/graph-memory.svg" alt="dsh.so 安装徽章"></a>
</p>

## 它解决什么

<p align="center">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/context-memory-illustration.webp" alt="不断增长的 Agent 历史转化为图谱导航和精简的近期上下文" width="100%">
</p>

Graph Memory 接管的是**发给模型的历史表面**，不会删除 DSH 的事件记录。默认保留最近 5 个已完成用户轮次；已完成的推理和工具轨迹不再重复发送；旧轮次与跨会话记忆按当前问题自动召回。

## 1.6 轮次记忆导航升级

| 以前 | 现在 |
|---|---|
| 直接从消息抽 TASK / SKILL / EVENT | 每个完成轮次先形成一句自包含摘要，再从同一句摘要派生 SPO |
| 图节点容易变成事实正文 | 摘要、SPO、社区只负责导航；原始问题与最终回答才是事实源 |
| 当前会话旧历史可能被整体过滤 | 只排除最近窗口中仍然可见的消息；窗口外同会话与跨会话统一召回 |
| 社区扩展可能带入一整片内容 | 本地 LPA 缩小候选范围，查询时 PPR 排序，只回溯命中的原始问答 |
| DSH 保留完整工具/推理轨迹 | 完成轮次仅保留问题与最终回答，旧前缀由固定标记替换 |

整个写入过程每轮只有 **1 次辅助 LLM 调用**；社区计算和 PPR 都在本地完成。没有节点/边数量硬编码，没有语义方向门禁，也不会用 JSON repair 把错误输出伪装成成功数据。[查看完整设计、源代码映射与另一个项目的移植顺序 →](docs/TURN_MEMORY_NAVIGATION_UPGRADE_CN.md)

## 先看真实结果

<p align="center">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/dsh-context-takeover-chart.svg" alt="DSH 20 轮首请求上下文对比" width="100%">
</p>

| GLM-5.2 真实 20 轮测试 | 历史原生 DSH 基线 | 最新 Graph Memory | 变化 |
|---|---:|---:|---:|
| 第 20 轮首请求 | 56,998 Token | **11,008 Token** | **−80.69%** |
| 第 20 轮模型可见消息 | 171 | **21** | **−87.72%** |
| 20 轮首请求上下文累计 | 532,451 Token | **165,896 Token** | **−68.84%** |
| 全部实测 Token¹ | 2,487,776 | **2,327,728** | **−6.43%** |

<sub>¹ 最新候选包含 166 次主 Agent 请求、20 次轮次抽取和 41 次 Embedding 请求；历史基线为 77 次主请求。两次运行的 DSH 提交和非确定性工具循环不同，因此不是严格同期 A/B；首请求上下文是直接接管指标，完整账单同时公开。</sub>

**20/20** 轮任务通过 · **20/20** 次结构化抽取成功 · **0** 隔离 · **20** 轮次摘要 · **92** 条 SPO · **30** 个社区 · **20** 个摘要向量。T11、T19、T20 均自动召回窗口外记忆，并携带精确原始问题与最终回答。

[查看 Markdown 实测报告、逐轮数据、方法与限制 →](benchmarks/dsh-context-takeover/README.md)

## 上下文可以缩小，记忆不会消失

<p align="center">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/dsh/plugin-inventory-active.png" alt="Graph Memory 已在 DSH 启用" width="48%">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/dsh/vector-cross-session-recall.png" alt="全新 DSH Session 中的跨会话召回" width="48%">
</p>

图谱只是**导航层**，不是拿摘要代替证据。TASK、SKILL、EVENT 节点会指回原始用户问题和最终可见回答，召回时把这些精确来源一起交给模型。

## 安装到 DeepSeek Harness

Node.js 22.13+ · 不 fork DSH · npm registry 已发布 `@multilab/graph-memory`（`npm view` 当前返回 `2.0.4`），直接从 npm 安装：

```bash
npx @deepseek-ai/dsh plugin --profile web add @multilab/graph-memory
npx @deepseek-ai/dsh --profile web --dump-config
npx @deepseek-ai/dsh web
```

如需固定 GitHub 版本，请使用上游真实存在的 tag `v1.6.0-beta.17`（经 `git ls-remote` 验证；上游 tag 止于 `v2.0.0`，`2.0.x` npm 发布线未在其上重新打 tag）：

```bash
npx @deepseek-ai/dsh plugin --profile web add github:adoresever/graph-memory#v1.6.0-beta.17
```

`1.6.0-beta.17` 适配 DSH `0.1.7-rc.2`（会话格式 V4）：Graph Memory 所有落盘的表面替换消息改用 V4 要求的 producer-owned source kind `plugin:graph-memory`（[#113](https://github.com/adoresever/graph-memory/issues/113)）。旧版 DSH 同样接受该形态。

npm registry 旧包名 `graph-memory` 仍是旧版 `1.5.8`，不要用它验证 DSH——上文使用的 `@multilab/graph-memory` 才是维护中的发布线。

在 **Settings → Plugins** 确认 @multilab/graph-memory/dsh 已启用。默认数据库位于 $DSH_HOME/graph-memory/graph-memory.db，通常是 ~/.dsh/graph-memory/graph-memory.db。

> **版本号说明。** `package.json`（`2.0.4`）是 npm 发布线，`openclaw.plugin.json` 与其完全同步——一个包一个版本。本文其余位置出现的 `1.6.0-beta.x` 指上游 GitHub tag 线（`v1.6.0-beta.15` 至 `beta.17` 均为经 `git ls-remote` 验证的真实上游 tag），属继承的发布线文档，并非本包版本号。

## DSH 设置面板

Graph Memory 提供 DSH 设置界面：在 DSH Web 面板 **Settings → Plugins → graph-memory** 打开。面板由 schemastery `Config` schema 驱动，非法值在面板内联报错，而非运行时报错。

### 热生效字段与重启生效字段

大部分字段即时生效——插件监听 DSH 的 `loader/volatile-update` 事件，无需重启即重新读取当前有效配置：

| 即时生效 | 需要重启 |
|---|---|
| `extractionEnabled`、`recallEnabled`、`recallMaxNodes`、`semanticScoreThreshold` | `dbPath`（重开数据库） |
| `maintenanceInterval`、`freshTurnCount`、`assistantTools`、`messageRetention`、`decay` | `embedding.baseURL` / `model` / `dimensions`（重建 embedding 函数与向量指纹） |
| `extract.*`、`llmReasoningEffort`、`contextCompactionEnabled`、`projectCompletedTurnTools` | `llmMaxTokens`（启动快照） |
| `extractionSource`、`llmProvider`、`llmModel`、`extractionModels` | |

### 抽取模型来源

| 来源 | 语义 |
|---|---|
| `session`（默认） | 跟随当前会话 provider/model 路由（零配置兜底） |
| `preset` | 显式 `llmProvider` / `llmModel` 对（兼容旧的 env 配置，不 breaking） |
| `pool` | `extractionModels` 列表顺序尝试；单模型失败级联下一个；空池回退 `preset`，再回退 `session` |

### Embedding API Key（凭据存储优先）

Embedding API key **绝不存入插件配置**。唯一通道是 **`apiKeyEnv`** 凭据引用（默认 `GRAPH_MEMORY_EMBEDDING_API_KEY`，与厂商无关）：把 key 以该引用名存入 DSH 凭据存储（`.credentials.yaml` 的 `refs`）或环境变量，插件连接期经 `ctx.credentials.resolve` 解析。换 Embedding 厂商只改存储的值，引用名不变。

schema 注入的默认引用名单独出现不视为"已配置"（未配置用户零探测噪音）；配置 `baseURL`（或自定义引用名）即启用向量召回——**仅设置 `model` 不会启用**。

配置页 Embedding 区块提供**「测试」按钮**：用当前表单值（含未保存的草稿）发起一次真实 `/embeddings` 调用，报告维度、延迟与凭据来源；key 全程在宿主侧经 `ctx.credentials.resolve` 解析，不回显、不出宿主。

### 多 key 故障转移与面板凭据管理（2.0.14）

**多 key 格式。** 在同一个引用名下存放多把 key，用逗号分隔整串 —— `"k1,k2,k3"` —— 写入凭据存储（`.credentials.yaml` 的 `refs`）或环境变量。宿主按逗号分割、逐段 trim、丢弃空段，因此 `" k1 , , k2 "` 解析为 `[k1, k2]`。单 key（或无 key）无需逗号，行为与旧版完全一致。保存后**所有 key 并行探测**，每把 key 的有效性、维度与延迟在面板报告行中逐条呈现。

**故障转移语义。** 当配置了 ≥2 把 key 时，任何失败都**立即**轮换到下一把 —— ≥2 key 时单 key 内不重试（429/5xx 不退避，直接轮换）。粘性指针记住上一次成功的 key 并从该处开始下一次调用，环形回绕。仅配置 1 把（或无）key 时，沿用原来的重试退避路径不变（单 key 零回归）。

**面板管理。** 设置 → 插件 → graph-memory → 凭据区块提供密码输入框与**保存**、**清除**按钮。保存把整串逗号分隔值经宿主凭据服务写入 `.credentials.yaml`（挂在配置的引用名下）；清除移除该引用。输入值**绝不进入插件配置、绝不回显到客户端、绝不进入日志** —— 仅流向宿主凭据服务。连接测试与保存后逐 key 探测的单次请求超时为面板可调的 `embedding.testTimeoutMs`（1000–300000，默认 30000）——跨境慢端点此前被硬编码超时误报超时的，可在同表单调高。生产 embedding 调用另有面板可调基线 `embedding.requestTimeoutMs`（1000–300000，默认 10000，需重启生效）：单发请求直接使用该值，批量请求用 `max(requestTimeoutMs, 2s×条数)` —— 显式调大的基线只抬高自适应下限，不会缩窄自适应公式。

**生效边界。** key 值变更**无需重启**即生效（凭据按批解析且宿主监听凭据存储），因此轮换到新 key 或增删 key 都是热生效。`baseURL`、`model`、`dimensions` 仍需重启生效 —— 它们是连接期参数，会重建 embedding 函数与向量指纹。

**环境变量遮蔽。** 若引用名由启动环境变量提供，凭据服务报告 `writable: false`；面板随即显示只读提示并禁用保存按钮，因为该 key 归 shell/环境所有而非凭据存储。请在 shell/环境中管理此类 key。

**全失败错误。** 当所有配置 key 全部失败时，embedding 引擎报告 `[graph-memory] Embedding API: all N API keys failed (last: <轮换序末位失败 key 的错误>)`；逐 key 明细（状态、维度、延迟、错误）显示在面板报告行中。启动期全失败探测会降级到 FTS5 全文召回，不会崩溃。

### 迁移

已有 `GRAPH_MEMORY_*` 环境变量用户**零迁移**——env 兜底仍在 `apply()` 内生效，默认 `apiKeyEnv` 引用已指向 `GRAPH_MEMORY_EMBEDDING_API_KEY`。曾在面板 `embedding.apiKey` 字段直填过 key 的用户（≤ 2.0.7，明文落在 profile patch）：请把 key 迁入凭据存储的默认引用名下并重启，并**手动删除 profile patch 里遗留的 `embedding.apiKey` 行**——该行会一直留在文件里直到你删掉它。2.0.13 起插件在 `apply()` 入口按字段白名单重建 embedding 段，旧明文 `apiKey`（及一切未知遗留字段）不会再被读进运行时内存；schema 本身不 strip 未知键。随附的 `cordis.patch.yml` 已精简为仅 `dbPath`。

> **对既有用户的行为变更**：默认 `extractionSource: "session"` 下，抽取改为跟随当前会话 provider/model 路由，已配置的 `llmProvider`/`llmModel` 对仅在无会话路由时作为**兜底**；旧版是"专用路由优先"。若你依赖抽取始终使用专用模型，请在面板将 `extractionSource` 设为 `preset`（`GRAPH_MEMORY_LLM_PROVIDER`/`GRAPH_MEMORY_LLM_MODEL` 的既有语义通过 preset 源保留）。`gm_status` 现已报告当前生效的抽取源。

<details>
<summary><strong>故障排查：损坏库自动隔离</strong></summary>

Graph Memory 在启动时打开 SQLite 库；若文件发生页级损坏（`SQLITE_CORRUPT`，经典消息 `database disk image is malformed`——常见于异常关机或磁盘错误后），插件**不会崩溃**，而是：

1. 把损坏文件连同 `-wal`/`-shm` 侧车改名为 `<dbPath>.corrupt-<时间戳>`，留在库文件同目录（**永不删除**，原件保留给抢救工具）。
2. 在原路径重建全新空库继续运行——插件保持激活（抽取/召回照常），但**记忆从空库重新开始**。

如何得知：隔离发生时宿主日志会记一条 `console.warn`；只要库文件同目录存在隔离残留，`gm_status` 就会报告 `DB quarantine: N isolated corrupt file(s) …`。

数据抢救：隔离文件在你删除前原样保留。可尝试 `sqlite3 <dbPath>.corrupt-<时间戳> ".recover" | sqlite3 recovered.db`，再把 `dbPath` 指向 `recovered.db`（或移回原位）并重启。检测面刻意收窄：其余损坏形状（如 `SQLITE_NOTADB`，"file is not a database"）不做自动隔离、照常报错。

</details>

## 已交付能力

| 能力 | 实现方式 |
|---|---|
| 上下文接管 | 最近 N 个完成轮次可配置；旧模型表面由一个归档标记替换 |
| 轻量抽取 | 只处理用户问题与最终回答；严格结构化工具合同；不摄入推理/工具轨迹 |
| 查询优先召回 | 向量 Top-K，FTS5 降级；图谱命中携带精确来源问答 |
| 持久记忆 | 本地 SQLite、稳定溯源、跨轮次/跨会话/跨项目召回 |
| 失败行为 | 非法抽取进入隔离；前台对话继续；坏数据不修补、不入库 |
| 宿主支持 | DSH/Cordis 原生适配；继续维护 OpenClaw Context Engine 适配 |

<details>
<summary><strong>可选 Embedding</strong></summary>

支持 OpenAI-compatible Embedding。没有配置向量时自动降级到 FTS5，不阻塞对话。

```bash
export GRAPH_MEMORY_EMBEDDING_API_KEY="replace-with-your-key"
export GRAPH_MEMORY_EMBEDDING_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
export GRAPH_MEMORY_EMBEDDING_MODEL="text-embedding-v4"
export GRAPH_MEMORY_EMBEDDING_DIMENSIONS="1024"
dsh web
```

</details>

<details>
<summary><strong>DSH 工具与独立抽取模型</strong></summary>

| 工具 | 用途 |
|---|---|
| gm_status | 存储、抽取、召回、向量与保留状态 |
| gm_search | 显式搜索图记忆 |
| gm_record | 确定性写入 TASK、SKILL 或 EVENT |
| gm_stats | 图谱与保留策略回执 |
| gm_maintain | 执行一次有界维护 |
| gm_retry_extraction | 显式重试隔离的抽取任务 |

自动召回不需要工具调用。抽取可通过 GRAPH_MEMORY_LLM_PROVIDER 和 GRAPH_MEMORY_LLM_MODEL 使用独立模型；可选控制项为 GRAPH_MEMORY_LLM_REASONING_EFFORT 与 GRAPH_MEMORY_LLM_MAX_TOKENS。

</details>

<details>
<summary><strong>OpenClaw 兼容</strong></summary>

```bash
openclaw plugins install graph-memory
openclaw plugins enable graph-memory
openclaw gateway restart
```

在 ~/.openclaw/openclaw.json 激活 Context Engine：

```json
{
  "plugins": {
    "slots": { "contextEngine": "graph-memory" },
    "entries": { "graph-memory": { "enabled": true } }
  }
}
```

<p align="center">
  <img src="https://cdn.jsdelivr.net/gh/adoresever/graph-memory@main/docs/images/token-comparison.png" alt="早期 OpenClaw 七轮 Token 对照" width="76%">
</p>

</details>

<details>
<summary><strong>Graph Memory Pro</strong></summary>

仓库包含实验性的 DSH Pro Lite Host + Client，只读读取 Community SQLite。2D/3D 图工作台、对话分屏与受控拖入上下文仍在规划中。详见 [dsh-pro/README_CN.md](dsh-pro/README_CN.md)。

</details>

## 验证与边界

当前 release `2.0.4` 已通过 **491/498 自动化测试（7 skipped）**、两套 TypeScript 构建、npm 包验证，并用本机安装的 DSH `0.1.7-rc.2` 运行时完成真实兼容性验证（对全部落盘表面写入执行会话格式 V4 准入检查）。

- DSH `0.1.7` 兼容性：落盘表面写入统一使用 producer-owned source kind `plugin:graph-memory`；V4 持久化准入会拒绝旧式 V3 包装 `{ kind: "plugin", plugin: "graph-memory" }`，导致滚动压缩或工具轨迹归档触发时整轮回合失败（[#113](https://github.com/adoresever/graph-memory/issues/113)）。

- DSH `0.2` 线宿主支持（`0.2.0-rc.2` 与 `0.2.1-rc.2`）：`package.json` 声明 `engines.dsh` `>=0.1.7-rc.2 <0.3.0`（按宿主 app-boot 规则，声明后即以此为准、peer 不参与 DSH 门禁），并将 `@deepseek-ai/dsh-typert-protocol` peer 收敛到同一 `<0.3.0` 上限（`>=0.1.0-rc.5 <0.3.0`）。`0.2` 线的宿主核心依赖与本插件实测基线一致（cordis `~4.0.4`/`~4.0.5`、schemastery `~3.18.4`/`~3.18.5`），会话格式仍为 V4（0.2.1 的 "V4.1" 指 DeepSeek-V4.1-Flash-1017 模型而非消息格式）。已就 `0.2.1-rc.2` 做面核查：`dsh-client-ui-primitives` 仍导出客户端使用的 `SettingsForm`/`SegmentedControl`/`Switch`；`dsh-typert-protocol@0.2.1-rc.2` 仍导出 `Remote`/`TypertRemoteService`；0.2.1 引入的 breaking change（`./invariant` 导出移除、composer `stats` 拆分为 `activity`/`usage`、`webRuntime→webStartup` 更名、Agent Team 收件箱改道、工具 `both` 展示模式移除、`azure-openai-responses→azure` 更名）均不触及本插件使用面。`0.2.0-rc.2` 早于以上所有 breaking change。

- 结构化抽取仍依赖模型遵守合同：最新实测 20/20 成功；未来若失败，数据保持隔离且不会阻塞前台对话。
- 召回数量由 Top-K 限制。聚焦问题实测成功；一次包含多个主题的宽查询可能需要提高 Top-K 或拆开提问。
- 当前发布的是工程工作流实测，不是 LoCoMo/LongMemEval 的通用分数。
- 本次“轮次摘要 + SPO 导航 + 精确问答事实源”升级的设计、代码落点和移植步骤见 [升级与移植指南](docs/TURN_MEMORY_NAVIGATION_UPGRADE_CN.md)。

从 [benchmarks/dsh-context-takeover/](benchmarks/dsh-context-takeover/) 复跑。原始对话、供应商响应、本地路径和密钥均未进入仓库。

## 开发

```bash
npm install
npm test
npm run build
npm run verify:package
```

[MIT](LICENSE) © 2026 adoresever · [素材与商标说明](docs/ATTRIBUTIONS.md)
