Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d1fcbb6
feat(external-context): Support open-source Mem0 providers
yiliang114 Aug 24, 2026
b4c1839
test(external-context): Cover the open-source Mem0 example config
yiliang114 Aug 24, 2026
ee3fefb
refactor(external-context): Address review on the mem0 provider
yiliang114 Aug 24, 2026
04aaef5
fix(external-context): Ship the mem0 example with HTTPS defaults
yiliang114 Aug 24, 2026
ec2707f
test(external-context): prove shipped mem0 example constructs a provider
yiliang114 Aug 25, 2026
d729081
refactor(external-context): address mem0 review round — merge duplica…
yiliang114 Aug 25, 2026
5702f63
fix(external-context): Scope OSS mem0 by agent_id instead of app_id
yiliang114 Aug 25, 2026
a381a6f
fix(external-context): Scope Mem0 provider to PolarDB
yiliang114 Aug 25, 2026
184071d
feat(external-context): add versioned Mem0 presets
yiliang114 Aug 27, 2026
c02db5b
fix(external-context): survive an unverified Mem0 deployment build
yiliang114 Aug 27, 2026
b1ded13
feat(external-context): add a read-only Mem0 preset probe
yiliang114 Aug 27, 2026
52892f7
refactor(external-context): drop the vendor prefix from the PolarDB p…
yiliang114 Aug 27, 2026
657c3f5
fix(external-context): restore bounded Mem0 contracts
yiliang114 Aug 27, 2026
eb21ca1
docs(external-context): ship a PolarDB example that loads as it is sh…
yiliang114 Aug 27, 2026
68cd9a0
docs(external-context): clarify PolarDB setup
yiliang114 Aug 27, 2026
1ebda66
Merge branch 'main' into feat/external-context-mem0-provider
yiliang114 Aug 30, 2026
de5531a
Merge branch 'main' into feat/external-context-mem0-provider
yiliang114 Sep 1, 2026
50c7b2d
Merge branch 'main' into feat/external-context-mem0-provider
yiliang114 Sep 2, 2026
155b4c0
Merge branch 'main' into feat/external-context-mem0-provider
yiliang114 Sep 2, 2026
9ea2b62
Merge branch 'main' into feat/external-context-mem0-provider
yiliang114 Sep 3, 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
199 changes: 199 additions & 0 deletions docs/design/direct-external-context-mem0-presets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Direct External Context Mem0 Presets

**Status:** Implemented in the private direct integration

**Date:** 2026-08-27

**Related designs:**
[Direct External Context Provider](./direct-external-context-provider.md),
[Direct External Context Mem0 Write](./direct-external-context-mem0-write.md),
[External Context Provider Extensions](./external-context-provider-extensions.md)

## Decision

The private Direct External Context integration uses one `mem0` provider type
with an administrator-selected, versioned built-in preset for Mem0-compatible
REST services. A preset owns the upstream wire contract; instance
configuration owns only the deployment endpoint, credential reference, fixed
scope, and timeout inherited from the direct profile.

This replaces provider types that combine a product name with one hard-coded
protocol, such as `polardb-mem0`. It does not expose a free-form API version,
request template, custom header map, JSONPath expression, or dynamic provider
module. The model-facing MCP contract remains `context_search({ query })` and,
only for an explicitly enabled version 1 configuration whose preset defines a
verified direct-import operation, `context_remember({ content })`.

The existing `mem0-platform-v3` configuration remains accepted for backward
compatibility. New deployments use `type: "mem0"`.

## Why a preset instead of `apiVersion`

One upstream product can mix operation versions. PolarDB Mem0 search and
direct import, for example, use different versioned paths. Authentication,
scope placement, result fields, write response semantics, and trailing-slash
requirements also vary independently of a numeric API version.

A preset therefore identifies one complete, verified contract. Published
preset identifiers are immutable. An incompatible upstream change receives a
new identifier rather than silently changing an existing mapping.

The first built-in presets are:

- `mem0-platform-v3`
- `mem0-oss-rest-2026-08`
- `aliyun-polardb-mysql-2026-08`

## Configuration

```json
{
"version": 1,
"timeoutMs": 5000,
"provider": {
"type": "mem0",
"preset": "aliyun-polardb-mysql-2026-08",
"endpoint": {
"origin": "https://memory.example.com",
"basePath": ""
},
"credentialEnv": "MEM0_API_KEY",
"scope": {
"userId": "repository-memory",
"agentId": "qwen-code"
}
}
}
```

`origin` contains only scheme and authority. `basePath` is a static optional
reverse-proxy prefix. They are validated separately so joining a preset path
cannot discard a configured prefix or reinterpret endpoint authority.
Embedded URL credentials, query strings, fragments, dot segments, encoded path
material, whitespace, and control characters are rejected from endpoint
configuration.

HTTPS is required by default. Loopback HTTP is accepted for local relays.
Non-loopback HTTP requires the explicit `allowInsecureHttp` flag and sends the
credential and memory content in cleartext; it is intended only for deployments
whose trusted private network is deliberately part of the boundary.

The preset declares which scope values it consumes and whether each value is
required or optional. Startup fails closed when a required value is missing or
when configuration supplies a value the selected preset does not use. Scope is
administrator input and never appears in tool arguments.

One absolute `QWEN_EXTERNAL_CONTEXT_CONFIG` path is loaded once per MCP child.
The path, file contents, endpoint, preset, scope, and credential-to-corpus
binding must remain immutable for the whole Qwen session, including MCP child
restarts. Switching any of them requires a new Qwen session and a new config
path.

## Bounded preset contract

Built-in presets select only reviewed constants:

- `Authorization: Token`, `Authorization: Bearer`, or `X-API-Key`
- one static POST search path
- `top_k` or `limit` as the result-limit field
- `user_id`, `agent_id`, and `app_id` placed at the JSON root, under
`filters`, or omitted
- a closed set of fixed search options such as `threshold` and `rerank`
- a `results` response collection with reviewed identifier and content fields
- an optional static direct-import path and one reviewed response mapping

The engine always sends at most five as the provider limit and retains at most
five valid results. It does not retry, redirect, probe alternate paths, or
fallback between presets. A malformed item is dropped independently; a
malformed response envelope fails the request.

Adding a preset requires authoritative protocol evidence plus request and
response contract tests. A service that cannot fit this grammar uses its own
local or remote MCP Extension under the External Context Provider Extensions
design instead of expanding this configuration into a programming language.

## Initial mappings

### Mem0 Platform V3

- Search: `POST /v3/memories/search/`
- Authentication: `Authorization: Token`
- Scope: required `appId` under `filters.app_id`
- Limit: `top_k`
- Direct import: `POST /v3/memories/add/`, `infer: false`
- Write response: `PENDING` plus a UUID `event_id` is `accepted`; only
`SUCCEEDED` is `stored`

The legacy `mem0-platform-v3` configuration remains a fixed-endpoint shorthand
for this mapping.

### Mem0 OSS REST 2026-08

- Search: `POST /search`
- Authentication: `X-API-Key`
- Scope: required `userId` and optional `agentId` under `filters`
- Limit: `top_k`
- Direct import: `POST /memories`, `infer: false`
- Write response: a valid `results[].id` is `stored`; otherwise `unknown`

This mapping follows the stock `mem0ai/mem0` REST server. Bearer-authenticated
deployments need a separately verified preset rather than a per-instance
authentication override.

### Aliyun PolarDB MySQL 2026-08

- Search: `POST /v2/memories/search`
- Authentication: `Authorization: Token`
- Scope: required `userId` under `filters`, optional top-level `agentId`
- Limit: `top_k`
- Direct import: `POST /v1/memories`, `infer: false`
- Write response: a valid `results[].id` is `stored`; otherwise `unknown`

An `event_id` alone is never treated as proof of storage. If a later PolarDB
contract documents asynchronous event polling, that behavior requires a new
preset and write design rather than changing this preset in place.

## Protocol evidence

- Mem0 Platform V3 follows the official
[search API](https://docs.mem0.ai/api-reference/memory/search-memories) and
[V2-to-V3 migration contract](https://docs.mem0.ai/migration/platform-v2-to-v3).
- Mem0 OSS REST is pinned to upstream commit
[`39bc023`](https://github.com/mem0ai/mem0/tree/39bc02330563764e7d4465f1ecff5f002d94da1a),
specifically [`server/main.py`](https://github.com/mem0ai/mem0/blob/39bc02330563764e7d4465f1ecff5f002d94da1a/server/main.py)
and [`server/auth.py`](https://github.com/mem0ai/mem0/blob/39bc02330563764e7d4465f1ecff5f002d94da1a/server/auth.py).
- The PolarDB preset follows the official
[PolarDB for MySQL Mem0 contract](https://help.aliyun.com/en/polardb/polardb-for-mysql/use-polardb-mem0)
and the [live end-to-end evidence](https://github.com/QwenLM/qwen-code/pull/9952#issuecomment-5407141853)
recorded for this PR. The public documentation covers the endpoint,
authentication, and scope placement; the live instance additionally proves
that `infer: false` is honored for direct import.

Tests must continue to pin these exact request and response shapes. If an
upstream contract changes incompatibly, add a new preset identifier instead of
updating an existing mapping in place.

## Relationship to provider Extensions

This provider is a bounded compatibility feature inside the already private
`integrations/external-context` process. It is not a public provider registry.
Third-party teams still own independently distributed integrations through
MCP Extensions. The direct preset path is appropriate only for Mem0-family
contracts that Qwen maintainers explicitly verify and agree to maintain.

Operators must enable either this direct server or another Mem0 Extension for
one corpus, not both. Exposing two `context_search` tools for the same corpus
would make provider selection model-controlled and could duplicate queries.

## Verification

The implementation must prove:

- strict configuration parsing and fail-closed preset/scope validation
- safe `origin` plus `basePath` joining
- exact authentication, path, scope, and limit mapping for every preset
- per-item response normalization and the five-result cap
- conservative synchronous versus asynchronous write outcomes
- no retry on search or write failures
- loadability of the shipped PolarDB and OSS examples
- compatibility of the existing `mem0-platform-v3` configuration
46 changes: 33 additions & 13 deletions docs/design/direct-external-context-mem0-write.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ configuration containing the strict `write: { enabled: true }` block. The
default extension manifest, existing version 1 configurations, Generic HTTP,
and version 2 auto-recall remain write-free.

The tool sends the validated text unchanged through Mem0 V3 Direct Import with
`infer: false`. It does not pre-search, summarize, normalize, retry, poll, cache,
or deduplicate. A separately installed `PreToolUse` command Hook displays a
The tool sends the validated text unchanged through the selected, verified
Mem0 direct-import preset with `infer: false`. The legacy
`mem0-platform-v3` configuration remains supported. New `mem0` configurations
select an immutable built-in preset as defined in
[Direct External Context Mem0 Presets](./direct-external-context-mem0-presets.md).
The tool does not pre-search, summarize, normalize, retry, poll, cache, or
deduplicate. A separately installed `PreToolUse` command Hook displays a
reversible escaped representation of the complete text and asks the user to
confirm it. This confirmation is a direct-profile user-experience safeguard,
not a server-side authorization boundary.
Expand All @@ -30,8 +34,9 @@ not a server-side authorization boundary.
### Goals

- Let trusted collaborators save exact repository-shared text to one
administrator-bound Mem0 Project.
- Keep the Project credential and `app_id` outside model-controlled input.
administrator-bound Mem0 corpus.
- Keep the credential, endpoint, preset, and fixed scope outside
model-controlled input.
- Make the complete text visible before the MCP call executes.
- Perform at most one Provider request per approved tool invocation.
- Represent asynchronous and ambiguous Provider outcomes without claiming
Expand All @@ -56,7 +61,7 @@ sequenceDiagram
participant Q as Qwen Code
participant H as PreToolUse Hook
participant E as External Context MCP
participant P as Bound Mem0 Project
participant P as Bound Mem0 corpus
M->>Q: context_remember(content)
Q->>Q: Ordinary MCP permission check
Q->>H: Exact tool name, mode, and content
Expand Down Expand Up @@ -121,6 +126,12 @@ configuration validation. The default extension manifest still includes only
linking. Administrators must use the dedicated pinned MCP configuration whose
`includeTools` contains exactly search and remember.

New Mem0-compatible deployments may replace the legacy provider block with a
strict `type: "mem0"` block containing one built-in preset, endpoint,
credential reference, and preset-valid fixed scope. Writes are accepted only
when that preset defines a reviewed direct-import mapping. A custom endpoint
does not imply a custom write protocol.

The remember tool annotations are `readOnlyHint: false`,
`destructiveHint: false`, `idempotentHint: false`, and `openWorldHint: false`.
They describe behavior for clients; they are not permission or authorization.
Expand Down Expand Up @@ -174,7 +185,7 @@ content sent to Mem0.

## Mem0 request and result semantics

The adapter sends exactly one request:
The legacy Platform V3 adapter sends exactly one request:

```http
POST /v3/memories/add/
Expand All @@ -196,19 +207,28 @@ detection, so approving the same text twice can create two memories. The
integration intentionally does not add a hidden search or content hash because
neither would provide idempotency for an async remote operation.

The versioned `mem0` adapter also sends exactly one request, using only the
selected preset's fixed path, authentication, and scope placement. Platform V3
uses its asynchronous add response. The initial OSS REST and PolarDB presets
claim `stored` only for a valid `results[].id`; a bare `event_id`, alternate
root object, or root array is `unknown`. The client never treats request
acceptance as proof of persistence.

Result mapping is conservative:

| Provider outcome | Tool result |
| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| Valid `SUCCEEDED` | `stored` |
| Valid `PENDING` with UUID `event_id` | `accepted` with operation ID |
| Explicit `FAILED`, or HTTP 400, 401, 403, or 404 | `failed` with stable MCP error |
| Valid asynchronous `PENDING` with a UUID `event_id` | `accepted` with operation ID |
| Valid synchronous direct-import `results[].id` for a preset that defines it | `stored` with memory ID |
| Explicit `FAILED` in an asynchronous status response, or HTTP 400, 401, 403, or 404 | `failed` with stable MCP error |
| Timeout, cancellation, redirect, other HTTP status, broken or oversized response, invalid JSON, unknown status, or invalid identifier | `unknown` with MCP error |

Mem0 Add normally returns `PENDING`, so `accepted` is the expected successful
result and means queued, not persisted. `stored` is retained only for a valid
synchronous `SUCCEEDED` response. `failed` is a definitive rejection and tells
the model not to retry without changing the content or configuration.
Mem0 Platform Add normally returns `PENDING`, so `accepted` is the expected
successful result and means queued, not persisted. `stored` is retained only
for a valid synchronous `SUCCEEDED` response or a preset-defined synchronous
direct-import result. `failed` is a definitive rejection and tells the model
not to retry without changing the content or configuration.
`unknown` states that the Provider may already have accepted the write and
tells the model not to retry automatically. The integration never polls the
event or retries. User cancellation likewise does not prove that no record was
Expand Down
41 changes: 35 additions & 6 deletions docs/design/direct-external-context-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ The optional Phase 4 Mem0 variant adds an administrator-only
`context_remember({ content })` surface with a separate content-visible
confirmation Hook. Its detailed design is in
[Direct External Context Mem0 Write](./direct-external-context-mem0-write.md).
Versioned Mem0-compatible request mappings are defined in
[Direct External Context Mem0 Presets](./direct-external-context-mem0-presets.md).

The extension supports two explicit read adapters:
The extension supports three explicit read paths:

- Mem0 Platform V3 Search for repository-shared agent memory.
- The legacy fixed-endpoint Mem0 Platform V3 adapter.
- A bounded `mem0` adapter whose reviewed built-in preset selects Mem0
Platform V3, stock Mem0 OSS REST, or Aliyun PolarDB Mem0.
- Generic HTTP Search V1 for an existing knowledge base, RAG service, or
enterprise search endpoint.

Expand Down Expand Up @@ -144,9 +148,11 @@ The interface deliberately contains no tenant, user, repository, namespace,
application ID, or arbitrary filter. The explicit provider factory binds those
values from administrator-controlled configuration before a tool call.

Neither interface is a public package API. Adding another provider requires a
reviewed adapter and an explicit factory case. Generic HTTP remains
search-only; write semantics are deliberately provider-specific.
Neither interface is a public package API. Adding another provider family
requires a reviewed adapter and an explicit factory case. A Mem0-family wire
contract that fits the bounded preset grammar instead adds one immutable
built-in preset with contract tests. Generic HTTP remains search-only; write
semantics are deliberately provider-specific.

## Runtime behavior

Expand Down Expand Up @@ -245,6 +251,12 @@ and a shorter provider timeout.
}
```

New Mem0-compatible deployments use `type: "mem0"` with a versioned preset,
an `endpoint` containing separately validated `origin` and `basePath`, a
`credentialEnv`, and a fixed `scope`. The complete schema and compatibility
policy are defined in the Mem0 preset design. The legacy
`mem0-platform-v3` shape above remains accepted.

The managed launcher must control the configuration path and credential. An
MCP subprocess does not reload either value, but Qwen can restart the
subprocess after a disconnect or explicit MCP restart. The configuration path,
Expand Down Expand Up @@ -323,6 +335,21 @@ provider-side state change must verify that Memory Decay remains disabled.
Provider audit or access logs may still be retained. See
[Mem0 Memory Decay](https://docs.mem0.ai/platform/features/memory-decay).

### Versioned Mem0-compatible Search

The `mem0` adapter selects one immutable built-in preset at startup. The
initial presets cover Mem0 Platform V3, the stock Mem0 OSS REST server, and
Aliyun PolarDB Mem0. Each preset fixes authentication, search and direct-import
paths, scope placement, limit field, response fields, and write outcome
semantics. Instance configuration cannot override those fields.

The endpoint uses a separately validated origin and static base path. Search
always sends at most five results, never follows redirects, never retries, and
does not probe or fall back between protocol versions. Scope remains fixed
outside model input. See
[Direct External Context Mem0 Presets](./direct-external-context-mem0-presets.md)
for the exact mappings and extension boundary.

### Generic HTTP Search V1

The configured `baseUrl` must be an origin with no path, query, credentials, or
Expand Down Expand Up @@ -445,7 +472,8 @@ The optional auto-recall profile is implemented separately in
The optional Mem0 write variant is implemented separately in
[Direct External Context Mem0 Write](./direct-external-context-mem0-write.md).
The broader proposal in #7585 retains possible additional provider-specific
adapters where the Generic HTTP contract is not sufficient.
Extensions where neither Generic HTTP nor the bounded Mem0 preset contract is
sufficient.

The remaining items are not latent switches in either direct profile. They
require separate review and implementation.
Expand Down Expand Up @@ -473,3 +501,4 @@ require separate review and implementation.

- [Mem0 Organizations & Projects](https://docs.mem0.ai/api-reference/organizations-projects)
- [Mem0 Search Memories](https://docs.mem0.ai/api-reference/memory/search-memories)
- [Mem0 OSS server](https://github.com/mem0ai/mem0/tree/main/server)
7 changes: 7 additions & 0 deletions docs/design/external-context-provider-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ result to its existing `context_search`; it does not change Provider HTTP
requests, result ranking, write behavior, configuration schemas, or Auto
Recall output.

That private integration may carry a bounded set of maintainer-owned,
versioned Mem0 presets as documented in
[Direct External Context Mem0 Presets](./direct-external-context-mem0-presets.md).
Those built-ins are a compatibility surface for verified Mem0-family
contracts, not a public registry or an alternative distribution path for
third-party providers.

The reference MCP now rejects unrecognized `context_search` arguments instead
of silently ignoring them. Existing query-only calls are unchanged. A client
that sent undeclared selector or metadata fields must remove those fields; the
Expand Down
Loading
Loading