Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782fa392a5 | ||
|
|
9d60fd9f4e |
@@ -3106,6 +3106,23 @@ Removes Sidekick and its start/stop hooks. Memory capture, recall, and six skill
|
||||
|
||||
<Tab title="DeepSeek Harness">
|
||||
|
||||
<Update label="2026-09-11" description="deepseek-plugin v0.3.1">
|
||||
|
||||
**Fixed:**
|
||||
- Automatic recall uses the claimed human inbox message before Harness appends it to session history, so the first request in a fresh session can recall memories and later turns use the current prompt.
|
||||
- Native `dsh.bundle` activation loads Mem0 when the package is installed into a Harness profile. `pnpm pack` builds the plugin and includes its activation patch.
|
||||
- Automatic recall guidance points to the available memory search tool for deeper searches.
|
||||
|
||||
**Added:**
|
||||
- Optional `memoryScope: workspace` applies the same canonical workspace filter to automatic recall, capture, and both memory tools. Missing workspace paths skip automatic operations and reject explicit tools without falling back to shared memory.
|
||||
|
||||
**Upgrade notes:**
|
||||
- `memoryScope: user` remains the default and shares memories across workspaces. Existing memories are not migrated into workspace scopes; separate clones and worktrees have separate scopes when workspace mode is enabled.
|
||||
- Export `MEM0_USER_ID` alongside `MEM0_API_KEY`. Remove the old manual `insert: mem0` overlay; customize the bundle's existing row with an `id: mem0` override. See [DeepSeek Harness setup](/integrations/deepseek-plugin).
|
||||
- Capture extraction remains asynchronous. This patch does not change extraction policy or guarantee that every preference or numeric constraint becomes a memory.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="deepseek-plugin v0.3.0">
|
||||
|
||||
**Added:**
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Add persistent memory to DeepSeek Harness with automatic recall, a
|
||||
|
||||
Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-ai/deepseek-harness) with `@mem0/deepseek-plugin`. The plugin recalls relevant context before a model request, captures completed turns, and provides explicit Mem0 tools when the agent needs them.
|
||||
|
||||
<Info>Current package version: `0.3.0`.</Info>
|
||||
<Info>Current package version: `0.3.1`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
@@ -32,7 +32,7 @@ A Cordis plugin is a module exporting `apply(ctx, config)`. This plugin waits fo
|
||||
|
||||
Cordis removes the listeners and tools when the plugin unmounts. Memory failures are fail-open, so a Mem0 outage does not stop the agent from completing its normal work.
|
||||
|
||||
DeepSeek Harness provides subagents through separate host-composition packages. This Mem0 package does not register a named Sidekick or claim child filesystem isolation. A Harness child uses Mem0 only when its own agent preset includes the Mem0 plugin.
|
||||
DeepSeek Harness provides subagents through separate host-composition packages. This Mem0 package does not register a named Sidekick or claim child filesystem isolation. Memory availability in children depends on the Harness composition and service scope.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -40,25 +40,27 @@ DeepSeek Harness provides subagents through separate host-composition packages.
|
||||
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-deepseek-plugin">Sign up at app.mem0.ai</a>
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-deepseek-plugin">Get your API key</a> (starts with `m0-`)
|
||||
|
||||
2. The DeepSeek Harness installed.
|
||||
2. The DeepSeek Harness installed and a funded model-provider credential (`DEEPSEEK_API_KEY` for the official DeepSeek provider). This is separate from your Mem0 key.
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
3. Your Mem0 key and a stable user identity exported in the shell that launches Harness:
|
||||
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
echo 'export MEM0_USER_ID="your-user-id"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
echo 'export MEM0_USER_ID="your-user-id"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Try it locally
|
||||
|
||||
1. Build and pack the plugin:
|
||||
1. Build and pack the plugin from the full repository (the source imports the sibling shared core):
|
||||
```sh
|
||||
cd integrations/deepseek-plugin
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -70,33 +72,32 @@ source ~/.bashrc
|
||||
2. Install it into a disposable Harness profile so Harness supplies its peer dependencies:
|
||||
```sh
|
||||
DSH_HOME=/tmp/mem0-dsh-dev pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 \
|
||||
plugin --profile headless add /tmp/mem0-deepseek-plugin/mem0-deepseek-plugin-0.3.0.tgz
|
||||
plugin --profile web add /tmp/mem0-deepseek-plugin/mem0-deepseek-plugin-0.3.1.tgz
|
||||
```
|
||||
|
||||
3. Copy `cordis.example.yml`, set its installed package path and your `userId`, then load it with the same profile:
|
||||
3. Launch the same profile. The package's `dsh.bundle` activates Mem0 automatically:
|
||||
```sh
|
||||
DSH_HOME=/tmp/mem0-dsh-dev pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 \
|
||||
web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
web
|
||||
```
|
||||
|
||||
4. Open the web UI and ask the agent to remember something, then recall it in a later turn.
|
||||
4. Open the web UI, select a workspace, and state a synthetic preference without mentioning Mem0. Allow extraction to finish, start a fresh session, and ask for that preference without tools. Inspect the `mem0:recall` context to confirm automatic recall.
|
||||
|
||||
The `cordis.yml` entry looks like this:
|
||||
`pnpm pack` builds the package automatically and ships its activation patch. To customize it, copy `cordis.example.yml` and launch with `web --patch /absolute/path/to/cordis.yml`. The override updates the existing bundle row:
|
||||
|
||||
```yaml
|
||||
- name: "@deepseek-ai/dsh-system-prompt"
|
||||
- name: "@deepseek-ai/dsh-tools"
|
||||
- insert:
|
||||
- id: mem0
|
||||
name: "/tmp/mem0-dsh-dev/profiles/headless/node_modules/@mem0/deepseek-plugin/dist/index.js"
|
||||
config:
|
||||
# apiKey is read from MEM0_API_KEY when omitted here.
|
||||
userId: "your-user-id"
|
||||
autoRecall: true
|
||||
autoCapture: true
|
||||
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
|
||||
- id: mem0
|
||||
config:
|
||||
# A config override replaces the whole config, so restate userId.
|
||||
userId: !!js process.env.MEM0_USER_ID
|
||||
memoryScope: workspace
|
||||
autoRecall: true
|
||||
autoCapture: true
|
||||
# host: "https://your-onprem.mem0.ai" # optional: Platform dedicated base URL
|
||||
```
|
||||
|
||||
When upgrading from a manual installation, remove the old patch that inserts a `mem0` row; the bundle now inserts it. Keep custom settings as an override by `id`.
|
||||
|
||||
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override, not a switch to self-hosted Mem0 OSS.
|
||||
|
||||
## Configuration
|
||||
@@ -105,12 +106,27 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
|
||||
|---|---|---|---|
|
||||
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
|
||||
| `userId` | yes | | Default entity that owns the memories |
|
||||
| `memoryScope` | no | `user` | `user` shares memory across workspaces; `workspace` isolates automatic and explicit memory operations |
|
||||
| `allowUserOverride` | no | `false` | Permit model-selected access to a different user only in a trusted multi-user deployment |
|
||||
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
|
||||
| `autoRecall` | no | `true` | Recall relevant memory before model requests |
|
||||
| `autoCapture` | no | `true` | Store completed human and assistant turns |
|
||||
|
||||
Both tools also accept optional per-call `userId`, `agentId`, and `runId` params so a single install can partition memory by entity, agent, or session; `userId` defaults to the configured user, while `agentId` and `runId` are omitted unless supplied. Automatic recall and capture use the configured user without an agent, repository, or session filter. Automatic capture preserves full redacted user and assistant message text without a per-message character cutoff.
|
||||
Both tools also accept optional per-call `userId`, `agentId`, and `runId` params so a single install can partition memory by entity, agent, or session; `userId` defaults to the configured user, while `agentId` and `runId` are omitted unless supplied. Automatic capture preserves full redacted user and assistant message text without a per-message character cutoff.
|
||||
|
||||
## Workspace scope
|
||||
|
||||
By default, automatic capture and recall use the configured user across all workspaces. This is intentional compatibility behavior. To isolate workspaces, set `memoryScope: workspace` as shown above. All writes then attach an `appId` derived from the canonical absolute session workspace path; all searches require its matching `app_id`. The model cannot override that workspace filter.
|
||||
|
||||
Symlink aliases share a scope. Different directories, clones, or worktrees have different scopes; moving a directory changes its scope. Missing or invalid workspace paths skip automatic memory operations and reject explicit tools without falling back to user-wide access. Existing user-only memories are not migrated into workspace scopes.
|
||||
|
||||
User scope searches all memories belonging to the user, including workspace-tagged memories. Workspace isolation applies only when workspace scope is configured; it is application filtering, not a separate Mem0 account or credential.
|
||||
|
||||
## Extraction and recall limits
|
||||
|
||||
Capture sends the full redacted turn to Mem0 for asynchronous extraction. A queued write, or even an initially nonempty memory list, does not mean every extracted fact is ready. Allow processing to finish and inspect stored memories before diagnosing lost preferences or missing filenames and numeric constraints. Exact extraction remains model-dependent; a one-off request should not automatically become a standing preference.
|
||||
|
||||
Automatic recall is a shallow first pass: up to five results, 4,000 context characters, and a two-second wait. The explicit `search_memory` tool can use a more focused query and returns up to ten results by default.
|
||||
|
||||
## Telemetry
|
||||
|
||||
@@ -124,6 +140,6 @@ Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0 can attribute usage to thi
|
||||
|
||||
- **`MISSING_CREDENTIAL` for `deepseek-official`**: Configure `DEEPSEEK_API_KEY` through Harness's Models page or export it in the shell that launches Harness.
|
||||
- **`EMFILE: too many open files, watch` on macOS**: Launch Harness with `CHOKIDAR_USEPOLLING=1`.
|
||||
- **Mem0 tools do not appear**: Run Harness with `--dump-config` and confirm the final composition contains the `mem0` row and the installed `dist/index.js` path.
|
||||
- **Mem0 tools do not appear**: Run Harness with `--dump-config` for the same profile used at installation and confirm it contains a `mem0` row naming `@mem0/deepseek-plugin`. Export `MEM0_USER_ID` and `MEM0_API_KEY` before launching.
|
||||
|
||||
Per-call `userId` overrides are rejected unless the operator enables `allowUserOverride: true`. Automatic recall and capture always use the configured user.
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
---
|
||||
title: Hermes Agent
|
||||
description: "Add long-term memory to Hermes agents with Mem0 using the managed Platform, a self-hosted server, or fully local OSS."
|
||||
description: "Add long-term memory to Hermes agents with Mem0, on managed Mem0 Cloud or fully self-hosted (OSS), with automatic background sync and zero-latency prefetch."
|
||||
---
|
||||
|
||||
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 learns facts from your conversations and surfaces relevant ones for the current question, without slowing down the chat.
|
||||
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 learns facts from your conversations and surfaces relevant ones before each turn, without slowing down the chat.
|
||||
|
||||
You can run Mem0 in three ways:
|
||||
You can run Mem0 in two ways:
|
||||
|
||||
- **Platform mode** (default): managed Mem0 Cloud. Add your API key and you are ready.
|
||||
- **Self-hosted server mode**: point the plugin at a Mem0 server you run yourself (the Docker-shipped server). The plugin only talks HTTP to your server.
|
||||
- **OSS mode**: run Mem0 in-process with your own LLM, embedder, and vector store. No Mem0 server required.
|
||||
- **OSS mode**: fully self-hosted with your own LLM, embedder, and vector store. No data leaves your machine.
|
||||
|
||||
## How It Works
|
||||
|
||||
Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alongside one external provider. When Mem0 is active, it works additively with the built-in system at two points in every conversation turn.
|
||||
Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alongside one external provider. When Mem0 is active, it works additively with the built-in system at three points in every conversation turn.
|
||||
|
||||
### 1. Current-turn recall (bounded wait)
|
||||
### 1. Before the agent responds (prefetch)
|
||||
|
||||
When you send a message, Hermes searches your stored memories for the current question and waits up to 3 seconds for results. If they arrive in time, they are injected into the system prompt so the model can see them. If the backend is slower, Hermes skips the injection and the model can still call `mem0_search` itself — so a slow backend never blocks a turn.
|
||||
When you send a message, Hermes checks for cached Mem0 search results from the previous turn. If they exist, those memories are injected into the system prompt so the model can see them. This is zero-latency, with no waiting on an API call.
|
||||
|
||||
### 2. Background fact extraction (sync)
|
||||
### 2. After the agent responds (sync)
|
||||
|
||||
Once the model finishes, Hermes sends the `(user message, assistant response)` pair to Mem0 in a background thread. Mem0 extracts facts automatically (for example, "user prefers Python" or "user works at Acme Corp"), so you never have to tell it what to remember. Each write is tagged with the gateway channel it came from.
|
||||
|
||||
### 3. Background prefetch for the next turn
|
||||
|
||||
At the same time, Hermes runs a background search to pre-load relevant memories for your next message. By the time you type, the results are already cached.
|
||||
|
||||
## Agent Tools
|
||||
|
||||
When Mem0 is active, the model gets four tools it can call during a conversation:
|
||||
When Mem0 is active, the model gets five tools it can call during a conversation:
|
||||
|
||||
| Tool | Description | Parameters |
|
||||
|------|-------------|------------|
|
||||
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (default `false`, Platform mode only) |
|
||||
| `mem0_list` | List all stored memories, for a full overview | `page`, `page_size` (default 100, max 200) |
|
||||
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (default `true`, Platform mode only) |
|
||||
| `mem0_add` | Store a fact verbatim, with no LLM extraction | `content` (required) |
|
||||
| `mem0_update` | Update a memory's text by ID | `memory_id`, `text` (both required) |
|
||||
| `mem0_delete` | Delete a memory by ID | `memory_id` (required) |
|
||||
@@ -75,36 +79,6 @@ memory:
|
||||
|
||||
That's it. Mem0 runs automatically from here.
|
||||
|
||||
## Self-Hosted Server Setup
|
||||
|
||||
Run the [Mem0 server](https://github.com/mem0ai/mem0/tree/main/server) (FastAPI + pgvector) from its Docker image and point the plugin at it. Unlike OSS mode, the plugin just talks HTTP to your server.
|
||||
|
||||
### Interactive
|
||||
|
||||
```bash
|
||||
hermes memory setup
|
||||
# Select "mem0", then "Self-hosted server", and enter the server URL
|
||||
```
|
||||
|
||||
### With flags
|
||||
|
||||
```bash
|
||||
hermes memory setup mem0 --mode selfhosted \
|
||||
--host http://localhost:8888 \
|
||||
--api-key your-admin-api-key
|
||||
```
|
||||
|
||||
### With environment variables
|
||||
|
||||
```bash
|
||||
echo "MEM0_HOST=http://localhost:8888" >> ~/.hermes/.env
|
||||
echo "MEM0_API_KEY=your-admin-api-key" >> ~/.hermes/.env
|
||||
```
|
||||
|
||||
Then start a fresh Hermes session and call `mem0_search` — it connects to your server. The plugin authenticates with `X-API-Key` and uses the server's `/search` and `/memories` routes. The API key is optional only for servers running with `AUTH_DISABLED`.
|
||||
|
||||
<Note>Setting `host` routes to the self-hosted server automatically. Don't combine it with `mode: oss` — OSS takes precedence and ignores `host`.</Note>
|
||||
|
||||
## OSS (Self-Hosted) Setup
|
||||
|
||||
OSS mode runs Mem0 entirely on your own infrastructure: your LLM, your embedder, and your vector store. No data is sent to Mem0 Cloud, and no Mem0 API key is required.
|
||||
@@ -137,20 +111,15 @@ hermes memory setup mem0 --mode oss \
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--mode` | `platform`, `selfhosted`, or `oss` |
|
||||
| `--api-key` | Platform API key, or the admin key of a self-hosted server |
|
||||
| `--host` | Self-hosted server URL (with `--mode selfhosted`) |
|
||||
| `--mode` | `platform` or `oss` |
|
||||
| `--oss-llm` | LLM provider (`openai` or `ollama`, default `openai`) |
|
||||
| `--oss-llm-key` | LLM API key (for `openai`) |
|
||||
| `--oss-llm-model` | Override the LLM model |
|
||||
| `--oss-llm-url` | LLM base URL (for `ollama` or a custom endpoint) |
|
||||
| `--oss-embedder` | Embedder provider (default `openai`) |
|
||||
| `--oss-embedder-key` | Embedder API key |
|
||||
| `--oss-embedder-model` | Override the embedder model |
|
||||
| `--oss-embedder-url` | Embedder base URL (for `ollama` or a custom endpoint) |
|
||||
| `--oss-vector` | Vector store (`qdrant` or `pgvector`, default `qdrant`) |
|
||||
| `--oss-vector-path` | Local Qdrant storage path |
|
||||
| `--oss-vector-url` | Qdrant server URL |
|
||||
| `--oss-vector-host`, `--oss-vector-port` | PGVector or remote Qdrant host and port |
|
||||
| `--oss-vector-user`, `--oss-vector-password`, `--oss-vector-dbname` | PGVector connection details |
|
||||
| `--user-id` | Canonical user identifier |
|
||||
@@ -158,7 +127,7 @@ hermes memory setup mem0 --mode oss \
|
||||
|
||||
## Switching Modes
|
||||
|
||||
You can move between the three modes at any time. Run the setup command again, or edit `~/.hermes/mem0.json` directly.
|
||||
You can move between Platform and OSS at any time. Run the setup command again, or edit `~/.hermes/mem0.json` directly.
|
||||
|
||||
```bash
|
||||
# Platform to OSS
|
||||
@@ -167,9 +136,6 @@ hermes memory setup mem0 --mode oss --oss-llm-key sk-...
|
||||
# OSS to Platform
|
||||
hermes memory setup mem0 --mode platform --api-key sk-...
|
||||
|
||||
# Platform to a self-hosted server
|
||||
hermes memory setup mem0 --mode selfhosted --host http://localhost:8888
|
||||
|
||||
# Preview without writing anything
|
||||
hermes memory setup mem0 --mode oss --oss-llm-key sk-... --dry-run
|
||||
```
|
||||
@@ -180,7 +146,7 @@ A self-hosted `~/.hermes/mem0.json` looks like this:
|
||||
{
|
||||
"mode": "oss",
|
||||
"oss": {
|
||||
"llm": {"provider": "openai", "config": {"model": "gpt-5-mini", "is_reasoning_model": true}},
|
||||
"llm": {"provider": "openai", "config": {"model": "gpt-5-mini"}},
|
||||
"embedder": {"provider": "openai", "config": {"model": "text-embedding-3-small"}},
|
||||
"vector_store": {"provider": "qdrant", "config": {"path": "~/.hermes/mem0_qdrant"}}
|
||||
}
|
||||
@@ -193,12 +159,11 @@ Behavioral settings live in `~/.hermes/mem0.json` and are written for you by `he
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-managed, in-process). Self-hosted server routing is set via `host` |
|
||||
| `host` | none | Self-hosted Mem0 server URL. When set, the plugin talks HTTP to your server instead of the cloud |
|
||||
| `api_key` | none | Mem0 Platform API key, or the admin key of a self-hosted server. Stored in `.env` as `MEM0_API_KEY` |
|
||||
| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-hosted) |
|
||||
| `api_key` | none | Mem0 Platform API key, required in Platform mode. Stored in `.env` as `MEM0_API_KEY` |
|
||||
| `user_id` | `hermes-user` | Identifier that scopes memories. See cross-channel behavior below |
|
||||
| `agent_id` | `hermes` | Agent identifier attached to writes |
|
||||
| `rerank` | `false` | Rerank search results for relevance (Platform mode only) |
|
||||
| `rerank` | `true` | Rerank search results for relevance (Platform mode only) |
|
||||
|
||||
### Cross-channel memories
|
||||
|
||||
@@ -209,11 +174,12 @@ Hermes can run from the CLI and from gateways like Telegram, Slack, and Discord.
|
||||
|
||||
Either way, every write is tagged with `metadata.channel` (for example `telegram` or `cli`), so per-channel views are still possible at query time.
|
||||
|
||||
|
||||
## Reliability
|
||||
|
||||
- **Circuit breaker**: if Mem0 fails five times in a row, Hermes pauses calls for two minutes, then retries. The agent keeps working without memory during that window. Expected client errors, like a 404 on a missing memory id, do not count toward tripping the breaker.
|
||||
- **Non-blocking**: fact extraction runs in a background daemon thread, and current-turn recall waits at most 3 seconds, so a slow or failed call never blocks your conversation.
|
||||
- **Thread-safe**: the client uses lazy initialization with locking, and the background sync and recall threads are guarded so concurrent gateway messages cannot produce duplicate memories.
|
||||
- **Non-blocking**: every Mem0 call runs in a background daemon thread, so a slow or failed call never blocks your conversation.
|
||||
- **Thread-safe**: the client uses lazy initialization with locking, and the background sync and prefetch threads are guarded so concurrent gateway messages cannot produce duplicate memories.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -222,7 +188,6 @@ Either way, every write is tagged with `metadata.channel` (for example `telegram
|
||||
The circuit breaker tripped after five consecutive failures and resets after two minutes.
|
||||
|
||||
- **Platform mode**: check your API key and internet connection.
|
||||
- **Self-hosted server mode**: check that the server is running and reachable at the configured `host` URL.
|
||||
- **OSS mode**: make sure your vector store (Qdrant or PGVector) is running and reachable.
|
||||
|
||||
### OSS: vector store connection refused
|
||||
@@ -252,8 +217,8 @@ curl http://localhost:11434/api/tags
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Three ways to run**: managed Platform, a self-hosted server, or fully local OSS, switchable at any time.
|
||||
2. **Current-turn recall**: memories for the current question are injected within a 3-second window, with `mem0_search` as the model's own backstop.
|
||||
1. **Two ways to run**: managed Platform or fully self-hosted OSS, switchable at any time.
|
||||
2. **Zero-latency recall**: memories are prefetched in the background and cached before you type.
|
||||
3. **Automatic extraction**: Mem0 extracts and deduplicates facts from each exchange for you.
|
||||
4. **Non-blocking and fault tolerant**: background threads plus a circuit breaker keep the agent responsive even when Mem0 is unreachable.
|
||||
5. **Additive memory**: works alongside Hermes' built-in file memory (`MEMORY.md`, `USER.md`).
|
||||
|
||||
@@ -149,7 +149,7 @@ export async function buildRecallContext(
|
||||
if (!unseen.length) return "";
|
||||
|
||||
const prefix =
|
||||
"<mem0-relevant-memories>\nRetrieved automatically for the current request. This is a shallow first pass — search mem0_memory for more if you need it.\n";
|
||||
"<mem0-relevant-memories>\nRetrieved automatically for the current request. This is a shallow first pass — use the available memory search tool for more if you need it.\n";
|
||||
const suffix = "\n</mem0-relevant-memories>";
|
||||
const maxChars = options.maxChars ?? DEFAULT_MAX_CONTEXT_CHARS;
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -13,7 +13,7 @@ It gives a Harness agent automatic long-term memory plus two explicit memory too
|
||||
|
||||
Unlike the local/file-based memory plugins in the ecosystem, Mem0 is a managed backend: server-side extraction, semantic dedup and conflict resolution, and memories that other agents can retrieve when their user and entity filters match.
|
||||
|
||||
Current package version: `0.3.0`.
|
||||
Current package version: `0.3.1`.
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](../claude-code-plugin/README.md#sonnet-sidekick-agent).
|
||||
|
||||
@@ -47,21 +47,27 @@ Cordis owns listener and tool cleanup when the plugin unmounts. Every automatic
|
||||
mkdir -p /tmp/mem0-deepseek-plugin
|
||||
pnpm pack --pack-destination /tmp/mem0-deepseek-plugin
|
||||
```
|
||||
2. Set your Mem0 key:
|
||||
2. Set your Mem0 key, a stable user identity, and a funded model API key:
|
||||
```sh
|
||||
export MEM0_API_KEY=...
|
||||
export MEM0_USER_ID=your-user-id
|
||||
export DEEPSEEK_API_KEY=...
|
||||
```
|
||||
3. Install it into a disposable Harness profile:
|
||||
```sh
|
||||
DSH_HOME=/tmp/mem0-dsh-dev pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 \
|
||||
plugin --profile headless add /tmp/mem0-deepseek-plugin/mem0-deepseek-plugin-0.3.0.tgz
|
||||
plugin --profile web add /tmp/mem0-deepseek-plugin/mem0-deepseek-plugin-0.3.1.tgz
|
||||
```
|
||||
4. Copy `cordis.example.yml`, set its installed package path and your `userId`, then run Harness with the same profile:
|
||||
4. Run Harness with the same profile. The installed bundle activates Mem0 automatically:
|
||||
```sh
|
||||
DSH_HOME=/tmp/mem0-dsh-dev pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 \
|
||||
web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
web
|
||||
```
|
||||
5. Open http://127.0.0.1:3080 and ask the agent to remember something, then recall it in a later turn.
|
||||
5. Open http://127.0.0.1:3080, select a workspace, and state a synthetic preference without mentioning Mem0. Wait for asynchronous extraction to finish, then start a fresh session and ask for that preference without tools. Inspect the `mem0:recall` context to verify automatic recall.
|
||||
|
||||
Build from the full repository: the source imports the sibling shared core. `pnpm pack` runs the build and includes the activation patch. An installed tarball is self-contained. For custom settings, copy `cordis.example.yml` and pass its absolute path with `--patch`. When upgrading from the old manual setup, remove the old patch that **inserts** a `mem0` row; the bundle now inserts it.
|
||||
|
||||
On macOS, set `CHOKIDAR_USEPOLLING=1` if Harness reports `EMFILE`. The Mem0 key and model-provider key are separate credentials.
|
||||
|
||||
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` overrides the Platform base URL. It does not support the self-hosted Mem0 OSS API.
|
||||
|
||||
@@ -71,6 +77,7 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
|
||||
|---|---|---|---|
|
||||
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
|
||||
| `userId` | yes | | Entity that owns the memories |
|
||||
| `memoryScope` | no | `user` | `user` shares memory across workspaces; `workspace` isolates all automatic and explicit operations by the session workspace |
|
||||
| `allowUserOverride` | no | `false` | Permit model-selected access to a different user only in a trusted multi-user deployment |
|
||||
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
|
||||
| `autoRecall` | no | `true` | Recall relevant memory before model requests |
|
||||
@@ -80,10 +87,20 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
|
||||
|
||||
Automatic capture and recall use the configured `userId` across sessions. Automatic writes do not attach a repository ID or `runId`.
|
||||
|
||||
This cross-workspace sharing is intentional compatibility behavior. Set `memoryScope: workspace` in the profile patch to isolate memory. The plugin then adds an `appId` derived from the canonical absolute session workspace path to every write and the matching `app_id` to every search. Symlink aliases share a scope; different directories (including separate clones or worktrees) do not. Moving a workspace changes its scope. Tools cannot override it. Missing or invalid workspace paths skip automatic memory operations and reject explicit tools, without falling back to user-wide access.
|
||||
|
||||
Workspace scope does not migrate existing user-only memories. User scope still searches all memories for that user, including workspace-tagged memories; isolation applies when the plugin is configured with workspace scope. These filters are application-level separation, not separate Mem0 credentials.
|
||||
|
||||
Both `search_memory` and `add_memory` accept optional `agentId` and `runId`. On search, these narrow the returned memories; on add, they attach those identities to the stored memory. Pass a known `runId` to search memories explicitly saved with that session ID. This does not include automatically captured user-only memories or identify the session making the request.
|
||||
|
||||
Per-call `userId` overrides are rejected unless the operator enables `allowUserOverride: true`. Automatic recall and capture always use the configured user.
|
||||
|
||||
## Extraction and recall limits
|
||||
|
||||
The plugin sends full redacted completed turns to Mem0; Mem0 extracts the memories asynchronously. A queued write is not proof that every extracted fact is already searchable. Even an initial nonempty memory list can be incomplete. Check the stored memories after processing before diagnosing extraction loss.
|
||||
|
||||
Automatic recall is a bounded first pass (up to five results, 4,000 context characters, and a two-second wait). `search_memory` uses a focused query and returns up to ten results by default. Exact preference extraction remains model-dependent; include filenames and numeric constraints in release tests, and distinguish standing preferences from one-off requests.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
# Example DeepSeek Harness config that loads deepseek-plugin alongside the built-in
|
||||
# tools plugin. Load with:
|
||||
#
|
||||
# DSH_HOME=/tmp/mem0-dsh-dev pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 \
|
||||
# web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
#
|
||||
- insert:
|
||||
- id: mem0
|
||||
# Install the packed plugin into this disposable profile first; loading
|
||||
# raw dist bypasses Harness's peer dependency installation.
|
||||
name: "/tmp/mem0-dsh-dev/profiles/headless/node_modules/@mem0/deepseek-plugin/dist/index.js"
|
||||
config:
|
||||
# apiKey is read from the MEM0_API_KEY env var when omitted here.
|
||||
userId: "your-user-id"
|
||||
# autoRecall: true # optional; enabled by default
|
||||
# autoCapture: true # optional; enabled by default
|
||||
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
|
||||
# Optional override for the mem0 row installed by dsh.bundle.
|
||||
# Copy this file, then use: dsh web --patch /absolute/path/to/cordis.yml
|
||||
- id: mem0
|
||||
config:
|
||||
# Replacing a row's config requires restating its userId.
|
||||
# apiKey is read from MEM0_API_KEY when omitted here.
|
||||
userId: !!js process.env.MEM0_USER_ID
|
||||
memoryScope: workspace # optional; default is user (shared across workspaces)
|
||||
# autoRecall: true
|
||||
# autoCapture: true
|
||||
# host: "https://your-onprem.mem0.ai" # Mem0 Platform dedicated deployment
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
- insert:
|
||||
- id: mem0
|
||||
name: "@mem0/deepseek-plugin"
|
||||
config:
|
||||
# Credentials stay in the process environment, not the bundle.
|
||||
userId: !!js process.env.MEM0_USER_ID
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/deepseek-plugin",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
@@ -29,13 +29,21 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"cordis.patch.yml",
|
||||
"cordis.example.yml",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepack": "pnpm build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import { MemoryClient } from "mem0ai";
|
||||
import { formatMemoryList, formatAddResult } from "./formatting.ts";
|
||||
import { truncateOutput } from "./output.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
import { resolveSearchFilters, resolveAddParams, resolveWorkspaceId } from "./scoping.ts";
|
||||
import { captureEvent, errorKind } from "./telemetry.ts";
|
||||
import { createMemoryLifecycle } from "../../agent-plugin-core/typescript/src/lifecycle.ts";
|
||||
|
||||
@@ -44,6 +44,7 @@ interface HarnessMessage {
|
||||
interface SessionState {
|
||||
lifecycle: ReturnType<typeof createMemoryLifecycle>;
|
||||
messages: HarnessMessage[];
|
||||
currentPrompt?: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@@ -51,6 +52,8 @@ export interface Config {
|
||||
apiKey?: string;
|
||||
/** Default entity that owns the memories (Mem0 user scope). */
|
||||
userId: string;
|
||||
/** Share user memory across workspaces (default), or isolate by session workspace. */
|
||||
memoryScope?: "user" | "workspace";
|
||||
/** Explicitly allow model-selected cross-user access. Defaults to false. */
|
||||
allowUserOverride?: boolean;
|
||||
/** Optional Mem0 Platform base-URL override (on-prem / dedicated); defaults to api.mem0.ai. Not a switch to self-hosted OSS. */
|
||||
@@ -98,6 +101,19 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (!userId || /^\*+$/.test(userId)) {
|
||||
throw new Error("deepseek-plugin: config.userId is required");
|
||||
}
|
||||
if (config.memoryScope !== undefined && !["user", "workspace"].includes(config.memoryScope)) {
|
||||
throw new Error("deepseek-plugin: config.memoryScope must be user or workspace");
|
||||
}
|
||||
const workspaceId = (session?: { header: { cwd?: string } }) =>
|
||||
config.memoryScope === "workspace" ? resolveWorkspaceId(session?.header.cwd) : undefined;
|
||||
const searchFilters = (params: Parameters<typeof resolveSearchFilters>[0], session?: { header: { cwd?: string } }) => {
|
||||
const appId = workspaceId(session);
|
||||
return { ...resolveSearchFilters(params, userId), ...(appId ? { app_id: appId } : {}) };
|
||||
};
|
||||
const addParams = (params: Parameters<typeof resolveAddParams>[0], session?: { header: { cwd?: string } }) => {
|
||||
const appId = workspaceId(session);
|
||||
return { ...resolveAddParams(params, userId), ...(appId ? { appId } : {}) };
|
||||
};
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey,
|
||||
@@ -123,13 +139,20 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}, client);
|
||||
|
||||
if (config.autoRecall !== false) {
|
||||
// Harness assembles the prompt before appending claimed messages to history.
|
||||
ctx.on("agent/inbox/claimed", ({ agent, message }) => {
|
||||
if (message.source.kind !== "user") return;
|
||||
const state = stateFor(agent.session);
|
||||
state.currentPrompt = state.lifecycle.prepareConversation([message]).at(-1)?.content;
|
||||
});
|
||||
|
||||
ctx.on("system-prompt/assemble", async (_input, context, next): Promise<PromptAssembly> => {
|
||||
const assembly = await next();
|
||||
const { agent, signal } = context;
|
||||
if (!agent || signal?.aborted) return assembly;
|
||||
|
||||
const state = stateFor(agent.session);
|
||||
const prompt = state.lifecycle.prepareConversation(
|
||||
const prompt = state.currentPrompt ?? state.lifecycle.prepareConversation(
|
||||
agent.session
|
||||
.deriveMessages()
|
||||
.filter((message) => message.role === "user" && message.source?.kind === "user"),
|
||||
@@ -140,7 +163,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const result = await client.search(query, {
|
||||
filters: resolveSearchFilters({}, userId),
|
||||
filters: searchFilters({}, agent.session),
|
||||
topK: AUTO_RECALL_LIMIT,
|
||||
});
|
||||
captureEvent("deepseek.recall.auto", {
|
||||
@@ -179,8 +202,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const conversation = state.lifecycle.prepareConversation(state.messages);
|
||||
state.messages = [];
|
||||
if (event.data.reason.kind !== "completed" || conversation.length === 0) return;
|
||||
void client
|
||||
.add(conversation, { userId, source: SOURCE })
|
||||
void Promise.resolve()
|
||||
.then(() => client.add(conversation, { ...addParams({}, session), source: SOURCE }))
|
||||
.then(() => captureEvent("deepseek.capture.auto", {
|
||||
success: true,
|
||||
message_count: conversation.length,
|
||||
@@ -209,12 +232,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...scopeParams,
|
||||
},
|
||||
output: textOutput,
|
||||
async execute({ query, limit, userId: u, agentId, runId }) {
|
||||
async execute({ query, limit, userId: u, agentId, runId }, exec) {
|
||||
if (u?.trim() && u.trim() !== userId && config.allowUserOverride !== true) {
|
||||
throw new Error("Cross-user access requires allowUserOverride in plugin configuration.");
|
||||
}
|
||||
const safeQuery = toolLifecycle.prepareUserText(query);
|
||||
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
|
||||
const filters = searchFilters({ userId: u, agentId, runId }, exec.agent?.session);
|
||||
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
|
||||
const started = Date.now();
|
||||
try {
|
||||
@@ -262,17 +285,17 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...scopeParams,
|
||||
},
|
||||
output: textOutput,
|
||||
async execute({ text, userId: u, agentId, runId }) {
|
||||
async execute({ text, userId: u, agentId, runId }, exec) {
|
||||
if (u?.trim() && u.trim() !== userId && config.allowUserOverride !== true) {
|
||||
throw new Error("Cross-user access requires allowUserOverride in plugin configuration.");
|
||||
}
|
||||
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
|
||||
const params = addParams({ userId: u, agentId, runId }, exec.agent?.session);
|
||||
const started = Date.now();
|
||||
try {
|
||||
const result = await client.add(
|
||||
[{ role: "user", content: toolLifecycle.prepareUserText(text) }],
|
||||
{
|
||||
...addParams,
|
||||
...params,
|
||||
source: SOURCE,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { realpathSync } from "node:fs";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
export {
|
||||
entityAddParams as resolveAddParams,
|
||||
entitySearchFilters as resolveSearchFilters,
|
||||
} from "../../agent-plugin-core/typescript/src/identity.ts";
|
||||
export type { EntityParams } from "../../agent-plugin-core/typescript/src/identity.ts";
|
||||
|
||||
export function resolveWorkspaceId(cwd: string | undefined): string {
|
||||
if (!cwd || !isAbsolute(cwd)) {
|
||||
throw new Error("Workspace memory scope requires an absolute session workspace path.");
|
||||
}
|
||||
return `deepseek-workspace-${createHash("sha256").update(realpathSync(cwd)).digest("hex")}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Offline mock of the Mem0 SDK so these tests never touch the network.
|
||||
const mockSearch = vi.fn();
|
||||
@@ -75,6 +78,11 @@ describe("apply() config validation", () => {
|
||||
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
|
||||
});
|
||||
|
||||
it("rejects unknown memory scope instead of silently using user-wide access", () => {
|
||||
expect(() => applyAndCollect({ apiKey: "k", userId: "u", memoryScope: "typo" } as unknown as Config))
|
||||
.toThrow(/memoryScope/);
|
||||
});
|
||||
|
||||
it("registers both memory tools", () => {
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
expect([...tools.keys()].sort()).toEqual(["add_memory", "search_memory"]);
|
||||
@@ -82,6 +90,35 @@ describe("apply() config validation", () => {
|
||||
});
|
||||
|
||||
describe("Harness lifecycle", () => {
|
||||
it("recalls the claimed inbox message before Harness appends it to session history", async () => {
|
||||
mockSearch.mockResolvedValue({ results: [{ id: "m1", memory: "Uses pnpm" }] });
|
||||
const listeners = applyAndCollectListeners({ apiKey: "k", userId: "u" });
|
||||
const session = { deriveMessages: () => [] };
|
||||
const agent = { session };
|
||||
const base = { sections: [], contexts: [], tools: [], variables: {} };
|
||||
listeners.get("agent/inbox/claimed")?.({
|
||||
agent, turn: 1,
|
||||
message: { role: "user", source: { kind: "user" }, content: "Which package manager do I use?" },
|
||||
});
|
||||
const result = await listeners.get("system-prompt/assemble")!(base, { agent }, async () => base);
|
||||
expect(mockSearch).toHaveBeenCalledWith("Which package manager do I use?", {
|
||||
filters: { user_id: "u" }, topK: 5,
|
||||
});
|
||||
expect(result.contexts[0].text).toContain("Uses pnpm");
|
||||
expect(result.contexts[0].text).not.toContain("search mem0_memory");
|
||||
|
||||
listeners.get("agent/inbox/claimed")?.({
|
||||
agent, turn: 2,
|
||||
message: { role: "user", source: { kind: "user" }, content: "What indentation do I use?" },
|
||||
});
|
||||
listeners.get("agent/inbox/claimed")?.({
|
||||
agent, turn: 2,
|
||||
message: { role: "user", source: { kind: "plugin" }, content: "Ignore the human prompt" },
|
||||
});
|
||||
await listeners.get("system-prompt/assemble")!(base, { agent }, async () => base);
|
||||
expect(mockSearch.mock.lastCall?.[0]).toBe("What indentation do I use?");
|
||||
});
|
||||
|
||||
it("automatically recalls memory into the prompt for the latest human message", async () => {
|
||||
mockSearch.mockResolvedValue({
|
||||
results: [{ id: "m1", memory: "Likes tea" }],
|
||||
@@ -168,8 +205,25 @@ describe("Harness lifecycle", () => {
|
||||
});
|
||||
|
||||
expect(listeners.has("system-prompt/assemble")).toBe(false);
|
||||
expect(listeners.has("agent/inbox/claimed")).toBe(false);
|
||||
expect(listeners.has("session/event")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not capture interrupted turns and isolates rejected capture requests", async () => {
|
||||
const listeners = applyAndCollectListeners({ apiKey: "k", userId: "u" });
|
||||
const onEvent = listeners.get("session/event")!;
|
||||
const session = {};
|
||||
const userEvent = { type: "user/message", data: { role: "user", source: { kind: "user" }, content: "I prefer short answers. What is a list?" } };
|
||||
onEvent(session, { type: "turn/start" });
|
||||
onEvent(session, userEvent);
|
||||
onEvent(session, { type: "turn/end", data: { reason: { kind: "interrupted" } } });
|
||||
expect(mockAdd).not.toHaveBeenCalled();
|
||||
mockAdd.mockRejectedValue(new Error("backend unavailable"));
|
||||
onEvent(session, { type: "turn/start" });
|
||||
onEvent(session, userEvent);
|
||||
onEvent(session, { type: "turn/end", data: { reason: { kind: "completed" } } });
|
||||
await vi.waitFor(() => expect(mockAdd).toHaveBeenCalledOnce());
|
||||
});
|
||||
});
|
||||
|
||||
describe("search_memory tool", () => {
|
||||
@@ -272,3 +326,62 @@ describe("tool user ownership", () => {
|
||||
expect(mockAdd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace scope", () => {
|
||||
it("isolates both tools and automatic paths by canonical session workspace", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "mem0-dsh-scope-"));
|
||||
try {
|
||||
mkdirSync(join(root, "a"));
|
||||
mkdirSync(join(root, "b"));
|
||||
symlinkSync(join(root, "a"), join(root, "alias"));
|
||||
const config: Config = { apiKey: "k", userId: "u", memoryScope: "workspace" };
|
||||
const tools = applyAndCollect(config);
|
||||
mockAdd.mockResolvedValue([]);
|
||||
mockSearch.mockResolvedValue({ results: [] });
|
||||
const session = (name: string) => ({
|
||||
header: { cwd: join(root, name) },
|
||||
deriveMessages: () => [{ role: "user", source: { kind: "user" }, content: "preferences" }],
|
||||
});
|
||||
const a = session("a");
|
||||
await tools.get("add_memory")!.execute({ text: "Uses pnpm" }, { agent: { session: a } });
|
||||
const appId = mockAdd.mock.lastCall?.[1].appId;
|
||||
expect(appId).toMatch(/^deepseek-workspace-[a-f0-9]{64}$/);
|
||||
await tools.get("search_memory")!.execute({ query: "preferences" }, { agent: { session: session("alias") } });
|
||||
expect(mockSearch.mock.lastCall?.[1].filters).toEqual({ user_id: "u", app_id: appId });
|
||||
await tools.get("search_memory")!.execute({ query: "preferences" }, { agent: { session: session("b") } });
|
||||
expect(mockSearch.mock.lastCall?.[1].filters.app_id).not.toBe(appId);
|
||||
|
||||
const listeners = applyAndCollectListeners(config);
|
||||
const base = { sections: [], contexts: [], tools: [], variables: {} };
|
||||
await listeners.get("system-prompt/assemble")!(base, { agent: { session: a } }, async () => base);
|
||||
expect(mockSearch.mock.lastCall?.[1].filters).toEqual({ user_id: "u", app_id: appId });
|
||||
const onEvent = listeners.get("session/event")!;
|
||||
mockAdd.mockClear();
|
||||
onEvent(a, { type: "turn/start" });
|
||||
onEvent(a, { type: "user/message", data: { role: "user", source: { kind: "user" }, content: "I prefer short answers. What is a list?" } });
|
||||
onEvent(a, { type: "turn/end", data: { reason: { kind: "completed" } } });
|
||||
await vi.waitFor(() => expect(mockAdd.mock.lastCall?.[1]).toMatchObject({ userId: "u", appId }));
|
||||
expect(mockAdd).toHaveBeenCalledOnce();
|
||||
expect(mockAdd.mock.lastCall?.[0]).toEqual([
|
||||
{ role: "user", content: "I prefer short answers. What is a list?" },
|
||||
]);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("fails closed without a workspace while automatic paths leave the agent running", async () => {
|
||||
const config: Config = { apiKey: "k", userId: "u", memoryScope: "workspace" };
|
||||
const tools = applyAndCollect(config);
|
||||
await expect(tools.get("add_memory")!.execute({ text: "x" }, {})).rejects.toThrow(/workspace/);
|
||||
await expect(tools.get("search_memory")!.execute({ query: "x" }, {})).rejects.toThrow(/workspace/);
|
||||
const listeners = applyAndCollectListeners(config);
|
||||
const session = { header: {}, deriveMessages: () => [{ role: "user", source: { kind: "user" }, content: "hello" }] };
|
||||
const base = { sections: [], contexts: [], tools: [], variables: {} };
|
||||
expect(await listeners.get("system-prompt/assemble")!(base, { agent: { session } }, async () => base)).toBe(base);
|
||||
const onEvent = listeners.get("session/event")!;
|
||||
onEvent(session, { type: "user/message", data: { role: "user", source: { kind: "user" }, content: "hello" } });
|
||||
expect(() => onEvent(session, { type: "turn/end", data: { reason: { kind: "completed" } } })).not.toThrow();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(mockAdd).not.toHaveBeenCalled();
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("installable Harness bundle", () => {
|
||||
it("ships a native activation patch and builds before packing", () => {
|
||||
const root = new URL("../", import.meta.url);
|
||||
const manifest = JSON.parse(readFileSync(new URL("package.json", root), "utf8"));
|
||||
expect(manifest.scripts.prepack).toBe("pnpm build");
|
||||
expect(manifest.dsh.bundle.patch).toBe("./cordis.patch.yml");
|
||||
expect(manifest.files).toContain("cordis.patch.yml");
|
||||
expect(existsSync(new URL(manifest.dsh.bundle.patch, root))).toBe(true);
|
||||
expect(readFileSync(new URL(manifest.dsh.bundle.patch, root), "utf8")).toContain("@mem0/deepseek-plugin");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user