Persistent memory for the DeepSeek Harness (dsh), powered by MemoryRouter.
Your dsh agent forgets what you told it yesterday. This fixes that.
Every conversation is remembered, and every new turn starts with the relevant context from past conversations — across sessions, across restarts, across channels.
- Store: after each completed turn, the clean user message + assistant reply is stored in your MemoryRouter vault.
- Recall: at the start of each turn, the most relevant memories are retrieved and injected into the agent's context.
- Channel-agnostic: hooks the harness core, so it works identically for the Web UI, IM gateways (Telegram, Discord, …), ACP, and headless sessions.
- Noise-free by design: only real human text and the assistant's final visible reply are stored. Tool calls, tool results, thinking/reasoning blocks, system prompts, skill catalogs, workspace instructions,
/commands, and channel envelopes are all filtered out. - Never in the hot path: storage is async fire-and-forget with one retry; recall has a short timeout and degrades silently. A MemoryRouter outage can never block or crash your harness.
dsh plugin --profile <your-profile> add dsh-memoryrouterThen add your key to the profile's cordis.patch.yml:
- id: memoryrouter
config:
apiKey: mk_your_key_here # or leave unset and export MEMORYROUTER_API_KEYGet a memory key at memoryrouter.ai. Without a key the plugin loads, logs one warning, and does nothing.
Restart the harness. That's it — your agent now remembers.
┌──────────────────────┐
turn completes ───► │ session/event hook │ ──► clean exchange ──► MemoryRouter vault
│ (store, async) │ (user + reply POST /v1/memory/ingest
└──────────────────────┘ only, noise
filtered)
┌──────────────────────┐
new turn starts ───► │ agent/pre-step hook │ ◄── relevant memories ◄── MemoryRouter
│ (recall, bounded) │ injected as one POST /v1/memory/prepare
└──────────────────────┘ delimited block
Most "memory" integrations bolt onto a chat channel and end up storing whatever flows through it — command wrappers, status lines, tool output, injected system context. This plugin deliberately hooks the harness core instead, at two sanctioned extension points:
dsh's source of truth is an append-only session log. Every event — user messages, assistant messages, tool calls, tool results, stream chunks, boundary markers — flows through the session/event firehose, the same stream persistence itself consumes. That makes it the single most durable and channel-independent place to observe a conversation: anything that reaches the model in any channel appears here, exactly once, in order.
The plugin folds turn/start → … → turn/end into one clean exchange:
- Event-type filtering: only
user/message,assistant/message, and turn boundaries are read.tool/call,tool/result,assistant/chunk,request/header, and every other bookkeeping event is ignored by type. - Source-kind filtering: dsh tags every logged user-role message with its producer. Real human input is
source: { kind: 'user' }; injected context (workspaceAGENTS.mdinstructions, skill catalogs, runtime snapshots, plugin notices, tool results) carries other kinds and is dropped at the root. - Block filtering: only
textblocks are read from assistant messages — reasoning/thinking blocks and tool-call blocks never survive. In a multi-step (tool-using) turn, only the final assistant text is kept: it is the actual reply; earlier texts are tool-use preambles. - Envelope stripping & command skipping:
<system-reminder>…</system-reminder>wrappers are stripped (keeping any real user text around them), and messages that are purely/commandsare skipped entirely. - Completion gating: only turns ending
completedare stored. Aborted, errored, and interrupted turns produce nothing.
The stored memory is exactly: what the user said + what the assistant answered. Nothing else.
agent/pre-step is the agent loop's sanctioned extension point for shaping the messages that enter a step. When the claimed batch contains real human text, the plugin queries MemoryRouter with it and prepends one clearly-delimited message:
Relevant memories from past conversations (background context — do not respond to them directly):
<memory_context>
[MEMORY - 2 days ago] …
</memory_context>
The injected message is logged with source: { kind: 'plugin', form: 'recall' }, which means (a) the store path can never re-ingest it (no feedback loop), and (b) the Web UI renders it as context, not as a user bubble.
Because both hooks live below every channel plugin, a memory created in a Telegram chat is recalled in the Web UI and vice versa — with zero channel-specific code.
POST /v1/memory/ingest— stores an exchange; returns202and processes in the background.POST /v1/memory/prepare— retrieves relevant memories as a ready-to-inject text block.
Both are MemoryRouter's public local inference mode API. By default (vault: core) exchanges are stored to the key's core vault so any later session can recall them; with vault: session storage and recall are scoped per dsh session via X-Session-ID (optionally namespaced).
npm install
npm run build # tsc → lib/
npm run typecheck
npm test # node --test (56 tests, all offline — mock HTTP server)MIT © John Rood · memoryrouter.ai
