JunNanLYS/dsh-layered-memory

JunNanLYS/dsh-layered-memory

dsh 长期记忆:对话自动蒸馏为原子事实、场景摘要与用户画像,每步模型调用前自动注入相关记忆。开箱零配置——BM25 + 向量混合检索,可选本地离线嵌入,chat/work 双族隔离。

记忆agent-memoryai-agentcontext-engineering

DeepSeek Harness hero banner: conversations distilled into layered memories and recalled before every model step — chat bubbles dissolve into three progressively brighter light layers flowing into a frosted-glass capsule with a glowing orb and gradient track (tick labels: 日常·工作·智能·关闭), with light threads looping back to suggest recall

dsh-layered-memory

A layered distillation memory plugin for DeepSeek Harness: conversations are processed in the background through L0 capture → L1 atomic memories → L2 scene consolidation → L3 persona distillation, and relevant memories are automatically injected into context before every model step — neither the user nor the model needs to do anything.

简体中文 · Latest release · Report issues

Getting Started

Requires Node ≥ 22.16 and DeepSeek Harness 0.1.2-alpha.1 ~ 0.1.2-rc.1 (as of 0.10.0 support extends to 0.1.2-rc.1, which the npm "latest" tag already points to; for older hosts see the release history). Two invocation styles — the npx prefix can replace dsh in any command below:

# Option 1: run the official CLI directly via npx (no pre-installed dsh)
npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory

# Option 2: with the dsh CLI installed (upgrade: npm i -g @deepseek-ai/dsh,
# then restart; dsh is a pnpm forwarder — npm i -g pnpm first if missing)
dsh plugin --profile web add dsh-layered-memory

# Alternative sources: GitHub repo / local path (dev & debugging, link: points at the repo; pnpm run build + restart dsh to apply)
dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
dsh plugin --profile web add /path/to/dsh-layered-memory

If your current agent can run terminal commands, send it this message as-is:

Please install the dsh-layered-memory plugin for the web profile of DeepSeek Harness.

Run only the two commands below and do not modify any other profile:
dsh plugin --profile web add dsh-layered-memory
dsh --profile web --dump-config

Confirm that dsh-layered-memory appears in the output, then report the result to me.
Do not close or restart my running DSH yourself; after installation, remind me to manually restart the DSH Web Host.

The agent should report the installation result and explicitly tell you whether dsh-layered-memory has appeared in the configuration.

This package declares a dsh.bundle composition layer (cordis.patch.yml); after installation the plugin entry is mounted automatically — no need to hand-edit $DSH_HOME/profiles/web/cordis.patch.yml. Then restart DeepSeek Harness and verify: the appearance of conversations/ records/ scenes/ and memory.db under ~/.dsh/memory/ means the plugin applied successfully; the "Memory" page in settings (the Memory Workspace with five zones) and the memory chip in the input bar (Memory · Auto) mean the client half is ready.

⚠️ Security note: installing a plugin = running third-party code with your privileges. This plugin reads session content, writes files in its data directory, and calls the LLM/embedding services you configured; if that concerns you, review the source first (src/).

Uninstall: dsh plugin --profile web remove dsh-layered-memory + restart. Data stays in ~/.dsh/memory/; delete the whole directory manually if you don't need it.

Terminal Hosts (TUI / headless)

Beyond web, the same plugin installs into terminal-shaped hosts:

# dsh-tui (Claude Code style TUI front door — see github.com/ccch1mneyyy/dsh-TUI · site https://dshtui.com)
dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui   # first time: create profile + install the TUI host
dsh plugin --profile dsh-tui add dsh-layered-memory
dsh --profile dsh-tui
  • /memory <auto|chat|work|off> switches the current session's memory mode; with no argument a selection panel pops up;
  • Status line: a persistent 记忆:智能-style mode readout sits right above the prompt;
  • /settings section: master switch, capture, distill, recall, distill model routing and input budget are editable in place (same store as the web settings panel; complex routing chains still go through web / YAML);
  • headless (dsh --profile headless "one-shot task"): capture and distillation work as usual with no UI elements. Web and terminal hosts share the same ~/.dsh/memory/ store.

Development from Source

git clone https://github.com/JunNanLYS/dsh-layered-memory
cd dsh-layered-memory
pnpm install && pnpm run build
dsh plugin --profile web add .        # link: install; after code changes, pnpm run build + restart dsh
pnpm run smoke                        # smoke test (rebuild first: see command below)
npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution nodenext --target es2022 --strict --skipLibCheck --esModuleInterop

Dependencies are managed with pnpm (matching the official dsh install channel; the version is pinned via the packageManager field).

Runtime Data Flow

Runtime data flow: session events from User and Assistant (left) flow into the plugin (L0 capture, L1–L3 distillation, retrieval, memory tools), which injects relevant memories into the DeepSeek Harness core (right) at agent/pre-step; distillation reuses the core's ctx.llm and data is dual-written to ~/.dsh/memory/

The plugin attaches to DSH-native event seams (session/event for capture, agent/pre-step for injection) and reuses the host's ctx.llm for distillation. Recall is presented as message-side injection: relevant memories enter the conversation as a synthetic message placed right before the user's new message, rendered as a "Context injection · memory" row in the chat flow (expand to see the hits) — so you can see "memory at work" directly. Injected content is bounded by length and time budgets — oversized lines are truncated (pointing the model at the memory tools for the full text) and a timed-out recall silently skips that turn, never slowing the chat. Per-session dedupe: a memory already injected in this session is not injected again (the model's context already holds it — follow-up questions on the same topic save tokens); the record resets when the context is compacted or cleared, so memories can flow back in, and an updated memory (new id after a content change) is never held back by the old suppression. Freshness weighting: recall ranking applies a soft weight relevance × max(0.5, 0.5^(days since last update / 30)) — among candidates of similar relevance the fresh one wins (slots rotate naturally), while a strongly relevant old memory still recalls fine (the floor caps its loss at half a ranking score, so long-lived facts never sink); tune via recall.decayHalfLifeDays, 0 disables. It also registers three model-callable memory tools: memory_search / conversation_search / memory_read_scene.

Cost dashboard: every distillation LLM call (extract / dedup / L2 / L3) writes its token cost to a SQLite detail table keyed by provider/model (configurable retention, default 365 days with rolling cleanup on write; accounting failures only log a warning and never block distillation). Visualize it under Settings → Memory → Insights → the Cost sub-page: per-model trend lines (day/week/month granularity + last-N-days window + L1/L2/L3 layer filter), a layer × time-window table (calls / output & reasoning tokens / mean / median), and per-model totals — distillation overhead at a glance. Input is counted in characters (DSH streaming usage carries no input tokens); output and reasoning in tokens.

In action: the "Context injection · memory" row surfaces relevant memories first, and the model then calls memory_read_scene directly to read scene blocks before answering from memory:

Real conversation UI (light theme): a

In restricted sessions where only the code-execution entry point is available, the model reaches the memory tools indirectly through run_code (nested as SUBTOOL calls in the trajectory view):

Tool-call trajectory view: a colored timeline on top and a step list on the left (SYSTEM/CONTEXT/USER/ASSISTANT/TOOL/SUBTOOL tags); a run_code tool step nests 5 memory_read_scene sub-tool calls (SUBTOOL tags), with a detail panel for the selected step on the right

Layered Memory (L0–L3)

Four memory layers refining from top-left to bottom-right: L0 raw conversation (chat bubbles) → L1 atomic memories (glowing fact particles) → L2 scene blocks (glass document slabs) → L3 core persona (radiant crystal core); stages connected by LLM extract/consolidate/distill light beams, shrinking width shows progressive refinement

Per-Session Memory Modes

Session memory chip and cascade menu in dark theme: in the composer-left cluster, right of the Read Only chip sits the

The conversation side is a distributed memory surface — each kind of information lives in the native host seat designed for it; the plugin no longer owns a strip:

  • Memory chip (composer-left cluster, right of the Read Only chip): a borderless Memory · {Auto|Personal|Work} chip in official composer-chip grammar; the text is the resolved truth — Memory · write-only (injection off), Memory · paused (gray dot), Memory · degraded (amber dot); zh/en bilingual, following the host language.
  • Cascade menu (click the chip, opens upward): two rows, Memory scope {value} › and Data flow {value} ›; data-flow options live in a hover-only secondary panel (Follow global / Read & write / Write only / Paused) — no click-pinning, with bridge hot-zones so slow mouse travel never breaks the hover chain; full keyboard path (arrow-key roving + focus reveal).
  • Inline slider (click "Memory scope", grows in place): three stops Personal / Work / Auto; crossing a stop updates the chip text live while dragging; keyboard arrows/Home/End + aria-valuetext.
  • Pause-resume snapshot: switching the data flow to "Paused" enters the off mode and persists the pre-pause scope and injection override; resuming restores them as-is. Per-session choices persist to session-modes.json (stacked with the global switches — global is the master gate); L2/L3 are fully family-isolated.
  • Memory occupancy lives only in the official context meter panel: opening the official ring shows the "Memory" section (recall snippets / memory stable zone); the composer area has zero occupancy UI — only a N pending distill telemetry segment appended to the official stats line.
  • Write-only sessions (#38): pick "Write only" in the data-flow panel for a write-only session — capture and distillation continue as usual (conversation still settles into L0→L1→L2/L3), but nothing is injected into this session (recall injection, the persona/navigation stable section and the tools guide all stop; memory_search and the other read tools return a write-only notice). The override persists per session; switching back to "Follow global" clears it to the recall toggle in Automation. Ideal for debug/eval/sensitive sessions that should absorb without interference. Orthogonal to Paused: paused is full stealth (capture off too), while write-only keeps the "in" and gates the "out".
  • Terminal host (TUI): the same mode system surfaces in the dsh-tui host as the /memory command, a status line and a /settings section (see Quick Start).

UI Preview

Memory Workspace overview in dark theme: five-zone task nav (Overview selected), health summary card (running normally + storage/vector/distill-queue subsystem chips + a pending-distill attention chip), recent-activity list (New/Updated verb tags + Memory/Scene kind tags + relative times), key-number tiles (memories / scenes / weekly distill output / last distill) and jump buttons to the other zones The same Memory Workspace overview in light theme: identical five-zone nav and health/activity/numbers layout on light card backgrounds with the same accent family, theme switch without reload

Settings → Memory is the Memory Workspace (five-zone task nav, sticky tabs + arrow-key roving):

  • Overview: health summary + recent activity + key numbers + a guided empty state;
  • Library: a read-only asset activity feed with L1 memories / L2 scenes / L3 personas mixed by update time (search + type/scope/time filters + in-place expansion + copy);
  • Automation: basic switches + advanced disclosure (distill route chains and budgets) + embedding-model disclosure;
  • Insights: Cost / Activity (7-day asset activity + distill calls & failures) / Recall (cumulative totals + disabled distribution);
  • Maintenance: runtime health + diagnostic log + the danger-zone full rebuild (confirm modal + progress + cancellable).

Measured Comparison (DSH-MemBench: Automated Benchmark)

Screenshots show what the plugin looks like — this section answers "what does enabling it actually buy you?" with measured numbers from an automated benchmark (bench/, one command to reproduce). Method: the same scenario bank with verbatim-identical inputs runs in Group A (memory on) with 3 merged repetitions and Group B (memory off) with 1 repetition (a memory-off long task burns multiples of the tokens per scenario — a cost guardrail); the dialog track now runs Group A only (memory-off probes in independent sessions cannot succeed, so the control carries no information — retired). Dialog-track environment: DeepSeek official deepseek-v4-flash, plugin 0.8.5 (judge same-source as tested; every answer archived for manual audit), Windows; taxonomy adapted from LongMemEval / LoCoMo / AMB, with the extended probe types and lifecycle track informed by MemoryAgentBench / GoodAI LTM / BEAM.

The dialog track below is the fresh 0.8.5 baseline (fixed plugin + corrected judging criteria); the workflow-track numbers remain the archived 0.8.3 run (the bank has since grown to 8 scenarios with a prospective-memory addition — re-run pending).

Dialog track (20 scenarios × 10 probe types × 3 reps = 420 questions): does it remember correctly

0.8.5 baseline (Group A data; the dialog-track B arm is retired — Group A only).

DSH-MemBench dialog track accuracy chart (Group A, memory on): overall accuracy 95.2% (400/420); six core probe types, 60 questions each — extraction 58/60, multi-hop 60/60, temporal 56/60, updates 55/60, scene recall 52/60, abstention 60/60 with 0 fabricated; four extended probe types, 15 each — accretive completion 15/15, update chains 15/15, event ordering 14/15, paraphrase 15/15

Dual-channel recall (Group A): passive injection hit rate 78.1% (the answer's key points appear in the recall injection, 281/360); most of the rest the model recovered by actively calling the memory tools — 106 questions with active queries, 75 rescued by tools. The end-to-end 95.2% is the composite of both channels plus model utilization. With the memory store accumulating across scenarios for the whole run, 295 probe injections carried other scenarios' memories (honestly counted) — yet accuracy actually rose from 92.8% (early, small store) to 97.7% (late, largest store), and offline flooding with 600 extra synthetic records moved retrieval recall@5 by only −2.8pp: interference resistance under a growing store, measured.

Layered weaknesses: offline retrieval metrics (recall@5, controlled replay) total 73.3%, with event ordering at 0% and scene recall at 50% — end-to-end still 93%+ thanks to model robustness over adjacent injected memories. Efficiency triangle (the cost of memory): injections add no latency (injected turns respond 210ms faster on average), recall text is ~10.3% of per-turn input, and the whole distillation pipeline costs ≈2727 input / 240 output tokens per captured message (1172 calls, 0 failures).

Workflow track (archived 0.8.3 · 7-scenario edition · Group A ×3 / Group B ×1, real tool sandbox): does it do it right, and cheaper

DSH-MemBench workflow track, Group A vs Group B: probe-phase completion A 59/69 (85.5%) vs B 10/23 (43.5%); cost comparison (Group B as the full-bar baseline, per-scenario means) — steps 24.3 vs 41.4 (B +70%), tool calls 37.7 vs 62.1 (B +65%), input tokens 266k vs 1.81M (B 6.8×); style-convention scenario probes A 12/12 vs B 0/4; long-task input tokens per scenario A 266k vs B 1.81M

Probe-phase completion 85.5% vs 43.5% (+42pp): both groups have live context during teach/change phases — the probe phase (continuation task in a fresh session) is the pure memory window. Group A scored a perfect 12/12 on all three new probe archetypes (workflow knowledge update / twin-runbook disambiguation / style-convention continuity), consistent across all three reps; Group B scored 0/4 on style-convention probes (naming/structure/thousands-separator/footer conventions exist only in memory — they cannot be explored out of the sandbox), while on the workflow-update scenario it can reverse-engineer the procedure by reading the script (discrimination limited by sandbox affordances, honestly noted).

Long-task cost: Group B burns 6.8× Group A's input tokens per scenario (1.81M vs 266k) — without memory the agent advances by re-exploring, and under a high reasoning effort it even builds its own projects to probe what a one-line script convention would have done; output tokens 3× (46.2k vs 15.4k), steps +70%. This is memory's core value: what it saves is not task difficulty, but pointless round-trips and re-exploration.

Methodology & reproduction

node bench/harness/run.mjs --arm A --repeats 3 --provider deepseek-official --model deepseek-v4-flash   # dialog track (Group A only)
node bench/harness/run.mjs --track workflow --arm AB --repeats 3 ...                                  # workflow track (A/B arms in parallel)
node bench/harness/run.mjs --track lifecycle --arm A ...                                              # lifecycle track (gating/off/rebuild/forget)
node bench/harness/report.mjs --latest [dialog|workflow]                                               # aggregate report
node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600                                     # retrieval metrics + flooding curve
  • Scoring: programmatic contains-all plus an LLM judge against key points (every answer and verdict is preserved in result.json for human audit); for stale-bearing probes (updates/update-chains/forget) an old value only fails when stated as the current answer, and abstention probes allow citing real adjacent facts while denying the asked point; workflow completion is verified programmatically from produced files and their contents (four check kinds: positive / forbidden-word / must-not-exist / exists);
  • Metric surface: beyond the per-type accuracy table (6 core + 4 extended types), reports automatically include offline retrieval metrics (recall@5 / injection precision / stale leakage), the efficiency triangle (injection latency differential / injection share / distillation accounting per captured message), scale-position analysis (accuracy & contamination vs store growth), and the lifecycle-track section (family-gating matrix / off-mode dual assertions / rebuild fidelity / forget requests);
  • Live progress: running the benchmark auto-starts a local progress panel and opens the browser (--no-panel to disable) — per-arm scenario/phase/message-level progress, heartbeat & activity freshness (distinguishes "stuck" from "process died"), and cumulative cost as it accrues;
  • Metrics come from provider-reported usage (input with cache-hit split) and session-event folding; the steady-state cache rate excludes each session's first request (0.8.5 baseline: 89.1% — memory injection does not hurt caching);
  • Regression use: run before/after a plugin change and diff with compare.mjs (environment header check including git SHA + Group-B control-drift warning + retrieval-metric comparison);
  • Limitations (stated honestly): single machine; Group A ×3 merged, Group B ×1 (cost guardrail — noisier); judge vs tested model: same model in the 0.8.5 dialog baseline, heterogeneous in the archived workflow run (glm-5.3 judging v4-flash); the scenario bank is author-built (biased toward memory-advantage scenarios — reproduce it yourself); sandbox-file affordances partially leak procedures (Group B can reverse-engineer by reading scripts — discrimination limits honestly noted); dual-tier tool audit (strict violation voids the scenario / loose heuristic flags only), with 0 violations measured on both sides.

Full reports and per-question data: bench/baseline/.

Configuration

Override configs go into the profile's own cordis.patch.yml as a top-level bare patch entry (direct id:, not wrapped in insert: — an insert with the same id as the bundle layer appends and causes duplicate loader entry id startup failure):

- id: dsh-memory
  name: dsh-layered-memory
  config:                    # keys replace whole lines (no deep merge); write out all keys you want to keep
    family: auto             # default mode for new sessions: auto | chat | work
    llm:                     # static distillation route (both fields set = deployment pin,
      provider: ''           # which outranks the settings-page route chain; when empty the route
      model: ''              # follows the route-chain primary row in the settings page or the default model)