dsh-pi-memory is a DeepSeek Harness (dsh) port of pi-memory, the most popular memory extension in the Pi ecosystem. Your coding agent forgets everything between sessions — this plugin gives it a memory: durable facts and decisions, a running daily log, and a scratchpad of things to come back to — all as plain Markdown files you can read, edit, and commit. With optional qmd it also gets keyword, semantic, and hybrid search across everything it has ever remembered.
Porting notes: the code and logic are 100% from upstream pi-memory (jayzeng/pi-memory, MIT). This port only changes the dsh plugin entry and lifecycle wiring; every change is listed with a reason in the delivery report of porting ticket #18. Please also star upstream pi-memory.
Known limitation: the prompts and memory-context templates injected into the agent are upstream English, preserved verbatim (not translated); the language of your memory content is up to you.
Features
Tool
Description
memory_write
Write to MEMORY.md (long-term) or daily log
memory_forget
Delete matching entries and create a durable recovery record
memory_restore
Restore a deletion using the recovery ID returned by memory_forget
memory_read
Read any memory file or list daily logs
scratchpad
Add/done/undo/clear/list checklist items
memory_search
Search across all memory files (requires qmd)
memory_status
Health check: where files live, qmd/collection/embeddings state, active config
The six core tools (memory_write, memory_forget, memory_restore, memory_read, scratchpad, memory_status) work immediately with no other setup. Search is opt-in below.
memory_search modes
Mode
Speed
Method
Best for
keyword
~30ms
BM25
Specific terms, dates, names, #tags, [[links]]
semantic
~2s
Vector search
Related concepts, different wording
deep
~10s
Hybrid + reranking
When other modes miss
What it feels like
# Session 1
you ▸ I always use pnpm in this repo, never npm. Remember that.
dsh ▸ Got it — saved to long-term memory. (writes MEMORY.md)
# …days later, brand new session…
you ▸ add prettier as a dev dependency
dsh ▸ pnpm add -D prettier
(recalled your package-manager preference from memory — no reminder needed)
Everything lives in $DSH_HOME/agent/memory/ (default ~/.dsh/agent/memory/) as Markdown, so you can also just cat it:
$ cat ~/.dsh/agent/memory/MEMORY.md
#preference [[package-manager]] Always use pnpm in this repo, never npm.
Installation
⚠️ dsh-pi-memory is not published to npm yet — install from a local checkout below. Once published, a bare-name install works (this README will be updated).
# 1. Clone this repo and install dependencies
git clone https://github.com/GongYuanCaiJi/dsh-pi-memory.git
cd dsh-pi-memory && npm install
# 2. Add it to a dsh profile (headless one-shot runs also need @deepseek-ai/dsh-headless@next)
P=verify-$(basename $PWD)-$$
dsh plugin --profile "$P" add @deepseek-ai/dsh-headless@next
dsh plugin --profile "$P" add .
# 3. Use it
dsh --profile "$P" "Remember: I prefer dark mode"
dsh --profile "$P" "What did I say I prefer?" # brand new session — it remembers
Optional: enable search with qmd
memory_search (and selective injection in per-turn mode) need qmd. Either install method works:
npm install -g @tobilu/qmd # no Bun required
bun install -g https://github.com/tobi/qmd # ensure ~/.bun/bin is on PATH
When qmd is present, the plugin automatically creates the pi-memory collection and path contexts on the next session start — no manual step. Run memory_status any time to confirm qmd, the collection, and embeddings are ready.
Semantic/deep modes need vector embeddings; the plugin keeps them current automatically (qmd embed runs in the background at session start and after writes). The very first embed downloads the embedding model, so semantic search may take a minute to come online on a fresh install. To set the collection up by hand:
Without qmd, the core tools still work fully — only memory_search and selective injection require it.
File layout
~/.dsh/agent/memory/
MEMORY.md # Curated long-term memory
SCRATCHPAD.md # Checklist of things to fix/remember
daily/
2026-02-15.md # Daily append-only log
2026-02-14.md
...
recovery/
.json # Complete payload and restore state for a memory_forget deletion
How it works
Context injection
Before every agent turn, the following are injected into the system prompt (in priority order):
Local prefix-caching runtimes (llama.cpp, vLLM, MLX) invalidate from the first divergent token onward. If the injected memory block changes turn-to-turn, every subsequent user / assistant / tool token gets reprocessed — effectively the entire conversation history each turn.
To keep the prefix byte-stable, the plugin snapshots the memory context at deliberate checkpoints and emits the same bytes for every turn in between. Snapshots refresh on:
session start — fresh snapshot per session
compaction — handoff is written then snapshot refreshes (one intentional cache boundary)
memory_write with target: long_term — marks the snapshot dirty so the next turn refreshes
Day rollover — snapshot's captured date no longer matches today
memory_write with target: daily and scratchpad writes do not mark dirty — they're high-frequency and the write content is already echoed via tool-call args. The model can always call memory_read / memory_search for the authoritative latest state.
Set PI_MEMORY_SNAPSHOT=per-turn to opt out and restore the old per-turn rebuild behavior, including automatic per-prompt qmd search injection.
Selective injection (opt-in via per-turn mode)
When PI_MEMORY_SNAPSHOT=per-turn is set and qmd is available, the plugin automatically searches memory using the user's prompt before each turn. The top 3 keyword results are injected alongside the standard context. The search has a 3-second timeout and fails silently. In the default stable mode, the model gets the same capability by calling memory_search on demand.
Tags and links
Use #tags and [[wiki-links]] in memory content to improve searchability:
#decision [[database-choice]] Chose PostgreSQL for all backend services.
#preference [[editor]] User prefers Neovim with LazyVim config.
#lesson [[api-versioning]] URL prefix versioning (/v1/) avoids CDN cache issues.
These are content conventions, not enforced metadata. qmd's full-text indexing makes them searchable for free.
Session handoff
When the context window compacts, the plugin automatically captures a handoff entry in today's daily log:
Persistence: Memory files are plain Markdown on disk — readable, editable, and git-friendly.
Recoverable deletion: memory_forget stores complete deleted entries under recovery/ before changing memory and returns a recovery ID that memory_restore can use. Recovery JSON is outside qmd's **/*.md index.
Tool response previews: Write/scratchpad tools return size-capped previews instead of full file contents.
qmd auto-setup: On first session start with qmd available, the plugin creates the collection and path contexts automatically.
qmd re-indexing: After every write, a debounced qmd update runs in the background (fire-and-forget, non-blocking) unless disabled via PI_MEMORY_QMD_UPDATE.
qmd embeddings: Vector embeddings for semantic/deep search are kept current automatically — qmd embed (incremental) runs in the background after each re-index and as a catch-up at session start. Disabled along with re-indexing via PI_MEMORY_QMD_UPDATE.
Graceful degradation: If qmd is not installed, core tools work fine. memory_search returns install instructions.
Configuration
Variable
Values
Default
Description
PI_MEMORY_DIR
path
$DSH_HOME/agent/memory
Override the memory storage directory (defaults under the dsh home, not Pi's ~/.pi)
PI_MEMORY_SNAPSHOT
stable, per-turn
stable
stable snapshots memory at checkpoints for KV cache stability; per-turn rebuilds every turn (legacy behavior)
PI_MEMORY_QMD_UPDATE
background, manual, off
background
Controls automatic qmd update + qmd embed after writes
PI_MEMORY_QMD_SEARCH_TIMEOUT_MS
positive integer (milliseconds)
60000
Sets the timeout for explicit memory_search qmd queries
PI_MEMORY_NO_SEARCH
1
unset
Disable selective injection in per-turn mode (no effect in stable mode)
PI_MEMORY_SUMMARIZE_TRANSITIONS
1, true, yes, on
unset
Also write exit summaries during lifecycle transitions (reload/new/resume/fork). By default these transitions skip summaries for speed.
PI_MEMORY_EXIT_SUMMARY
0, off, false, no to disable
unset (enabled)
Disable the exit summary on session end. Ending then does no LLM call and no qmd update, so it is instant; explicit memory_write during sessions is unaffected.
PI_MEMORY_EXIT_SUMMARY_MODEL
provider/model-id
unset (session model)
Model used to write the exit summary, e.g. a cheaper/faster one. Unresolvable specs fall back to the session model.
PI_MEMORY_EXIT_SUMMARY_TIMEOUT_MS
positive integer (milliseconds)
10000
Self-imposed timeout for exit-summary generation on session end. On expiry nothing is persisted.
Porting note: session_shutdown maps to dsh's agent/disposed — that event fires only when dsh disposes an agent while services are still live; in headless one-shot runs the services are torn down before plugin disposers run, so the exit summary is silently skipped (no crash, nothing persisted). This is a dsh-vs-Pi lifecycle difference, detailed in porting ticket #18.
Troubleshooting
Run the memory_status tool first — it reports most of these at a glance.
Symptom
Cause
Fix
memory_search says qmd is required
qmd not installed or not on PATH
Install qmd (npm install -g @tobilu/qmd); if installed via Bun, ensure ~/.bun/bin is on PATH
Search returns nothing for terms you know exist
Index is stale
A background qmd update runs after writes; if disabled (PI_MEMORY_QMD_UPDATE=off), run qmd update manually
"need embeddings" on semantic/deep search
Vectors not built yet
Embedding starts automatically in the background — retry shortly. If PI_MEMORY_QMD_UPDATE is manual/off, run qmd embed yourself
Collection pi-memory missing
Auto-setup didn't run (qmd installed mid-session)
Run any memory_search (auto-creates it) or qmd collection add ~/.dsh/agent/memory --name pi-memory
qmd works in the shell but not from dsh on Windows
Broken .cmd/.ps1 shims
The plugin bypasses them by invoking qmd's JS entry with node; make sure the npm global node_modules dir is on PATH
Memory isn't being injected after a write
Cache-stable snapshot only refreshes at checkpoints
Long-term writes refresh next turn; for daily/scratchpad use memory_read, or set PI_MEMORY_SNAPSHOT=per-turn
Running tests
# Unit tests (no LLM, no qmd — fast, deterministic. Node only.)
npm test
# End-to-end tests (requires dsh + API key, optionally qmd)
npm run test:e2e
# No API key? Use mock-llm.mjs at the repo root (a scripted mock LLM; its
# response rules are documented in the file header):
node mock-llm.mjs # serves on 127.0.0.1:8099
DEEPSEEK_BASE_URL=http://127.0.0.1:8099 DEEPSEEK_API_KEY=mock-key npm run test:e2e
# Recall effectiveness eval (requires dsh + API key + qmd)
npm run test:eval
# Pin provider/model for cheaper eval runs
PI_E2E_PROVIDER=deepseek-official PI_E2E_MODEL=deepseek-v4-flash npm run test:eval
All tests back up and restore existing memory files.
Recall accuracy with vs without selective injection
Development
This is a single-file plugin (index.js). No build step required.
# Test with dsh directly
P=verify-$(basename $PWD)-$$
dsh plugin --profile "$P" add @deepseek-ai/dsh-headless@next
dsh plugin --profile "$P" add .
dsh --profile "$P" "remember: I prefer dark mode"
# Verify memory was written
cat ~/.dsh/agent/memory/MEMORY.md
Publishing (maintainers)
Releases are tag-driven. Pushing a v* tag runs the publish workflow, which lints, builds, runs the unit tests, verifies the tag matches package.json, and then publishes to npm.
npm version patch # or minor / major
git push --follow-tags
Changelog
Upstream CHANGELOG.md is preserved verbatim (its SHA-256 is pinned in THIRD_PARTY_NOTICES.md so the claim is self-verifiable). The dsh port's own change list lives in the delivery report of porting ticket #18.
Third-party notices
Upstream pi-memory is MIT-licensed. Full notices and verbatim-file verification live in THIRD_PARTY_NOTICES.md; the license text is in LICENSE.