dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Markdown 笔记变成 Agent 的长期记忆:本地 embedding + SQLite FTS5 混合召回,可选密级过滤(dsh 插件)· Long-term memory for dsh agents: hybrid search (local embeddings + SQLite FTS5) over your markdown notes.
简体中文 | English
A long-term memory search plugin for DeepSeek Harness (dsh).
It turns a pile of markdown notes into memory an agent can actually recall:
two recall paths (local vectors + keywords), relevant old notes pulled into the
context every turn, and a memory_search tool the agent can dig through itself.
A long-running agent's problem is usually not that the model is not smart enough, but that it does not remember. This plugin does not train and does not fine-tune. It does one thing: turns the notes you wrote yourself into memory you can look up at any time.
More mature options in the same space: csyangwen/dsh-memory-evolve, Aik358/dsh-auto-memory and FuRongJun-1999/dsh-memory are all more mature than this one; look at them first if you are choosing. Why we keep this one: it is the memory-search part extracted from our own production setup - the two recall paths (local vectors + keywords) and the access-level filter are the ones that actually ran, its self-tests bring their own corpus so you do not have to wire up your data first, and a deleted index is rebuilt from the
.mdfiles. Its feature set is narrower than theirs, so compare before you pick.
Memory lives in .md files on disk; the database is only an index:
node-llama-cpp, CPU is
enough).modelPath empty and only the keyword path
runs, with not one extra dependency.memory_search tool (the agent asks on purpose) + automatic
per-turn recall (injected into the system prompt, never written into the
conversation log).dsh plugin --profile web add github:JackZo400/dsh-memory-search
After installing, go through the "Configuration" section below, then restart the matching profile.
Requirements: Node ≥ 22.5 (it uses the built-in node:sqlite; verified
working on 22.22, which prints one ExperimentalWarning — that is normal). Vector
search additionally needs node-llama-cpp plus a GGUF model you download yourself
— it is listed under optionalDependencies, and failing to install it does not
affect keyword search.
Configuration goes into your profile patch (the insert section of the bundle
patch):
- insert:
- id: memory-search
name: dsh-memory-search
config:
dbPath: .dsh-memory/index.db # 索引库落哪;相对路径按 dsh 的工作目录算
roots: # 语料根目录:递归收集里面的 .md
- ./memory
modelPath: '' # GGUF 嵌入模型;留空 = 只跑关键词
recall:
enabled: true # 每轮自动召回
limit: 4 # 最多带几条
budgetChars: 1600 # 注入的字符预算
order: 130 # 在系统提示里的排序位
embedding:
enabled: true
threads: 6 # CPU 线程数
contextSize: 1024 # 上下文窗口(开大更吃内存)
batchSize: 256
queryInstruction: '' # 见下
search:
candidatePool: 40 # 每路召回各取多少候选
rrfK: 10 # RRF 平滑常数
exclude: ['**/.index/**', '**/node_modules/**']
syncIntervalMs: 300000 # 多久扫一次新写的笔记
acceptSpecChange: false # 换切块规则才设 true(会重算全部向量)
A few things that are easy to trip over:
roots means an empty index, and the log will remind you.modelPath empty the plugin builds a keyword index only; turning
embedding.enabled on does nothing (and raises no error).queryInstruction: the default is the instruction prefix for
Qwen3-Embedding (it is instruction-aware, and retrieval quality depends on that
prefix). If you switch to another embedding model (bge-m3, nomic and the
like), clear it explicitly in the config, or the prefix will pollute the
semantics.There is no fixed structure; .md files are collected recursively. Two
suggestions:
public.md / personal.md / secret.md a 1.5x source weight and
down-weights the carryover/ directory, but no one knows your corpus structure
better than you do.The criterion is the file name (the directory it sits in does not matter):
| File name | Level | Who can recall it |
|---|---|---|
public.md | public | all sessions |
personal.md | personal | owner sessions only |
secret.md | secret | owner sessions only |
| any other file | personal (conservative) | owner sessions only |
Who counts as the owner is decided by an optional channel map — a JSON that maps a session key to a session id:
{ "c2c:1A2B3C": { "sessionId": "8f0c…" }, "group:123456": { "sessionId": "ab12…" } }
Once sessionMap is configured: keys starting with group:/grp: are judged
group (public only), keys listed in ownerIds are judged owner (everything),
and the rest are judged guest (public only). When the channel cannot be
determined, nothing is recalled (fail closed) — better to hand out too little
than to leak something private.
Without sessionMap no filtering happens at all and every session counts as
the owner — for single-user setups (no group chats) that is the default you want.
markdown 语料 ──scan──▶ 切块(按条目边界)──▶ SQLite
├── chunks(正文 + 向量 BLOB)
└── chunks_fts(bigram token)
查询 ──┬── FTS5 MATCH ──▶ bm25 排名 ─┐
└── embed ──────▶ 余弦排名 ──┴── RRF 融合 ──▶ 按来源加权 ──▶ 密级过滤 ──▶ top-k
CHUNK_SIG), by default
it only records the fact and does not re-chunk — re-chunking requires
acceptSpecChange: true (and that recomputes every vector).memory_search tool returns raw passages + their source (not
a summary); automatic recall hooks into system-prompt/assemble and adds a
memory-search block to the system prompt, without writing it into the
conversation log, so it does not pollute the context history.node test/selftest.mjs # 切块 / 分词 / 索引 / 检索(临时语料,无外部依赖)
node test/plugin-selftest.mjs # 插件契约:工具注册、系统提示段、服务暴露、密级
node test/spec-guard.mjs # 切块规范闸:改了规则不许偷偷重切
npm test # 上面三个一起
test/recall-eval.mjs is an evaluation harness, not a self-test — it runs a
batch of "question → passage/file expected to hit" cases against your own index
database and reports top-1 / top-3 / top-5 hit rates:
node test/recall-eval.mjs --db .dsh-memory/index.db --corpus ./memory
The cases have to be rewritten for your own corpus (there are examples at the top of the file). This is the part of the plugin most worth touching — recall quality is tightly coupled to corpus structure, and the default weights I ship were only validated on one kind of corpus.
src/recall.js.queryInstruction and the dimensions.MIT © 2026 JackZo400