GitHub - John-Rood/dsh-memoryrouter: Persistent cross-session memory for dsh (DeepSeek Harness) agents, powered by MemoryRouter · GitHub
Skip to content

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

dsh-memoryrouter

npm version license: MIT tests

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.

Quickstart (30 seconds)

dsh plugin --profile <your-profile> add dsh-memoryrouter

Then 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_KEY

Get 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.

How it works

                       ┌──────────────────────┐
  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

Configuration

Option Default Description
apiKey '' MemoryRouter memory key (mk_…). Prefer the env var for shared configs.
apiKeyEnv MEMORYROUTER_API_KEY Env var read when apiKey is empty.
baseUrl https://api.memoryrouter.ai API base URL.
enabled true Master switch.
store true Store completed exchanges.
recall true Inject relevant memories at turn start.
density default Retrieval density: low / default / high / xhigh.
recallLimit 0 Explicit chunk-count override (0 = server default).
recallMaxChars 24000 Hard bound on the injected recall block.
storeMaxChars 8000 Per-side bound on stored text.
namespace '' Prefix for session-vault ids — share one key across several dsh installs without mixing session vaults (used when vault: session).
vault core core stores/recalls in the key's core vault (memories shared across sessions — the point of persistent memory). session isolates each dsh session in its own vault.
includeSubagents false Also store/recall subagent sessions.
storeTimeoutMs 10000 Timeout per store attempt.
recallTimeoutMs 2500 Max time recall may delay a turn.
debug false Verbose logging.

Architecture: why the core layer, and how noise is filtered

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:

Store: the durable session event stream (session/event)

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:

  1. 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.
  2. Source-kind filtering: dsh tags every logged user-role message with its producer. Real human input is source: { kind: 'user' }; injected context (workspace AGENTS.md instructions, skill catalogs, runtime snapshots, plugin notices, tool results) carries other kinds and is dropped at the root.
  3. Block filtering: only text blocks 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.
  4. Envelope stripping & command skipping: <system-reminder>…</system-reminder> wrappers are stripped (keeping any real user text around them), and messages that are purely /commands are skipped entirely.
  5. Completion gating: only turns ending completed are stored. Aborted, errored, and interrupted turns produce nothing.

The stored memory is exactly: what the user said + what the assistant answered. Nothing else.

Recall: the pre-step waterfall (agent/pre-step)

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.

MemoryRouter endpoints used

  • POST /v1/memory/ingest — stores an exchange; returns 202 and 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).

Development

npm install
npm run build      # tsc → lib/
npm run typecheck
npm test           # node --test (56 tests, all offline — mock HTTP server)

License

MIT © John Rood · memoryrouter.ai

About

Persistent cross-session memory for dsh (DeepSeek Harness) agents, powered by MemoryRouter

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages