基于 SodaMem 守护进程的双时间轴记忆:每轮组装提示词时注入检索结果,回合结束后自动写回,读路径不经工具调用也不额外调用模型;每条召回的事实都标注来源回合并带有效期,失效的事实不再被检索到。
Bi-temporal memory backed by a SodaMem daemon: recall is contributed to the prompt while each turn is assembled and every closed turn is ingested, with no tool call and no model call on the read path; each recalled fact cites the turn it came from and carries validity dates, so superseded facts stop being retrieved.
安装
dsh plugin --profile web add github:SodaMem/dsh-plugin-sodamemGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
安装与环境配置指引、插件开发教程见 DSH 中文社区文档 ↗
安装即在你的机器上以你的权限运行第三方代码——它可读写文件、使用凭据、访问网络,DSH 的工具审批不会为插件代码加沙箱。「检测到 manifest」仅代表发现 dsh.bundle / dsh.plugin 清单,不构成兼容性或安全审查;安装前请审阅源码,不熟悉的插件先在不含密钥的环境试用。
README
目录
Long-term memory for DeepSeek Harness, backed by SodaMem.
- Recall — while a turn is being assembled, the plugin retrieves an evidence block for that turn's question and contributes it to the prompt. BM25 + vector + entity fusion, no model call.
- Retain — when the turn closes, its messages are ingested back into the store.
Neither is a tool call, so the model can't skip either one and neither costs tool-schema space.
SodaMem is a separate open-source memory engine that runs as a local daemon. This repo is only the dsh plugin; you need the daemon running for it to do anything.
What you get that a notes file doesn't
Every recalled fact names the turn it came from. This is one line of a real evidence block, verbatim:
support=I flew United to Boston last week.
predicate=User flew United to Boston
entities=airline=United|destination=Boston
source=s4/s4_turn_0 ← the actual turn, not "some earlier chat"
date=2023-06-10
FactEvent → SourceSpan → RawTurn is a foreign-key chain. When the model asserts something about the user, there is a row explaining why.
Facts expire. SodaMem keeps four time axes — when the event happened, when the fact was true, when it was said, when it was stored. "I moved to Chicago last year" and "I'm moving next year" are different rows, and a fact that stopped being true stops coming back. Corrections are ADD-only: a new version plus a SUPERSEDES edge, never an in-place rewrite.
The engine is benchmarked and the answers are published.
| benchmark | score | |
|---|---|---|
| LongMemEval-S | 92.8% (464/500) | 500 answers + 8,427 evidence rows, re-gradable with any judge |
| LoCoMo | 86.88% (1338/1540) | end-to-end QA, LLM-as-judge |
Those are SodaMem's numbers — the engine this plugin talks to, not the plugin itself.
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 harness's own seams instead — system-prompt/assemble for recall and agent/turn-stopping for retain — so both 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
dsh plugin --profile tui add dsh-plugin-sodamem
That is all that is needed. The package ships a dsh.bundle manifest pointing
at cordis.patch.yml, so dsh adds the plugin to the
profile's bundle list and composes its row — with working defaults — into the
profile tree. Confirm it landed:
dsh --profile tui --dump-config | grep -A6 'id: 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
The bundled cordis.patch.yml ships defaults that boot (apiUrl
http://127.0.0.1:8000, userId default). To change them, override the row
in your own profile cordis.patch.yml, which applies after every bundle
layer:
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- id: sodamem
config:
apiUrl: 'http://127.0.0.1:8000'
apiKey: 'dev'
userId: 'your-user-id'
tokenBudget: 1200
A patch replaces the targeted row's whole config rather than merging into
it, so restate every key you want to keep.
Without installing as a bundle, the same row can be inserted ad hoc:
npx @deepseek-ai/dsh web --patch ./sodamem-plugin.patch.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 question, not once per prompt assembly — a tool loop that takes six steps still issues one GET /v1/context. Steering mid-turn is a new question, so it earns its own recall.
Retain ingests only what a human or the model actually said. Tool results and the harness's runtime-context snapshot are excluded — the snapshot is where this plugin's own recalled evidence lives, and ingesting it would feed the store its own output back on every turn.
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.
On load the plugin also issues a cheap warm-up request, so the daemon's lazy store open — measured at ~630 ms against a ~130 ms steady state — is paid before your first question instead of by it. Nothing waits on that warm-up, and it is harmless when no daemon is running yet.
Performance
Measured on a real 1000-fact store (auth on, single-worker daemon, loopback, one machine, chromadb 1.1.1, chroma schema verified at 9 sysdb migrations). Full method, caveats, and reproduction steps: NOTES-latency.md (shipped in the package too).
Retraction. An earlier version of this section claimed cold start returns HTTP 500 in 10 runs out of 10 from a Chroma panic, and that warm steady state was p50 17 ms. Both are withdrawn. They were measured against a store whose chroma schema had been migrated by a different chromadb version than the one reading it — a defect of the test machine, not of the daemon or the plugin. The 17 ms figure was that broken store answering with vector search switched off, so it understated real warm latency by roughly 7x. Full account, including the old numbers:
NOTES-latency.md, and a summary inCHANGELOG.md.
- Cold start costs extra, but it succeeds. The daemon opens a user's store lazily. Across 3 daemon restarts the first request returned HTTP 200 every time, with full vector routes and no degraded retrieval (0 degraded / 17 citations, 3/3). It costs about 630 ms against a ~130 ms steady state; the second request is already warm. The plugin absorbs that one-time cost with a fire-and-forget warm-up at load (
src/warmup.ts), so it lands before the user's first question rather than on it. - Warm, steady state: p50 130 ms (min 101, p95 164, p99 186, max 296 — 200 sequential requests, six store-relevant queries,
token_budget1200). That is what auto-injection adds to time-to-first-token once the store is open. It is the zero-LLM path, so it does not grow with model spend. - Multi-client is the caveat, and it is smaller than previously stated — but do not read it as comfortable. The daemon runs one worker by design, and
/v1/contextlatency grows near-linearly with concurrent clients: median 128 / 211 / 361 / 672 ms at concurrency 1 / 2 / 4 / 8. At concurrency 8 the worst of 40 requests was 732 ms on an otherwise idle machine, and 986–1268 ms across five runs on a loaded one (load avg 14, where sequential p50 was 197 ms rather than 130 ms). So the margin against the 1500 ms recall deadline is somewhere between ~1.2x and ~2x depending on what else the box is doing — real headroom, not an order of magnitude, and the loaded machine's worst run came within 16% of the deadline. The withdrawn figures put it within 10% of the deadline on the broken store; the correction is smaller than that retraction alone would suggest.
Read the concurrency numbers as a shape, not as absolute milliseconds. The probe runs five rounds per level, which does not warm the BM25 index cache the way 200 sequential requests do, and its right-hand column is the worst of n rather than a p99. What reproduces across machines is the near-linear queueing; what moves is the millisecond.
So: the shape is unchanged — enough concurrent clients on one single-worker daemon and recall will start silently dropping. Nothing measured here reaches that point at 8 clients, on either machine. That is a property of the daemon's read path, not of this plugin; auto-injection is what makes it reachable, by turning an occasional tool call into a per-turn one. The numbers behind this, and the caveats on each, 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 — real
Cordis Context, real session store, real system-prompt registry, real agent
loop — and drives real turns against a running SodaMem daemon. It stubs only the
LLM adapter. See test-integration/README.md for
how to start the daemon.
The unit tests cannot prove the plugin works inside the loop: they mock the Cordis registration boundary, so they cannot see ordering. Treat the integration suite as the gate.
License
原始 README: https://github.com/SodaMem/dsh-plugin-sodamem/blob/main/README.md ↗
同类插件
查看全部 →
mnemon
LLM监督持久内存插件 — 基于图的召回,跨会话知识,单二进制。与DeepSeek Harness、Claude Code、OpenClaw及任何代理运行时兼容。

memtrace-public
面向 AI 编码 agent 的结构化记忆:双时态图谱、MCP 原生、零 LLM 调用;支持 Cursor · Claude Code · Codex · DeepSeek Harness · Hermes · VS Code · Windsurf

dsh-flowix-memory
将本地 flowix-cli 注册为 MCP 服务,让 agent 可以搜索、读取、创建和编辑 Flowix 备忘与思维导图产物。

flowix
笔记助你,记忆助你的代理。

engramory
AI代理的便携式内存协议 — 以静态规则加载;整理学科 + 参考规范 + 可选的钩子API

dsh-memory-evolve
为 DeepSeek Harness 提供纯插件实现的跨会话长期记忆与后台自我进化能力:五轨记忆、Git 分支感知、回合内自我审查、技能自我进化与技能管理器、四轨待办、COI 调度、会话广播、会话搜索、提示词管理器和临时信息便签;零核心修改、零运行时依赖,安装即用,卸载即净。