dsh-g-memory: Adding a cross-session semantic memory layer to DeepSeek Harness

Preface

The history sessions of DSH reside on disk, and the only usable entry in the past was the built-in FTS5 full-text index, matching by title and exact tokens. It could not answer “what we decided about retry strategies three weeks ago” because that sentence was not in any title.

dsh-g-memory wants to fill this layer: slice, embed, and index content with information from history sessions, then let the model retrieve it on demand. The maintainer is Gladess1102, categorized as “Memory”. As of 2026-11-06, the GitHub repository API returns stargazers_count: 1, license.key: mit; package.json has version: 0.14.3, license: MIT, engines.node: ">=20", and dependencies is empty. There is no new persistent service; retrieval is all done locally.

Below, I will introduce in the order of “what it is, what it remembers, how to search, how to configure”.

What it is

In one sentence: dsh-g-memory is a cross-session semantic memory plugin for DeepSeek Harness that builds a local vector and lexical index on session history, and injects recalled content as explicitly bounded non-trusted context on demand.

Let’s list the differences with DSH’s built-in capabilities first.

DSH Built-in FTS5 dsh-g-memory
Match Method Title, exact tokens Vector similarity + CJK bigram lexical
Can Answer “Sessions containing retry in title” “Conclusions about retry policy three weeks ago”
CJK Handling unicode61, whole paragraph as one token Adjacent character bigrams, weighted by idf
Deployment Prerequisite Built-in Needs a local embedding service
Index Location DSH itself Plugin’s own local index

The two do not conflict: the built-in index makes no changes; the plugin adds a semantic recall layer on top.

Ecosystem-wise, DSH’s design is “everything is a plugin”; SkillHub is the community plugin directory site, with no official affiliation with DeepSeek.

What content it remembers

There are many event types in session logs, and the plugin only keeps a few categories that have independent meaning. The README provides a list by category, organized here.

Kept:

  1. user/message. Human speech. Runtime injection (source.kind = plugin, e.g., prompt snapshots, reminders) is filtered out—these are not memory, yet they occupy a large portion of the index.
  2. assistant/message. Conclusions, explanations, and decisions written by the model.
  3. tool/result. Measurements, errors, command output. The highest density of facts in logs: strings like 5.95, -0.000033, 512 token appear 0 times in the dialogue body, only in tool output. Without this category, memory can remember “what we talked about”, but not “what we measured”.
  4. tool/call events that write files (write, edit, etc.). This is the only record of file history: on disk only the latest state remains, and overwritten or deleted content cannot be recovered.

Not Kept:

  1. chunk series streaming shard events, accounting for 84.6% of log events, with no independent meaning.
  2. File reading and blob JSON in tool/result, read when used; they take up space and crowd out real memory.
  3. Shell commands and run_code code bodies in tool/call, measuring this content in RED is +4.0 MiB and +6.9 MiB of rerunnable noise.

Chunking is done by sentence boundaries: body 800 characters, tool output 1200 characters, adjacent blocks overlap 120 characters. This is not a layout issue—README points out that fixed-width chunking would split values and the sentences explaining them; chunking by sentence allows the two retrieval arms to complement each other.

All rules for keeping and discarding fall in the indexing phase, not the recall phase: a small rolling index and a full rebuild follow the same selection logic.

Retrieval Pipeline

Retrieval runs two arms in parallel, then fuses them using Reciprocal Rank Fusion (RRF).

Vector Arm

The default model is EmbeddingGemma 2. The config is embed.model: embeddinggemma-2, embed.url: http://127.0.0.1:11439, 768 dimensions, int8 quantized, cosine for each unit. EmbeddingGemma-300M and BGE-M3 (1024 dimensions) can also be used per configuration, but the index is only valid in the embedding space where it was generated; changing models requires a full rebuild, mixing is not allowed.

This model requires role prefixes: the prompts for query and document differ. Thus there are two configs embed.queryPrefix and embed.docPrefix. The prefixes are added once at the request boundary, and the caller only declares whether they are embedding a query or a document. On the development corpus (28 annotated cases, 99 comparison pools): bare R@1 is 24/28, adding the model card’s built-in prompts is 26/28, while the previous generation it replaced was 25/28.

According to the model card, using the QuestionAnswering prompt on the query side is significantly better than the SearchResult prompt: under the same index, the vector arm @3 goes from 3/9 to 7/9, @20 from 15/22 to 19/22; the prompts on the document side are identical, so changing the query side prompts does not require rebuilding the index.

Lexical Arm

CJK is cut by adjacent character bigrams, Latin by whole words, then weighted by idf. This design has empirical basis: naive substring matching scores 4/8 on 8 Chinese annotated queries, improved to 5/8 after bigrams, surpassing the vector arm. DSH’s built-in FTS5 uses unicode61, treating the whole CJK paragraph as one token, having the same blind spot—README lists this as one of two findings.

Fusion and Precision Orientation

Performance of the three arms on the same 8 Chinese annotated queries:

Arm top-1 top-3 top-20
vector 4/8 5/8 7/8
lexical (bigram) 5/8 6/8 7/8
RRF fusion 4/8 6/8 8/8

Two points worth remembering. First, CJK must use bigrams. Second, this retrieval tuning is for recall rather than precision: the best top-1 among the three arms is only 5/8 (62.5%), no arm is reliable at rank 1, while the fused top-20 is 8/8. This layer is only responsible for putting relevant units into the candidate pool; the user selects the answer.

(This table was measured on README’s earlier BGE-M3 index; the author noted “the shape of the conclusion, not the specific values, is portable”.)

Late Results

The auto-recall path is split into two segments: when new session events arrive, the background starts embedding, retrieval, and synthesis; pre-step only reads what is already ready at that time, it does not wait. The entire pipeline takes 2.2–11 seconds (re-rank and deep pool take the bulk; embedding itself is about 10ms), while a normal Q&A has only one step available for consumption.

Previously, retrieval results were completed after the last step of the current round and would be dropped by the next message (marked late-drop, gone). Now these “completed but not consumed” results are brought into the next round: only providing a round of freshness, without increasing extra latency. Results carried over only appear when the retrieval for that round is still running, not injected into rounds already rejected, and expire with the result TTL. TTL is currently 5 minutes, increased from 2 minutes on 2026-11-03, based on the fact that 28 of 29 results expiring within three days on this deployment were re-retrieved by a new round of the same session, with a median interval of 7 minutes.

After the steps above, the model can already obtain relevant memories. The last path is discussed below.

Index Layer: Storing Memory in the System Prompt of Each Step

All previous paths reaching the model are waterfall: deciding if recall is needed → retrieval → gate → inject before step. All four steps must function normally in a round; but as of 2026-10-25实测, 54 out of 83 sessions (65%) had zero memory injection, stuck at max 2 cards visible in 267 sessions.

The index layer does not depend on these four steps. The approach is to append a line-by-line index to the system prompt assembled at each step inside the system-prompt/assemble hook: id prefix + first sentence of summary. It is just a map, does no relevance judgment, and the body is still retrieved only via memory_recall; the header already states this.

The budget is controlled by three configs: maxChars 1600 (total), maxCards 40 (limit on count), lineChars 72 (truncate length per line). A line either appears in full or not at all—half a line would be read as “a truncated fact”. The index block only lists current state memories; superseded / retired / expired do not appear. The footer states the actual visible count vs total, preventing the mistaking of “few that can be shown” for “only a few total”. The switch is indexLayer.enabled / DSH_G_MEMORY_INDEX_LAYER, default on.

The cost is low: the index block travels with every system prompt step, about 1073 steps per active day locally, converting to a few yuan worth of tokens per month; storage is zero bytes (blocks are rendered every time); memory is also static—82 MB loading and 296 MB inverted file are unrelated to this block.

Deployment and Configuration

Start the Local Embedding Service First

The only service required for retrieval is an OpenAI-compatible /v1/embeddings interface. The deployment method in the README is based on llama.cpp:

# EmbeddingGemma 2 Q8_0 is about 310 MB, model file from ggml-org/embeddinggemma-2-GGUF.
# Binary must contain PR #30054 (merged 2026-10-06); earlier versions cannot load this architecture,
# manifests as embedding endpoint failing to start.
llama-server -m embeddinggemma-2-Q8_0.gguf --embeddings \
  --host 127.0.0.1 --port 11439 -c 8192 -b 4096 -ub 2048

Two pitfalls are recorded in docs/DEPLOYMENT.md. --embeddings cannot be omitted, otherwise llama-server returns 501 This server does not support embeddings. Physical batch size cannot be left at the default 512: exceeding the limit is not truncation, but 500 input (N tokens) is too large to process, and the client treats the failure as “maintain order”, so re-rank fails every time but the system reports itself healthy. 1200-character tool output snippets are about 590 tokens, just enough to burst past 512, so both engines add -ub 2048.

This plugin was originally developed on Apple Silicon around BGE-M3 + bge-reranker-v2-m3 + MiniCPM5-2B (MLX), but that set was just a feasibility proof, not part of the architecture—the embedding, rerank, and synthesis slots are independent and swappable. On Windows, switching to llama.cpp / Ollama / LM Studio is sufficient.

Plugin Configuration Block

No official one-line install command appeared in the collected materials—the installation section in README was truncated by routing, unable to obtain the original text. Below only list verified configuration shapes from docs/DEPLOYMENT.md; the plugin in config is - id: dsh-g-memory plus config:

# ① Minimal: only embedding, everything else off
- id: dsh-g-memory
  config:
    embed:
      url: http://127.0.0.1:11439
      model: embeddinggemma-2
      queryPrefix: "task: question answering | query: "
      docPrefix: "title: none | text: "
    rerank: { url: '' }
    synthesis: { enabled: false }

The requirements for the three slots differ: embedding is required, rerank is strongly recommended, synthesis is optional. When rerank is unavailable, the gate falls back to RRF score, runs but distinction drops significantly; when synthesis is off, returns the original snippet directly, letting the session’s own model extract—the cost is about 1100 extra tokens per round, for zero extra service. Changing the embedding model requires a full rebuild; there is no “convert vectors”.

The embedding endpoint also accepts cloud OpenAI-compatible interfaces (OpenAI, Zhipu, Tongyi, etc., fill baseUrl + key), the cost is memory content leaving the local machine.

Rebuild When Changing Models

dsh-g-memory build --fresh      # Ignore old index and checkpoints, re-embed everything

The index header records dim and model; mismatched dimensions will error on query and explain why; same-width models or prefix changes are identified by the recorded name and prefix, the build end no longer reuses old vectors. Both the build end and card end have guards; the worst case is cards quietly failing to retrieve, not calculating wrong results.

An External Dependency

There are no runtime dependencies at the Node package level (dependencies empty in package.json), but one thing must be done first: zstd. The index prioritizes reading the .zstd compressed copy, falling back to plain .ndjson if no zstd is present; this is not a hard failure, but the first build will fail. Windows needs a separate zstd install (facebook/zstd releases into PATH); macOS is brew install zstd, Linux is apt install zstd.

Typical Usage

Command Line

The plugin comes with a CLI; subcommands come from package.json’s scripts:

dsh-g-memory build          # Incremental build (first time requires embedding service online)
dsh-g-memory stats          # View index statistics
dsh-g-memory recall         # Run a retrieval from the command line
dsh-g-memory selfcheck      # Check if embedding, rerank, etc. config is usable
dsh-g-memory plugincheck    # Check if plugin and host interface match

The scripts in the repository are bin/dsh-g-memory.mjs, and package.json’s scripts use node bin/dsh-g-memory.mjs <command>; docs/DEPLOYMENT.md uses dsh-g-memory <command> form. Both point to the same CLI.

selfcheck has practical use: it parses embedding config with the same priority as the host (environment variables → profile’s dsh-g-memory.config.embed → package default), and can detect inconsistent model names—model name inconsistency is not just a display issue, build treats a model name change as changing the embedding space, performing a full re-embedding and writing a header that the host will reject.

Dialogue Side

After configuration, the model calls memory_recall to get the body when needed. Injected content is labeled as explicitly bounded non-trusted context: memory is user content, not trusted fact, and the summary neutralizes and escapes <, so the card body has no chance to forge tags or system prompts.

The persistent card layer is optional: after consolidate.enabled: true, minIntervalHours: 24, minTurns: 5, synthesis compresses several snippets into one card. Cards have readCount / lastReadAt; cards with zero reads and older than staleAfterDays (default 90 days) are hidden from the index layer but can still be recalled—this is a visibility rule, not deletion.

Use Cases and Notes

Suitable for the following situations.

  1. Cross-session semantic retrospection. Questions like “what we decided about retry strategies three weeks ago” cannot be found by full-text search in titles.
  2. Memory not leaving the local machine. By default, retrieval is entirely local, with only one network call to a local embedding service.
  3. Want auditable injection. Injected content has explicit labels; the body is decided by the user whether to trust; the retrieval layer is only responsible for recall, not judgment.

Note these points.

  1. The plugin runs with the permissions of the current DSH process; what it can read depends on what DSH itself can read. Check the source code and license (MIT) before installing; there is another docs/SECURITY-REVIEW.md in the repo, which I have not verified line by line.
  2. GitHub shows 1 star, forks and open issues are 0. The author did a lot of empirical testing on their own deployment and publicly shared some data and methods, but the project scale is small—decide for yourself whether to adopt it.
  3. Retrieval tunes for recall. top-1 is unreliable; the user needs to pick the answer themselves.
  4. Changing the embedding model must fully rebuild; there is no “convert vectors”.
  5. Telemetry is on by default. CHANGELOG 0.14.3 notes: README used to say “telemetry off if not set”, but the target in the code is always the default path; local auto-recall-stats.jsonl has reached 3.7 MB / 29,382 lines, one line per model step. 0.14.3 changed to write documentation based on code facts (default on, variable only moved path), and added an 8 MB limit and rotation. Know this before configuring.

Conclusion

What dsh-g-memory does is add a semantic index to DSH’s existing session logs: retrieval by content rather than title, the index stays local, and injection is labeled as non-trusted. It doesn’t conclude for you; it only ensures the correct small segment is put into the candidate pool, and uses the small map in the system prompt of each step to let the model know “what is here first”.

Directory page: https://www.skillhub.cn/plugins/Gladess1102/dsh-g-memory
Repository: https://github.com/Gladess1102/dsh-g-memory

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye