Skip to content

Commit c84059b

Browse files
yiliang114qwencoder
andcommitted
fix(memory): Preserve Mem0 operator and legacy preset semantics
Co-authored-by: Qwen-Coder <[email protected]>
1 parent ed45308 commit c84059b

15 files changed

Lines changed: 286 additions & 24 deletions

‎docs/design/bundled-mem0.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[English](bundled-mem0.md) | [简体中文](bundled-mem0.zh-CN.md)
44

5-
Status: local implementation; live-provider acceptance remains a separate gate.
5+
Status: implemented; PolarDB V2 was verified through a private tunnel, not public-direct access. Other provider contracts remain bounded by their recorded evidence.
66

77
## Problem and scope
88

@@ -43,7 +43,7 @@ Add to user settings (`~/.qwen/settings.json`), then restart Qwen Code. As with
4343

4444
The default is the PolarDB-style v1-write/v2-search contract, not a guarantee for every “Mem0 v2” service or Hologres. The OSS contract is pinned to the existing integration; fake-server acceptance does not prove compatibility with every upstream OSS release.
4545

46-
Legacy IDs `mem0-platform-v3` and `mem0-oss-rest-2026-08` remain aliases. `aliyun-polardb-mysql-2026-08` remains accepted with its historical `top_k` field, not silently remapped to `limit`. Live verification of limit handling and slash behavior is still required. The adapter always caps final search results at five.
46+
Legacy IDs `mem0-platform-v3` and `mem0-oss-rest-2026-08` remain aliases. `aliyun-polardb-mysql-2026-08` remains accepted with its historical `top_k` field and raw search content, not silently remapped to `limit` or V2 normalization. Private-tunnel acceptance verified `mem0-v2` limit handling and direct-import text; this does not certify public routing or every service. The adapter always caps final search results at five.
4747

4848
## Binding and lifecycle
4949

‎docs/design/bundled-mem0.zh-CN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[English](bundled-mem0.md) | [简体中文](bundled-mem0.zh-CN.md)
44

5-
状态:本地实现;真实服务验收仍是独立门槛。
5+
状态:已实现;PolarDB V2 已经通过私网隧道验证,并未验证公网直连。其他 provider 契约仍以各自记录的证据为界。
66

77
## 问题与范围
88

@@ -43,7 +43,7 @@
4343

4444
默认是 PolarDB 风格的 v1 写入/v2 搜索合同,不保证兼容所有标称“Mem0 v2”的服务或 Hologres。OSS 合同固定为现有集成定义;假服务验收不等于兼容所有上游 OSS 版本。
4545

46-
保留旧 ID `mem0-platform-v3` 和 `mem0-oss-rest-2026-08` 作为别名。`aliyun-polardb-mysql-2026-08` 也继续接受并保留历史 `top_k` 字段,不会默默映射成 `limit`。仍需真实验证 limit 处理和斜杠行为。适配器始终把最终搜索结果限制为五条。
46+
保留旧 ID `mem0-platform-v3` 和 `mem0-oss-rest-2026-08` 作为别名。`aliyun-polardb-mysql-2026-08` 也继续接受并保留历史 `top_k` 字段及原样搜索内容,不会默默映射成 `limit` 或 V2 规范化行为。私网隧道验收验证了 `mem0-v2` 的 limit 处理和直接导入文本;这不证明公网路由或所有服务都可用。适配器始终把最终搜索结果限制为五条。
4747

4848
## 绑定与生命周期
4949

‎docs/design/direct-external-context-mem0-presets.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Direct External Context Mem0 Presets
22

3+
[English](direct-external-context-mem0-presets.md) | [简体中文](direct-external-context-mem0-presets.zh-CN.md)
4+
35
**Status:** Implemented in the private direct integration
46

57
**Date:** 2026-08-27
@@ -38,11 +40,14 @@ A preset therefore identifies one complete, verified contract. Published
3840
preset identifiers are immutable. An incompatible upstream change receives a
3941
new identifier rather than silently changing an existing mapping.
4042

41-
The first built-in presets are:
43+
The built-in presets are:
4244

4345
- `mem0-platform-v3`
4446
- `mem0-oss-rest-2026-08`
4547
- `aliyun-polardb-mysql-2026-08`
48+
- `mem0-v2`
49+
- `mem0-v3`
50+
- `mem0-oss-2026-08`
4651

4752
## Configuration
4853

@@ -100,6 +105,7 @@ Built-in presets select only reviewed constants:
100105
`filters`, or omitted
101106
- a closed set of fixed search options such as `threshold` and `rerank`
102107
- a `results` response collection with reviewed identifier and content fields
108+
- optional single-user direct-import message decoding, only for `mem0-v2` results marked `infer: false`
103109
- an optional static direct-import path and one reviewed response mapping
104110

105111
The engine always sends at most five as the provider limit and retains at most
@@ -149,6 +155,8 @@ authentication override.
149155
- Direct import: `POST /v1/memories`, `infer: false`
150156
- Write response: a valid `results[].id` is `stored`; otherwise `unknown`
151157

158+
The historical `aliyun-polardb-mysql-2026-08` identifier keeps `top_k` and raw search content. The newer `mem0-v2` mapping uses `limit` and unwraps one nonempty user message from an `infer: false` direct-import result. It leaves ordinary text, other message arrays, and results without that marker unchanged. `mem0-v3` and `mem0-oss-2026-08` alias the corresponding legacy V3 and OSS contracts without changing them.
159+
152160
An `event_id` alone is never treated as proof of storage. If a later PolarDB
153161
contract documents asynchronous event polling, that behavior requires a new
154162
preset and write design rather than changing this preset in place.
@@ -193,6 +201,7 @@ The implementation must prove:
193201
- safe `origin` plus `basePath` joining
194202
- exact authentication, path, scope, and limit mapping for every preset
195203
- per-item response normalization and the five-result cap
204+
- preservation of historical preset responses alongside the bounded `mem0-v2` direct-import decoding
196205
- conservative synchronous versus asynchronous write outcomes
197206
- no retry on search or write failures
198207
- loadability of the shipped PolarDB and OSS examples
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Direct External Context Mem0 预设
2+
3+
[English](direct-external-context-mem0-presets.md) | [简体中文](direct-external-context-mem0-presets.zh-CN.md)
4+
5+
**状态:** 已在私有直连集成中实现
6+
7+
**日期:** 2026-08-27
8+
9+
**相关设计:**
10+
[Direct External Context Provider](./direct-external-context-provider.md)、
11+
[Direct External Context Mem0 Write](./direct-external-context-mem0-write.md)、
12+
[External Context Provider Extensions](./external-context-provider-extensions.md)
13+
14+
## 决策
15+
16+
私有 Direct External Context 集成使用单一的 `mem0` provider 类型,通过管理员选择的内置、带版本的预设连接兼容 Mem0 的 REST 服务。预设定义上游协议;实例配置只负责部署地址、凭据引用、固定作用域,以及继承自直连配置的超时时间。
17+
18+
这取代了把产品名与单一协议写死在一起的 provider 类型,例如 `polardb-mem0`。它不开放任意 API 版本、请求模板、自定义请求头映射、JSONPath 表达式或动态 provider 模块。模型可见的 MCP 契约仍是 `context_search({ query })`;仅当版本 1 配置显式启用写入且预设定义了已验证的直接导入操作时,才暴露 `context_remember({ content })`。
19+
20+
为向后兼容,继续接受现有的 `mem0-platform-v3` 配置。新部署使用 `type: "mem0"`。
21+
22+
## 为什么使用预设而不是 `apiVersion`
23+
24+
同一个上游产品的不同操作可能采用不同版本的路径。例如 PolarDB Mem0 的搜索和直接导入使用不同版本的路径。认证方式、作用域字段位置、结果字段、写入响应语义和末尾斜杠要求也可能各自变化,并不由一个数字版本决定。
25+
26+
因此,每个预设代表一套完整、已验证的契约。已发布的预设标识不可改变;上游若有不兼容变更,应增加新标识,而不是悄悄修改原有映射。
27+
28+
内置预设包括:
29+
30+
- `mem0-platform-v3`
31+
- `mem0-oss-rest-2026-08`
32+
- `aliyun-polardb-mysql-2026-08`
33+
- `mem0-v2`
34+
- `mem0-v3`
35+
- `mem0-oss-2026-08`
36+
37+
## 配置
38+
39+
```json
40+
{
41+
"version": 1,
42+
"timeoutMs": 5000,
43+
"provider": {
44+
"type": "mem0",
45+
"preset": "aliyun-polardb-mysql-2026-08",
46+
"endpoint": {
47+
"origin": "https://memory.example.com",
48+
"basePath": ""
49+
},
50+
"credentialEnv": "MEM0_API_KEY",
51+
"scope": {
52+
"userId": "repository-memory",
53+
"agentId": "qwen-code"
54+
}
55+
}
56+
}
57+
```
58+
59+
`origin` 只包含协议和主机部分;`basePath` 是可选的静态反向代理前缀。两者分别验证,避免拼接预设路径时丢失前缀或重新解释服务地址。地址配置不允许内嵌凭据、查询参数、片段、点路径、编码路径材料、空白和控制字符。
60+
61+
默认要求 HTTPS。本机回环 HTTP 可用于本地中继;非回环 HTTP 必须显式设置 `allowInsecureHttp`,此时凭据与记忆内容会以明文传输,仅适用于明确把可信私有网络纳入安全边界的部署。
62+
63+
预设声明使用哪些作用域字段,以及字段是必需还是可选。缺少必需字段,或配置了预设不使用的字段时,启动会拒绝该配置。作用域由管理员固定,绝不出现在模型工具参数中。
64+
65+
每个 MCP 子进程只加载一次绝对路径 `QWEN_EXTERNAL_CONTEXT_CONFIG`。同一 Qwen 会话内,包括 MCP 子进程重启后,该路径、文件内容、地址、预设、作用域及凭据与语料库的绑定都必须保持不变。修改任一项都需要新会话和新的配置路径。
66+
67+
## 有界的预设契约
68+
69+
内置预设只能选择经过审查的常量:
70+
71+
- `Authorization: Token`、`Authorization: Bearer` 或 `X-API-Key`
72+
- 一个静态 POST 搜索路径
73+
- 作为结果数量字段的 `top_k` 或 `limit`
74+
- `user_id`、`agent_id`、`app_id` 放在 JSON 根级、`filters` 下,或省略
75+
- 封闭集合内的固定搜索选项,例如 `threshold` 和 `rerank`
76+
- 带有已审查 ID 和内容字段的 `results` 响应集合
77+
- 仅针对标记 `infer: false` 的 `mem0-v2` 结果,可选的单条用户消息直接导入解码
78+
- 可选的静态直接导入路径及一种已审查的响应映射
79+
80+
引擎向上游请求的数量上限始终为 5,并最多保留 5 个有效结果。它不会重试、跟随重定向、探测其他路径或在预设之间回退。格式错误的单条结果独立丢弃;响应外层格式错误则使请求失败。
81+
82+
新增预设需要权威的协议证据及请求、响应契约测试。不能纳入这个语法范围的服务,应按 External Context Provider Extensions 设计实现自己的本地或远程 MCP Extension,而不是把这份配置扩展成编程语言。
83+
84+
## 初始映射
85+
86+
### Mem0 Platform V3
87+
88+
- 搜索:`POST /v3/memories/search/`
89+
- 认证:`Authorization: Token`
90+
- 作用域:必需的 `appId` 放在 `filters.app_id`
91+
- 数量字段:`top_k`
92+
- 直接导入:`POST /v3/memories/add/`,`infer: false`
93+
- 写入响应:`PENDING` 加 UUID 格式的 `event_id` 表示 `accepted`;只有 `SUCCEEDED` 表示 `stored`
94+
95+
旧的 `mem0-platform-v3` 配置仍作为这个映射的固定地址简写。
96+
97+
### Mem0 OSS REST 2026-08
98+
99+
- 搜索:`POST /search`
100+
- 认证:`X-API-Key`
101+
- 作用域:必需的 `userId` 和可选的 `agentId` 放在 `filters` 下
102+
- 数量字段:`top_k`
103+
- 直接导入:`POST /memories`,`infer: false`
104+
- 写入响应:有效的 `results[].id` 表示 `stored`,否则表示 `unknown`
105+
106+
该映射对应标准 `mem0ai/mem0` REST 服务。使用 Bearer 认证的部署需要单独验证的预设,而不是按实例覆盖认证方式。
107+
108+
### Aliyun PolarDB MySQL 2026-08
109+
110+
- 搜索:`POST /v2/memories/search`
111+
- 认证:`Authorization: Token`
112+
- 作用域:必需的 `userId` 放在 `filters` 下,可选的 `agentId` 放在顶层
113+
- 数量字段:`top_k`
114+
- 直接导入:`POST /v1/memories`,`infer: false`
115+
- 写入响应:有效的 `results[].id` 表示 `stored`,否则表示 `unknown`
116+
117+
历史标识 `aliyun-polardb-mysql-2026-08` 保持 `top_k` 与原样返回搜索内容。新的 `mem0-v2` 映射使用 `limit`,并仅对标记 `infer: false` 的直接导入结果提取一条非空用户消息。普通文本、其他消息数组和未带该标记的结果保持原样。`mem0-v3` 与 `mem0-oss-2026-08` 分别是旧 V3 和 OSS 契约的别名,不改变原有行为。
118+
119+
仅有 `event_id` 不能证明写入已持久化。如果未来 PolarDB 契约规定了异步事件轮询,应新增预设和写入设计,而不是原地更改这个预设。
120+
121+
## 协议依据
122+
123+
- Mem0 Platform V3 基于官方[搜索 API](https://docs.mem0.ai/api-reference/memory/search-memories)及 [V2 到 V3 迁移契约](https://docs.mem0.ai/migration/platform-v2-to-v3)。
124+
- Mem0 OSS REST 固定在上游提交 [`39bc023`](https://github.com/mem0ai/mem0/tree/39bc02330563764e7d4465f1ecff5f002d94da1a),具体参考 [`server/main.py`](https://github.com/mem0ai/mem0/blob/39bc02330563764e7d4465f1ecff5f002d94da1a/server/main.py) 与 [`server/auth.py`](https://github.com/mem0ai/mem0/blob/39bc02330563764e7d4465f1ecff5f002d94da1a/server/auth.py)。
125+
- PolarDB 预设基于官方 [PolarDB for MySQL Mem0 契约](https://help.aliyun.com/en/polardb/polardb-for-mysql/use-polardb-mem0)及[该 PR 记录的真实端到端证据](https://github.com/QwenLM/qwen-code/pull/9952#issuecomment-5407141853)。公开文档涵盖地址、认证和作用域字段位置;真实实例还验证了直接导入时会遵守 `infer: false`。
126+
127+
测试必须持续固定这些确切的请求与响应形状。上游协议若发生不兼容变化,应添加新预设标识,而不是原地修改已有映射。
128+
129+
## 与 provider Extension 的关系
130+
131+
这个 provider 是已有私有 `integrations/external-context` 进程内的有界兼容功能,不是公开的 provider 注册表。第三方团队仍通过 MCP Extension 独立维护与发布其集成。只有 Qwen 维护者明确验证并愿意维护的 Mem0 系列契约,才适合直连预设路径。
132+
133+
同一语料库只能启用这个直连服务或另一个 Mem0 Extension,不能同时启用。暴露两个 `context_search` 工具会把 provider 选择交给模型,还可能产生重复查询。
134+
135+
## 验证
136+
137+
实现必须证明:
138+
139+
- 严格解析配置,并在预设或作用域无效时拒绝启动
140+
- 安全拼接 `origin` 与 `basePath`
141+
- 对每个预设准确映射认证、路径、作用域和数量字段
142+
- 逐条规范化响应,并将结果上限控制在 5
143+
- 保留历史预设的响应行为,同时限制 `mem0-v2` 的直接导入解码范围
144+
- 保守判断同步与异步写入的结果
145+
- 搜索和写入失败时不重试
146+
- 随包提供的 PolarDB 和 OSS 示例可加载
147+
- 保持现有 `mem0-platform-v3` 配置兼容

‎docs/users/features/mem0.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Use the endpoint origin, optionally with a reverse-proxy prefix; do not append `
2929
- `mem0-v3`: Mem0 Platform V3, `Authorization: Token`, V3 search/add.
3030
- `mem0-oss-2026-08`: pinned OSS REST contract, `X-API-Key`, `/search` and `/memories`.
3131

32-
These are complete contracts, not universal version compatibility. Unknown versions and different request/response shapes need a verified adapter, not a renamed URL. Historical preset IDs remain accepted; `aliyun-polardb-mysql-2026-08` preserves its historical `top_k` search field.
32+
These are complete contracts, not universal version compatibility. Unknown versions and different request/response shapes need a verified adapter, not a renamed URL. Historical preset IDs remain accepted; `aliyun-polardb-mysql-2026-08` preserves its historical `top_k` search field and raw search content.
3333

3434
A trusted PolarDB address such as `http://your-endpoint:8080` additionally needs `"allowInsecureHttp": true`. Plain HTTP sends the credential unencrypted. This setting does not make a private endpoint reachable or bypass IP whitelists.
3535

@@ -41,7 +41,7 @@ The default user/repository scope survives restart and starting from Git subdire
4141

4242
Search is read-only by default. To enable saving, add `"enableWrites": true` inside `memory.mem0`, restart the interactive CLI, and ask Qwen to save specific content. The automatically installed Hook asks you to approve the exact content, including in YOLO mode. Rejecting sends no write request. Writes use `infer: false`.
4343

44-
PolarDB can return a single-user message array encoded as JSON for these direct imports. Its presets restore that message's exact text when the result is marked `infer: false`; ordinary text and other protocols are left unchanged.
44+
PolarDB can return a single-user message array encoded as JSON for these direct imports. `mem0-v2` restores that message's exact text when the result is marked `infer: false`; the historical `aliyun-polardb-mysql-2026-08` preset, ordinary text, and other protocols are left unchanged.
4545

4646
Noninteractive/ACP sessions and sessions with Hooks disabled keep search only. Bare/safe mode, untrusted/provisional folders and SSH workspaces do not activate this local binding. Workspace settings cannot configure the binding.
4747

‎integrations/external-context/README.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# External Context extension
22

3+
For normal Mem0 use, configure `memory.mem0` in the main CLI's user settings instead. This private direct integration is the advanced, separately managed profile; it is not required for the bundled path and does not require a separate npm package release.
4+
35
This private Qwen Code integration connects one interactive CLI process to one
46
administrator-bound external context corpus without changing Qwen Core. It has
57
three managed deployment variants:
@@ -402,13 +404,16 @@ remain administrator-owned:
402404

403405
The built-in presets are:
404406

407+
- `mem0-v2`: PolarDB-style V2 search with `limit`, V1 direct import, and single-user direct-import message decoding.
408+
- `mem0-v3`: the same contract as `mem0-platform-v3`.
409+
- `mem0-oss-2026-08`: the same contract as `mem0-oss-rest-2026-08`.
405410
- `mem0-platform-v3`: `/v3` Platform API, `Authorization: Token`, fixed
406411
`appId`.
407412
- `mem0-oss-rest-2026-08`: stock self-hosted Mem0 `/search` and `/memories`,
408413
`X-API-Key`, fixed `userId`, and optional `agentId`.
409414
- `aliyun-polardb-mysql-2026-08`: PolarDB `/v2/memories/search` and
410-
`/v1/memories`, `Authorization: Token`, fixed `userId`, and optional
411-
`agentId`.
415+
`/v1/memories`, `Authorization: Token`, fixed `userId`, optional
416+
`agentId`, historical `top_k`, and raw search content.
412417

413418
#### Connecting a PolarDB Mem0 instance
414419

@@ -445,7 +450,7 @@ the configuration file. PolarDB documents optional `agentId` at the request
445450
root; use a stable value when memories must be isolated by application.
446451
447452
The preset records the whole protocol, not just one API version. Search always
448-
sends a maximum of five through the preset's `top_k` field. The engine does not
453+
sends a maximum of five through the preset's `top_k` or `limit` field. The engine does not
449454
probe versions or fall back to a different preset. See
450455
`examples/polardb-mem0.json`, `examples/polardb-mem0-loopback.json`,
451456
`examples/mem0-oss.json`, and the

‎integrations/external-context/src/mem0-presets.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ const legacyPresets = {
103103
limitField: 'top_k',
104104
idField: 'id',
105105
contentFields: ['memory'],
106-
directImportMessages: true,
107106
},
108107
write: {
109108
path: '/v1/memories',
@@ -120,6 +119,7 @@ export const MEM0_PRESETS: Readonly<Record<Mem0PresetId, Mem0Preset>> = {
120119
search: {
121120
...legacyPresets['aliyun-polardb-mysql-2026-08'].search,
122121
limitField: 'limit',
122+
directImportMessages: true,
123123
},
124124
},
125125
'mem0-v3': legacyPresets['mem0-platform-v3'],

‎integrations/external-context/src/providers.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ describe('Mem0CompatibleAdapter', () => {
776776
{
777777
preset: 'aliyun-polardb-mysql-2026-08' as const,
778778
scope: { userId: 'fixed-user' },
779-
unwrap: true,
779+
unwrap: false,
780780
},
781781
{
782782
preset: 'mem0-platform-v3' as const,

‎packages/cli/src/config/config.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,16 @@ describe('loadCliConfig', () => {
13421342
);
13431343
});
13441344

1345+
it('does not create a bundled server for an explicitly disabled Mem0', async () => {
1346+
const createServer = vi.spyOn(Mem0Settings, 'createBundledMem0Server');
1347+
process.argv = ['node', 'script.js', '-p', 'hello'];
1348+
await loadCliConfig(
1349+
{ memory: { mem0: null } } as unknown as Settings,
1350+
await parseArguments(),
1351+
);
1352+
expect(createServer).not.toHaveBeenCalled();
1353+
});
1354+
13451355
it('overrides a workspace-scoped external-context server instead of aborting startup', async () => {
13461356
const server = {
13471357
command: process.execPath,

‎packages/cli/src/config/config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2386,7 +2386,7 @@ export async function loadCliConfig(
23862386
sshWorkspace ||
23872387
provisionalWorkspace ||
23882388
!trustedFolder ||
2389-
settings.memory?.mem0 === undefined
2389+
settings.memory?.mem0 == null
23902390
? undefined
23912391
: createBundledMem0Server(
23922392
settings.memory.mem0,

0 commit comments

Comments
 (0)