SodaMem/dsh-plugin-sodamem ↗★ 0
dsh-plugin-sodamem
SodaMem memory layer for the DeepSeek Harness (Cordis): unconditional per-turn recall and per-turn-close retain.
AI Analysis
核心用途是为 Agent 提供长期记忆存储。适合需要跨会话保留事实、避免 AI 遗忘关键背景信息的用户。需本地运行 SodaMem 守护进程。
Install
This plugin has no verified bundle, or compatibility checks failed. Read the repository notes first. Read the full README ↗
README
Read the full README ↗dsh-plugin-sodamem
A standalone plugin for the DeepSeek Harness (dsh) that wires in SodaMem as a persistent memory layer.
New here? SodaMem is an open-source long-term memory store for LLM agents: you ingest conversation turns into it and it extracts durable facts, then serves a prompt-ready evidence block back on demand. It runs as a local daemon. This repo is only the dsh plugin — the memory engine itself lives at github.com/SodaMem/SodaMem, and you need it running for this plugin to do anything.
The plugin installs SodaMem as a memory layer, not as a tool in the model's tool bag.
- Recall — before every turn, the plugin fetches a prompt-ready evidence block from SodaMem and contributes it to the system prompt.
- Retain — when every turn closes, the plugin ingests that turn's messages back into SodaMem.
Neither one is a tool call, so neither one depends on the model deciding to use it.
⚠️ Not ready for use: recall lands one turn late
Verified against a real
dshruntime and a real SodaMem daemon (npm run test:integration): recall does not reach the model on the turn that asked.AgentLoop.preStep()assembles the system prompt and projects the runtime-context snapshot before it dispatches theagent/pre-stepwaterfall this plugin recalls in, so turn N's evidence arrives in turn N+1's request — answering the previous question.Retain, the failure-degradation path, and the call deadlines all hold. Recall does not. See
test-integration/README.mdfor the evidence and the likely fix (move recall onto the asynchronoussystem-prompt/assemblewaterfall).
Why not the MCP bridge?
SodaMem already has an MCP integration for dsh, in the main SodaMem repo (integrations/deepseek-harness/). It exposes memory as tools, which means the model has to choose to call them — and on most turns it simply doesn't. The strongest thing SodaMem offers, the zero-LLM GET /v1/context evidence block, ends up left to the model's discretion.
MCP cannot fix that. A tool is pull-only, and nothing in the protocol lets a server contribute to the prompt or observe a turn closing. This plugin uses the two seams the harness itself exposes — agent/pre-step and agent/turn-stopping — so recall and retain happen unconditionally.
| MCP bridge | This plugin | |
|---|---|---|
| Recall | model calls a tool, if it decides to | every turn, automatically |
| Retain | model calls a tool, if it decides to | every closed turn, automatically |
| Model can skip it | yes | no |
| Costs tool-schema space | yes | no |
Do not run both against the same store. They would recall the same facts twice and ingest every turn twice. Pick one.
Requirements
- Node >= 22 (the harness requires it; the plugin uses
AbortSignal.any) - A running SodaMem daemon — see below
Install
npm install dsh-plugin-sodamem
Start the daemon first (once per machine):
sodamem daemon ensure # defaults to http://127.0.0.1:8000
Fact extraction needs LLM credentials on the daemon side. Put SODAMEM_LLM_PROVIDER / SODAMEM_LLM_API_KEY / SODAMEM_LLM_MODEL in the daemon's environment or .env. Without them recall still works, but every retain will be accepted and then fail during extraction.
Configure
Load it with a dsh patch file, the same mechanism the MCP guide uses:
# sodamem-plugin.patch.yml
- insert:
- id: sodamem
name: dsh-plugin-sodamem
config:
apiUrl: 'http://127.0.0.1:8000'
apiKey: 'dev'
userId: 'your-user-id'
tokenBudget: 1200
npx @deepseek-ai/dsh web --patch ./sodamem-plugin.patch.yml
Or add the same name / config entry to your cordis.yml.
Config fields
There are four, and they are all connection or scope facts.
| field | required | default | what it is |
|---|---|---|---|
apiUrl | yes | — | Origin of the SodaMem daemon |
apiKey | yes | — | Sent on every request. Any non-empty string works when the daemon runs with auth disabled — there is no magic fallback |
userId | yes | — | The SodaMem user_id every read and write is scoped to |
tokenBudget | no | 1200 | Token budget for the recalled evidence block |
There is deliberately no switch that turns recall or retain on or off, and no strategy selector. Auto-injection is the entire point of the plugin; a knob to disable it would just be a slower way to use the MCP bridge.
session_id on retain is the agent's id (in dsh, an agent and its session share one identity). agent_id is deliberately not sent — it would be the session id, which would narrow retrieval and fragment recall across sessions.
Remote mode only
The plugin talks HTTP to a daemon. It has no data-root option and imports nothing that can open a store locally, and that is a deliberate constraint rather than an unfinished feature.
Two processes writing one SODAMEM_DATA_ROOT corrupt it — per-user SQLite without cross-process WAL is not safe under concurrent writers, which is why the daemon is pinned to a single worker (SodaMem mcp_server/README.md and ADR 0001 §2). A plugin loaded inside an arbitrary harness process is the worst possible candidate for being that second writer — you would not know how many of them are running. So there is exactly one writer, the daemon, and everyone else is a client.
When SodaMem is down or slow
A SodaMem problem is never a dsh problem. Every call is wrapped so that no error, rejection, timeout, or abort escapes into the turn.
| Recall deadline | 1500 ms |
| Retain deadline | 5000 ms |
| Daemon unreachable, erroring, slow, or returning junk | recall contributes nothing; the turn proceeds normally |
| Turn cancelled | in-flight SodaMem requests are aborted with it |
The deadlines cover the whole call, headers and response body alike, so a daemon that answers 200 and then stalls mid-body cannot hang a turn.
Recall fires once per turn, not once per step — a tool loop that takes six steps still issues one GET /v1/context.
The one thing to know: when recall misses its deadline, the turn proceeds without memory and nothing surfaces to the user. The plugin logs a warning (ctx.logger.warn) on every degraded turn, and that log is the only signal you get. See the performance note below.
Performance
Measured on a real 1000-fact store (auth on, single-worker daemon, loopback, one machine). Full method, caveats, and reproduction steps: NOTES-latency.md.
- Single client, 200 sequential requests: p50 183 ms, p99 471 ms. That is what auto-injection adds to time-to-first-token. It is the zero-LLM path, so it does not grow with model spend.
- Multi-client is the caveat. The daemon runs one worker by design, and
/v1/contextlatency grows near-linearly with concurrent clients. At 8 concurrent clients the slowest sampled request was already within 10% of the 1500 ms recall deadline.
So: if a dsh turn, a Cursor hook, and a Claude Code hook all hit the same daemon, expect recall to start silently dropping. That is a property of the daemon's read path, not of this plugin — but auto-injection is what makes it reachable, by turning an occasional tool call into a per-turn one. The numbers behind this, including why the concurrency figures should be read as a shape rather than as precise milliseconds, are in NOTES-latency.md.
Development
npm install
npm run typecheck
npm test # no live daemon required; HTTP is mocked at the fetch boundary
npm run build # dual ESM/CJS into dist/
npm run test:integration # real dsh runtime + real daemon; not run by CI
npm run test:integration loads the plugin into a real dsh runtime and talks
to a running SodaMem daemon. It stubs only the LLM adapter. See
test-integration/README.md for how to start the
daemon and for the recall defect it currently exposes.