Skip to content

Commit 438869b

Browse files
author
chrishuan
committed
feat: release v0.3.6
1 parent 1bdcf28 commit 438869b

48 files changed

Lines changed: 2464 additions & 439 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ scripts/migrate-sqlite-to-tcvdb/dist/
2222
scripts/read-local-memory/dist/
2323

2424
# Root-level build output (not used at runtime, not published)
25-
/dist/
25+
dist/
2626

2727
node_modules/
2828
benchmark-runs/
@@ -41,3 +41,6 @@ test-offload-sessions.sh
4141
# npm pack / release tarballs (never commit packaged outputs)
4242
*.tgz
4343
*.tar.gz
44+
45+
# log files
46+
*.log

CHANGELOG.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,69 @@
44

55
---
66

7-
## [Unreleased]
7+
## [0.3.6] - 2026-05-27
8+
9+
### ✨ 新功能
10+
11+
- **Recall 上下文预算控制** ([#71](https://github.com/Tencent/TencentDB-Agent-Memory/pull/71) / [#70](https://github.com/Tencent/TencentDB-Agent-Memory/issues/70)):新增 `recall.maxCharsPerMemory``recall.maxTotalRecallChars` 两项配置,默认 `0` 不改变现有行为;设置为正整数后,会在 L1 召回完成、注入 `<relevant-memories>` 之前按分数顺序裁剪超长条目并丢弃溢出部分,避免长会话因记忆膨胀挤占上下文。已在 README、README_CN 与 `openclaw.plugin.json` 同步说明。
12+
- **L1 / L2 / L3 提示词按用户输入语言自适应** ([#38](https://github.com/Tencent/TencentDB-Agent-Memory/issues/38)):`l1-extraction``l1-dedup``scene-extraction``persona-generation` 四个 prompt 中所有自由文本字段(`scene_name`、记忆 `content`、scene `.md` 标题/正文、`persona.md` 各章节)现在跟随用户消息的主导语言书写;JSON 字段名、枚举值、ISO 时间戳、`persona.md` 等结构化文件名继续保持英文作为稳定契约。无需配置 `locale`,任意语言(en / fr / ja / es / …)均可直接使用。
13+
- **Embedding `sendDimensions` 可选关闭**`OpenAIEmbeddingService` 默认仍会在请求体携带 `dimensions` 字段(兼容 OpenAI `text-embedding-3-*` Matryoshka 截断);新增 `embedding.sendDimensions` 配置项,设置为 `false` 时省略该字段,可对接 BGE-M3 等不支持自定义维度的固定维度模型(原会被服务端 HTTP 400 拒绝 `does not support matryoshka representation`)。
14+
- **Gateway 可选 Bearer 鉴权 + CORS 白名单**:新增 `server.apiKey` / `TDAI_GATEWAY_API_KEY` 配置项,设置后所有非 `/health` 路由需携带 `Authorization: Bearer <key>``crypto.timingSafeEqual` 防时序攻击);新增 `server.corsOrigins` / `TDAI_CORS_ORIGINS` 配置项,显式指定允许的 CORS 来源列表(空列表 = 不发送 CORS 头,`"*"` = 保留旧版宽松行为)。启动时打印安全态势摘要,非回环地址 + 无 apiKey 时输出 WARN。两项均默认关闭,现有部署无需改动。Hermes Python 客户端同步支持 `MEMORY_TENCENTDB_GATEWAY_API_KEY` 环境变量自动附加 Bearer 头。
15+
- **Offload `collect` 模式**:新增 `offload.mode: "collect"` 配置,仅执行数据采集(L0 捕获 + 向量写入)而不触发 L3 压缩,适用于纯数据积累阶段或调试场景。
816

917
### 🐛 修复
1018

11-
- **Hermes Docker 镜像 `config.yaml` 缺少 `api_key` 字段**`Dockerfile.hermes` 的 CMD 脚本将 API Key 写入了 `.env``OPENAI_API_KEY`,但未写入 `config.yaml``model.api_key`,导致 `provider: custom` 时 Hermes 无法找到认证凭据(报 401 Authentication Fails)。现修复为在 `config.yaml``model` 段同步写入 `api_key: "${MODEL_API_KEY}"`
19+
#### 数据安全 / 数据隔离
20+
- **L2 LLM 提取失败导致 `scene_blocks/` 被清空 / 半写入** ([#88](https://github.com/Tencent/TencentDB-Agent-Memory/issues/88)):Phase 1 已对 `scene_blocks/` 做完整快照,但 LLM 抛错时 `catch` 直接 `return`,沙箱里的部分写入 / 删除不会回滚,后续 recall 因此看不到场景导航,降级为碎片召回。新增 `BackupManager.findLatestBackup` + `restoreLatestDirectory`,在 LLM 失败时自动从最新备份恢复;采用 fail-soft 设计:无备份时不动目标目录,恢复过程自身的错误也不会替换原始 LLM 错误。
21+
- **Cleaner 安全加固**`computeCutoffMsByLocalDay` 拒绝无效 cutoff(未来时间 / 距今不足 24h);SQLite 与 TCVDB 在 `expired/total > 80%` 时阻止删除;`runOnce` 增加最小保留护栏(L0:50 / L1:20)并产出 `cleaner_summary` JSON 审计日志;新增 `__tests__/cleaner/verify-cleaner-safety.ts` E2E 校验。
22+
- **场景文件名含空格导致 Persona Scene Navigation 引用失效**:LLM 在 L2 偶发用 `Daily Rhythm in Shanghai.md` 这类含空格的名字创建 scene block,导致 `persona.md``### Path: scene_blocks/<name>.md` 引用无法被下游 `\S+\.md` 风格解析器(health-checker 等)识别,soak 后健康检查必报 `Scene Navigation 存在但无场景引用`。修复:(1) `scene-extraction` prompt 增加"📛 文件命名规范(强制)"段,禁止空格 / 括号 / 引号等标点;(2) 新增 `core/scene/filename-normalizer.ts`,在 `SceneExtractor.extract` Phase 5b(cleanup 后、`syncSceneIndex` 前)自动归一化文件名(空格 → `-`、剥离危险标点、冲突时追加 `-2` 后缀),下游 PersonaGenerator / recall / profile-sync 自动使用干净名,无需改动。
23+
24+
#### OpenClaw 宿主兼容
25+
- **`api.runtime.state` 在新版 OpenClaw 上为 `undefined` 导致注册时崩溃** ([#78](https://github.com/Tencent/TencentDB-Agent-Memory/issues/78) / [#85](https://github.com/Tencent/TencentDB-Agent-Memory/pull/85) / [#79](https://github.com/Tencent/TencentDB-Agent-Memory/pull/79)):为两处调用点加可选链 + fallback,优先调用宿主 `runtimeState.resolveStateDir()`,缺失时退到 `OPENCLAW_STATE_DIR` 环境变量,再退到 `~/.openclaw`。同时修复 cli-metadata 注册模式下 `runtime` 为空对象 `{}` 仍会触发 `TypeError` 的问题(在该模式下提前 return,只调用 `registerCli`)。
26+
- **`contextEngine` slot ID 与插件名不一致**`registerContextEngine` 的 ID 从 `openclaw-context-offload` 改为 `memory-tencentdb`,避免 `openclaw doctor --fix` 把 slot 重置;`setup-offload.sh` 中的 `CONTEXT_ENGINE_ID` 同步更新。
27+
- **L1.5 settle 永不返回导致 L2 卡死**:在 L2 poll 中为 L1.5 settle 增加 60s 超时,当未配置 Context Engine slot 导致 `assemble` 永不被调用时,自动 force-settle 解锁 L2。
28+
- **Standalone 文本任务仍暴露工具导致 DeepSeek 等后端 L1 抽取不稳定** ([#58](https://github.com/Tencent/TencentDB-Agent-Memory/issues/58) / [#59](https://github.com/Tencent/TencentDB-Agent-Memory/pull/59)):`enableTools=false` 时彻底不传工具列表(此前即便只读子集也会鼓励 OpenAI 兼容后端尝试 tool calling,DeepSeek 上尤其明显)。
29+
- **L2 cold-start skip 被错误地更新 `l2LastRunTime`**:导致首次 skip 后必须等满 `l2MaxInterval` 才会真正运行 L2;现在仅在确实跑过的情况下更新时间戳。
30+
31+
#### Offload(Context Engine)稳定性
32+
- **`sanitizeText` 误删 emoji / CJK Extension B / Math Bold 等非 BMP 字符** ([#30](https://github.com/Tencent/TencentDB-Agent-Memory/issues/30) / [#31](https://github.com/Tencent/TencentDB-Agent-Memory/pull/31)):`UNSAFE_CHAR_RE``[\uD800-\uDFFF]` 但缺 `u` flag,JS 按 UTF-16 code unit 处理时会把每个非 BMP 码点的两个 surrogate 各自 strip 掉。加上 `u` flag 后只匹配孤立(畸形)surrogate,emoji / 扩展 CJK / 数学加粗等正常恢复;新增 vitest 套件覆盖保留与剥离两类 case。
33+
- **Emergency 截断在 `MIN_KEEP` 拒绝下死锁**`EMERGENCY_MIN_MESSAGES_TO_KEEP` 由 4 降到 2;新增 `_emergencyTruncateOversized`,当 head/tail 删除均被阻塞时就地截断超大消息(保留 `tool_use` 块结构),最后兜底强制删除并配对清理 `toolResult`,在 LLM 可见的内容里加截断告示。
34+
- **多轮 aggressive compression 累计耗时**:由 6 轮 `O(N × rounds)` 全量 tiktoken 改为单趟 `O(N × 1)` 直接计算到目标阈值的精确切点。615 条消息从 84s 降到 ~14s;tool 配对、user 消息保护、MMD 保留、stall 检测等安全机制全部保留。
35+
- **FP-HEAD-DELETE 在多轮 FAST-SKIP 后误删新消息**:移除该路径,改用 `FP-BOUNDARY-DELETE`(基于上一轮 aggressive 边界的 O(1) 头部删除,index + fingerprint 双重验证、tool-pair 安全)+ `BOUNDARY-INCR-SKIP`(增量估算低于阈值时跳过 tiktoken)。重放场景下 `assemble` 38s → 122ms(310×)。
36+
- **首次 assemble 慢**:为 `TAIL-ACCUMULATE` 增加 fast-token-estimate(基于字符,~51× 快于 tiktoken)前置短路,无边界且 fast estimate 明显高于阈值时跳过全量 tiktoken;同时为 `TAIL-ACCUMULATE` 增加向后 tool-pair 校正、user 消息保护、最少保留 10 条等安全检查。首次 assemble 29s → ~1.4s。
37+
- **Token 计算精度**`details` 字段加入 `INTERNAL_KEYS`(框架在送 LLM 前 strip 掉,不应计入 token);新增 `_stripLargeFields()` 移除非内容大字段;就地截断后调用 `invalidateTokenCache(msg)` 修正 WeakMap 缓存陈旧问题;`bestTokens < 600` 时跳过截断防反向膨胀;stub 文本简化为纯英文避免触发模型内容过滤;`l3TiktokenEncoding` 默认从 `o200k_base` 改为 `cl100k_base`(匹配 DeepSeek / GLM / MiniMax 分词器)。
38+
- **Offload 日志降级**`AGGRESSIVE` / `EMERGENCY` 等多数日志降到 debug,仅当超过 10s 时才输出 `SLOW` warn;Opik tracer 初始化、`after_tool_call` 无 session 等"正常 fallback"场景日志同步降级,减少 `plugins list` 噪音。
39+
40+
#### Hermes / Docker 部署
41+
- **`Dockerfile.hermes` 生成的 `config.yaml``api_key`** ([#77](https://github.com/Tencent/TencentDB-Agent-Memory/issues/77) / [#81](https://github.com/Tencent/TencentDB-Agent-Memory/pull/81)):`provider: custom` 模式下 Hermes 从 `config.yaml.model.api_key` 读密钥,而原 CMD 脚本仅写入 `.env``OPENAI_API_KEY`,造成容器内首条对话即报 401。修复为同步写入 `model.api_key: "${MODEL_API_KEY}"`
42+
- **安装脚本多处问题** ([#18](https://github.com/Tencent/TencentDB-Agent-Memory/issues/18) / [#19](https://github.com/Tencent/TencentDB-Agent-Memory/issues/19) / [#20](https://github.com/Tencent/TencentDB-Agent-Memory/issues/20) / [#54](https://github.com/Tencent/TencentDB-Agent-Memory/pull/54) / [#55](https://github.com/Tencent/TencentDB-Agent-Memory/pull/55)):
43+
- 支持 `HERMES_AGENT_DIR` 环境变量覆盖,适配 FHS 布局(把 hermes-agent 装在 `/usr/local/lib/hermes-agent` 等)。
44+
- root 用户执行时不再 `su - root` 无限递归。
45+
- `MEMORY_TENCENTDB_GATEWAY_CMD` 在 systemd 环境下用 `command -v node` 解析的绝对路径,`npx tsx` 改为 `node --import tsx/esm`,避免 nvm PATH 不可见时找不到 node。
46+
47+
### ✨ 改进
48+
49+
- **L1.5 settle 60s 超时保护**(详见上)。
50+
- **OpenAI-style standalone runner**`enableTools=false` 时不再传任何工具(详见上)。
51+
- **`l3TiktokenEncoding` 默认改为 `cl100k_base`**,匹配主流国产/开源模型分词器。
52+
- **Docker 文档**:补充 `cd docker/opensource` 前置步骤;新增 question/consultation issue 模板。
53+
54+
### 🧪 测试 / 内部
55+
56+
- 新增 `src/utils/backup.test.ts``findLatestBackup` 4 个用例 + `restoreLatestDirectory` 5 个用例,真 fs 沙箱。
57+
- 新增 `src/core/scene/scene-extractor.restore.integration.test.ts`:用真临时目录 + 真 BackupManager 验证 LLM 失败时 `scene_blocks/` 被恢复(含 step 日志,沙箱保留供人工 inspect)。
58+
- 新增 `src/utils/openclaw-state-dir.test.ts` + `index.test.ts` cli-metadata 模式安全性用例。
59+
- 新增 `__tests__/cleaner/verify-cleaner-safety.ts` E2E(SQLite + live VDB)。
60+
- 新增 `src/offload/fast-token-estimate.ts` + `benchmark-token-estimate.ts` 基线脚本。
61+
62+
### ⚠️ 配置项变化(向后兼容)
63+
64+
| Key | 默认值 | 说明 |
65+
|---|---|---|
66+
| `recall.maxCharsPerMemory` | `0` | `0`/未设置 = 不裁剪 |
67+
| `recall.maxTotalRecallChars` | `0` | `0`/未设置 = 不裁剪 |
68+
| `embedding.sendDimensions` | `true` | `false` 时不在请求体携带 `dimensions`,适配 BGE-M3 等 |
69+
| `l3TiktokenEncoding` | `cl100k_base`(原 `o200k_base`| 仅在显式依赖 `o200k_base` 时需手动覆盖回去 |
1270

1371
---
1472

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,45 @@ docker exec -it hermes-memory hermes
247247
---
248248

249249

250+
## 🔒 Gateway Security (optional)
251+
252+
The Hermes Gateway listens on `:8420` and exposes capture / search / recall HTTP endpoints. Two opt-in switches let you turn it from "open localhost sidecar" into "authenticated network service". **Both default to off so existing deployments keep working unchanged.**
253+
254+
| Field | env | Default | Description |
255+
| :--- | :--- | :--- | :--- |
256+
| `server.apiKey` | `TDAI_GATEWAY_API_KEY` | _(unset)_ | When set, every route except `GET /health` requires `Authorization: Bearer <apiKey>`; missing or wrong tokens get HTTP 401. Comparison is constant-time. |
257+
| `server.corsOrigins` | `TDAI_CORS_ORIGINS` (comma-separated) | `[]` | CORS allow-list. Empty list emits **no** `Access-Control-Allow-*` headers — browsers then block all cross-origin requests. Use `["*"]` only for local development. |
258+
259+
When `apiKey` is unset, the gateway prints a startup `WARN`. If it is bound to a non-loopback host (e.g. `0.0.0.0`) without an apiKey, a second louder warning is emitted.
260+
261+
Clients call protected routes with a Bearer token:
262+
263+
```bash
264+
curl -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
265+
-H "Content-Type: application/json" \
266+
-d '{"query":"...","session_key":"..."}' \
267+
http://127.0.0.1:8420/recall
268+
```
269+
270+
`GET /health` stays open without a token so orchestrator probes (`docker healthcheck`, `kubectl liveness`) keep working.
271+
272+
### Hermes plugin side
273+
274+
The Hermes `memory_tencentdb` plugin is a **client** of the Gateway. To make it talk to a Gateway that has auth enabled, set:
275+
276+
```bash
277+
export MEMORY_TENCENTDB_GATEWAY_API_KEY="<same-secret-as-gateway>"
278+
```
279+
280+
The plugin will then attach `Authorization: Bearer <key>` to every request it sends to the Gateway. If the variable is unset, the plugin sends no auth header — which matches the Gateway's legacy default and is fine for a Gateway that has not opted into `TDAI_GATEWAY_API_KEY`.
281+
282+
Important: the plugin only handles the **client half**. Whether the Gateway actually enforces a Bearer check is decided on the Gateway side (`TDAI_GATEWAY_API_KEY` / `server.apiKey`). Configure the same secret on both ends — the plugin does not propagate the secret across, since the Gateway might be started by Docker, systemd, or any other means outside the plugin's control.
283+
284+
If `MEMORY_TENCENTDB_GATEWAY_API_KEY` is unset, the plugin also looks at `TDAI_GATEWAY_API_KEY` as a fallback — handy when both processes share an env file and the operator only wants to set one variable name. The Gateway never reads `MEMORY_TENCENTDB_GATEWAY_API_KEY`; that name is plugin-side only.
285+
286+
---
287+
288+
250289
## 🔧 Configurable Parameters
251290

252291
**Every field has a sensible default — it runs with zero configuration.** When you want to tune, peel back the layers based on how deep you go.
@@ -293,6 +332,20 @@ docker exec -it hermes-memory hermes
293332
For all fields, types, and constraints see [`openclaw.plugin.json`](./openclaw.plugin.json)
294333

295334
- `embedding.*` — remote embedding service (OpenAI-compatible API)
335+
- `embedding.sendDimensions` (default `true`): whether to include the `dimensions` field in the request body. OpenAI `text-embedding-3-*` models rely on it for Matryoshka truncation, but some self-hosted / OSS models (e.g. **BGE-M3**) do not support custom dimensions and will reject the request with HTTP 400 `does not support matryoshka representation`. Set it to `false` for those backends, e.g.:
336+
```json
337+
{
338+
"embedding": {
339+
"enabled": true,
340+
"provider": "openai",
341+
"baseUrl": "http://your-host:your-port/v1",
342+
"apiKey": "<KEY>",
343+
"model": "bge-m3",
344+
"dimensions": 1024,
345+
"sendDimensions": false
346+
}
347+
}
348+
```
296349
- `llm.*` — standalone LLM mode (bypass OpenClaw's built-in model and run L1/L2/L3 with a designated API)
297350
- `offload.backendUrl / backendApiKey` — offload the L1/L1.5/L2/L4 flow to a backend service
298351
- `report.*` — metrics reporting

README_CN.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,44 @@ docker exec -it hermes-memory hermes
249249
> 镜像内置了腾讯云 DeepSeek-V3.2 的默认值,如果你使用该模型,`MODEL_BASE_URL`/`MODEL_NAME`/`MODEL_PROVIDER` 可以省略,只传 `MODEL_API_KEY` 即可。
250250
251251

252+
---
253+
254+
## 🔒 Gateway 安全配置(可选)
255+
256+
Hermes Gateway 监听 `:8420`,对外提供 capture / search / recall 的 HTTP 接口。新增两个开关,可以把它从“开放的本地 sidecar”切换为“需要鉴权的网络服务”。**两个开关默认都关闭,已有部署的行为不变。**
257+
258+
| 字段 | env | 默认值 | 说明 |
259+
| :--- | :--- | :--- | :--- |
260+
| `server.apiKey` | `TDAI_GATEWAY_API_KEY` | _(未设置)_ | 设置后,除 `GET /health` 外的所有接口都要求 `Authorization: Bearer <apiKey>`;缺失或错误的 token 返回 HTTP 401。Token 比较使用常量时间算法,避免时序侧信道。 |
261+
| `server.corsOrigins` | `TDAI_CORS_ORIGINS`(逗号分隔) | `[]` | CORS 白名单。空列表表示**不发送**任何 `Access-Control-Allow-*` 响应头,浏览器同源策略会自动阻止跨域请求。`["*"]` 仅供本地开发,不要用于生产。 |
262+
263+
`apiKey` 未设置时,Gateway 启动时会打印一条 `WARN`;如果同时还绑定在非 loopback 地址(例如 `0.0.0.0`),还会再打印一条更醒目的告警。
264+
265+
客户端在启用鉴权后用 Bearer token 调用:
266+
267+
```bash
268+
curl -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
269+
-H "Content-Type: application/json" \
270+
-d '{"query":"...","session_key":"..."}' \
271+
http://127.0.0.1:8420/recall
272+
```
273+
274+
`GET /health` 永远无需 token,方便 `docker healthcheck` / `kubectl liveness` 等编排探针继续工作。
275+
276+
### Hermes 插件侧的配置
277+
278+
Hermes `memory_tencentdb` 插件本身是 Gateway 的**客户端**。当 Gateway 开启了鉴权后,在 Hermes 进程上设置:
279+
280+
```bash
281+
export MEMORY_TENCENTDB_GATEWAY_API_KEY="<与 Gateway 同一份密钥>"
282+
```
283+
284+
插件随后会在每一次发往 Gateway 的请求上附带 `Authorization: Bearer <key>`。该变量未设置时,插件不发送任何鉴权头,与 Gateway 维持开放模式的旧行为完全匹配——已有部署 0 影响。
285+
286+
需要明确的边界:**插件只负责 client 一侧**。Gateway 是否真的强制鉴权由 Gateway 端自己的 `TDAI_GATEWAY_API_KEY` / `server.apiKey` 决定。两端要使用相同的密钥才能匹配;插件不会把这个值传递给 Gateway——因为 Gateway 可能由 Docker、systemd 或其它独立机制拉起,插件没有也不应该去管这个。
287+
288+
`MEMORY_TENCENTDB_GATEWAY_API_KEY` 没设置,插件还会回退读取 `TDAI_GATEWAY_API_KEY`,方便两个进程共享同一个 env 文件、只设一个变量名的场景。Gateway 永远不会读 `MEMORY_TENCENTDB_GATEWAY_API_KEY`,那是插件侧专用名字。
289+
252290
---
253291

254292
## 🔧 可调参数
@@ -297,6 +335,20 @@ docker exec -it hermes-memory hermes
297335
完整字段、类型、约束见 [`openclaw.plugin.json`](./openclaw.plugin.json)
298336

299337
- `embedding.*` — 远程 embedding 服务(OpenAI 兼容 API)
338+
- `embedding.sendDimensions`(默认 `true`):是否在请求体中携带 `dimensions` 字段。OpenAI `text-embedding-3-*` 系列依赖该字段做 Matryoshka 维度截断;但部分自托管 / 开源模型(如 **BGE-M3**)不支持自定义维度,会以 HTTP 400 报 `does not support matryoshka representation` 拒绝请求。此时请显式设为 `false`,例如:
339+
```json
340+
{
341+
"embedding": {
342+
"enabled": true,
343+
"provider": "openai",
344+
"baseUrl": "http://your-host:your-port/v1",
345+
"apiKey": "<KEY>",
346+
"model": "bge-m3",
347+
"dimensions": 1024,
348+
"sendDimensions": false
349+
}
350+
}
351+
```
300352
- `llm.*` — 独立 LLM 模式(绕过 OpenClaw 内置模型,用指定 API 跑 L1/L2/L3)
301353
- `offload.backendUrl / backendApiKey` — 将 L1/L1.5/L2/L4 offload 流程卸载到后端服务
302354
- `report.*` — 指标上报

0 commit comments

Comments
 (0)