Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add @dsh-plugins/dsh-user-mirror
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
Screenshots
README
With vs. without
| Without | dsh-user-mirror | |
|---|---|---|
| New session | You repeat your principles | Auto-injected — the model already knows |
| What's recorded | Opaque — you can only guess | Every entry shown in the Memory tab |
| Why it was recorded | Untraceable | Every entry carries its reason — click to see |
| Will it bloat context | Yes | Injected by token budget, not by entry count |
| Wrong entry recorded | You have to restart the session | Click ✕ to withdraw |
| Outdated preferences | Stays forever, misleads the model | Decays — entries unconfirmed for 30 days move to the fading section |
What this is
A DSH plugin: the model actively records how you think, persists it across sessions, and re-injects it into the system prompt.
- 🧠 Learn — when the model notices you expressed a reusable judgment, it calls
mirror_rememberand writes down why - 💾 Store — persisted locally via
ctx.storageDomain; capacity is bounded, entries decay, new beliefs overwrite old - 🎯 Use — auto-injected into the system prompt by token budget, not entry count
Only judgments, never facts
This is the only design choice that matters. Facts (paths, accounts, servers, repo addresses) have their own source of truth — recording them again here creates a second source of truth, and they will eventually drift apart. So this plugin only takes what isn't already in a source of truth: your principles, tradeoffs, redlines, working style.
This is also the answer to "the human brain can't grow infinitely": judgments are inherently bounded (dozens at most), facts are not. By taking only the former, capacity doesn't need to be brute-forced by eviction.
| Category | Records |
|---|---|
principle |
Principles / tradeoffs — how to choose under what conditions |
redline |
Redlines — never do X |
workflow |
Workflow — process & collaboration habits |
taste |
Aesthetic / expression — wording, style, presentation |
Why not infer from the thinking trace
Before v0.4 we regex-scanned the model's reasoning stream looking for phrases like "the user's preference is..." — zero hits on real data. The assumption that the model will paraphrase your preference in a specific Chinese phrasing is too brittle; and even when it does, "the user wants me to change this button" would get recorded as a long-term preference.
"Is this a long-term principle or a one-off request?" is a judgment. Regex is a hand. A hand can't make that judgment. So we let the model decide and call the tool itself — which also gives explainability: every recording happens in the conversation, visible to you, with a reason attached, instead of being silently written in the background.
What the Memory tab looks like
After install, a new Memory tab appears next to Conversation / Trace. It isn't a list — it's a portrait:
- One-line self-summary at the top — the most-confirmed judgment, i.e. the one the model is most sure about you
- Four sections — redlines in their own block (crossing one has cost and they don't decay), principles as the main body, workflow & taste compressed into two columns
- Weight = confidence — the more often a judgment is confirmed, the heavier the line; weak ones fade to grey. No "strength 3.7" numbers — those don't mean anything to humans
- Evidence folded inside — the front face is the abstract judgment; click "which sessions" to expand into the concrete events
- Decaying — judgments unconfirmed for 30 days move into a folded section, so "it decays" is visible, not theoretical
- Every entry can be withdrawn via
✕. Two-step: first click becomes "confirm withdrawal?", the second click deletes — the deletion endpoint only accepts DELETE/POST, returns 405 on GET, so browser pre-fetch or back/forward replay never accidentally deletes - An ai-orb status pill speaks up: reading / refreshing / just-recorded (with badge) / failed
Memory model: structured and decaying
Memory isn't unbounded — humans forget, and new beliefs overwrite old ones:
| Mechanism | Rule |
|---|---|
| Strength | strength = hits × time-decay (forgetting curve) |
| Decay | Half-life default 30 days: how long without re-confirmation until strength halves |
| Overwrite | Same-topic new statements overwrite old (no near-synonym piling) |
| Cap | Default 20 entries; weakest evicted first |
| Budget | Injected by token budget (default 500), not "dump everything" |
Same-topic detection uses two channels: English entities (r2 / picx / main — in tech context these are basically topic tags; two shared = same topic) plus Chinese bigram overlap. Known boundary: purely-Chinese paraphrases with no shared entities ("reply in Chinese" vs "always Chinese, keep technical terms in English") fall through, recorded as two. To truly fix it you'd need embeddings — not worth the weight for one edge case. Plus the model sees existing memory when recording, so dedup is mostly the model's job; findSimilar is only the safety net.
Known limitation: heavy-paraphrase Chinese gets recorded twice
Same-topic detection runs on two channels — English entities (r2 / picx etc.; two shared = same topic) and Chinese bigram overlap (threshold 0.3). Pure-Chinese statements that differ by only one or two characters merge correctly (measured similarity 0.67–0.77), but restated freely slips through:
| A | B | bigram similarity | Outcome |
|---|---|---|---|
| Always run tests before committing | Always run the tests once before commit | 0.77 | ✅ merged |
| Must ask me before deleting anything | Before you delete something, come ask me first | 0.18 | ❌ recorded twice |
The cause: bigram only sees surface overlap, not synonymy. With a hard entry cap, duplicates push out useful memory.
Current workaround: when calling mirror_remember, reuse the existing phrasing; if you spot a duplicate, call mirror_forget to drop one.
Real fix is synonym merging (word vectors or letting the model self-dedupe) — not done yet. This limitation is locked down by a test (test/memory-core.test.mjs "known defect" case); improving it later will fail that test on purpose, reminding you to update this section.
Install
Only Node.js is required — no global dsh install; upstream's official entry is
npx @deepseek-ai/dsh. If dsh is installed globally, usedshinstead.
npx @deepseek-ai/dsh plugin --profile web add @dsh-plugins/dsh-user-mirror
⚠️ Only works on profiles that provide storageDomain and webServer (i.e. web). A headless profile has neither — the plugin will sit in pending (waiting for services: storageDomain, webServer) and break boot. The first is the foundation for persistence; the second is the channel the Memory tab reads from.
On ai-orb vendoring
The sole runtime dependency is ai-orb (an SVG status pill, zero deps). It's declared in dependencies but is not bundled into the client — the client loads it from same-origin via import('/dsh-mirror/vendor/ai-orb/index.js'). The path is served by the host side, which exposes node_modules/ai-orb/src/ as the static route /dsh-mirror/vendor/ai-orb.
Why not bundle: the status pill is instantiated by multiple pages at once; a separate ESM module rides on HTTP/2 multiplexing without blocking first paint. And upgrading ai-orb is just pnpm update ai-orb — no hand-copied source to keep in sync.
How it works
- The model sees existing memory in the system prompt (sorted by strength, bounded by token budget)
- When you express a reusable judgment, the model calls
mirror_remember(text, kind, reason) - Normalize + run entity/bigram dual-channel matching to decide: overwrite same-topic old memory, or add new
- If capacity is exceeded after adding, evict the weakest entry
- Next time the system prompt is assembled, this entry is already in it
Tool calls are visible, so "what did it record and when" you see right in the conversation. The Memory tab also shows the reason (why recorded) plus strength, confirmation count, and last-confirmed time.
Configuration
Override in cordis.patch.yml:
- insert:
- id: dsh-mirror
name: dsh-user-mirror
config:
maxPreferences: 20 # Memory capacity cap (entries)
halfLifeDays: 30 # Half-life in days; longer = slower decay
maxTokens: 500 # Token budget for injection
sectionOrder: 160 # Position in the system prompt (smaller = earlier)
Tools
mirror_remember(text, kind, reason)— record a judgmentmirror_forget(text, reason)— withdraw an incorrectly-recorded one (matched by topic, exact wording not needed)
There is no query tool, by design. Memory is already injected into the system prompt; adding a query tool would be a second read path, eating tokens for nothing. v0.6 replaced the old mirror_preferences with mirror_forget — without withdrawal, a wrongly-recorded entry would either take half a month to decay or wait for the same-topic new statement to overwrite it (and Chinese paraphrases miss), polluting every system prompt turn in the meantime.
Privacy
- Preferences stay local (
~/.dsh/storages/) - No network, no upload
- Want to wipe? Delete the
dsh_mirrordomain
License
MIT © webkubor
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.