返回目录
Agent 与会话 插件

dsh-markdown-memory

SYMlp/dsh-markdown-memory

Markdown folder long-term memory plugin for DeepSeek Harness (dsh): one file per fact, human-editable, git-versionable

Stars
0
Forks
0
Issues
0
更新
今天

PROJECT TOPICS

项目标签

INSTALL REFERENCE

安装参考

未验证
dsh plugin --profile web add github:SYMlp/dsh-markdown-memory

该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。

PROJECT README

README

dsh-markdown-memory

Your memory is a folder. Every agent already knows how to read a folder.

Markdown folder long-term memory for DeepSeek Harness.

A memory directory holds one markdown file per durable fact, each with YAML frontmatter (name, description, optional metadata) followed by the fact body. An index file (default MEMORY.md) carries one line per memory. This plugin mounts the index and a recursive file listing into every request as a system prompt section; the model reads individual memory files on demand with its file tools.

Why this one

  • Zero infrastructure — no daemon, no database, no account, no embedding pipeline. Point at a folder.
  • Agent-agnostic by format, not by bridge — the same folder a Claude Code auto-memory already writes, the same vault Obsidian already manages. dsh joins your memory; nothing migrates, nothing syncs, because there is only one copy.
  • Human-owned — read it, edit it, git diff it, delete a line of it. When the agent is gone, your memory is still yours.

The position is deliberate: cross-agent memory does not need a mechanism. It needs a format every agent can already read, and a markdown folder is that format.

Compared to other memory approaches

Approach Where memory lives Cross-agent You can edit a memory by hand
Graph / vector memory engines Engine's own store (binary/DB) Via that engine everywhere No — through the engine
Hosted memory services Their cloud Via their connectors Through their UI/API
Session-distillation plugins Generated files per harness Usually single-harness Yes, but the pipeline rewrites
dsh-markdown-memory A folder you already own Any agent that reads files Yes — it's just a file

Each row is a legitimate trade. Pick an engine when you want automatic extraction and semantic search out of the box; pick a service when you want fleet-wide memory without owning storage. Pick this plugin when you want your memory to outlive every agent that reads it.

Install

dsh plugin --profile <name> add dsh-markdown-memory

Then point path at your memory directory in the profile's cordis.patch.yml:

- id: markdown-memory
  name: dsh-markdown-memory
  config:
    path: /absolute/path/to/your/memory
    # indexFile: MEMORY.md
    # maxBytes: 32768
    # sectionOrder: 120

Configuration

Field Default Meaning
path (required) Absolute path of the memory directory. Load fails when it is missing or not a directory.
indexFile MEMORY.md Index file name inside the directory. Injected in full.
maxBytes 32768 Byte bound for the injected section. Longer content is truncated with a notice.
sectionOrder 120 Prompt section order, inside the 100–199 tool-guidance band.
recursive true Whether subdirectories are listed.
ignore .obsidian, .git, .trash, node_modules Directory names skipped at every level. Setting this replaces the default list.
maxDepth 8 Directory levels walked below the memory directory; 1 lists the top level only.
maxFiles 500 How many paths the listing carries. Further matches are counted in a notice, not dropped silently.
seedMaxBytes 16384 Byte budget for the bodies seeded at session start. A memory that does not fit is skipped whole, never truncated.
recall true Whether trigger-matched memories are recalled at each step.
maxRecallPerStep 3 How many memories one step may recall.
caseSensitiveTriggers false Whether trigger matching is case sensitive.

Misconfiguration fails at load with a message naming the field.

Mount your Obsidian vault

An Obsidian vault is a markdown folder, so it mounts directly:

- id: markdown-memory
  name: dsh-markdown-memory
  config:
    path: /absolute/path/to/YourVault
    indexFile: Home.md   # or whatever your map-of-content note is

What the model then gets: your map-of-content note verbatim, plus a listing of every note in the vault as a path relative to the vault root (projects/alpha.md), and it reads any note on demand. [[wikilink]] guidance is already part of the injected section, so vault-style links resolve naturally when the model follows them.

Vault housekeeping folders are skipped by default — .obsidian, .git, .trash, node_modules. Setting ignore replaces that list rather than extending it, so keep the entries you still want skipped.

Current constraints for vaults (honest ones): the walk stops at maxDepth levels and the listing carries at most maxFiles paths, and when a vault exceeds either bound the section says how many notes it left out rather than trimming quietly. Large vaults should raise maxFiles and maxBytes together — the listing is part of the byte-bounded section. Attachments and non-.md files are never listed.

Seeded memories

Most memories are pointers: the model reads the index, decides one is relevant, and opens the file. A few are facts the model should hold before it is asked anything. Mark those in the memory's frontmatter:

---
name: deploy-ritual
description: the release steps that must not be reordered
metadata:
  seed: true
---

Never reorder the migration step ahead of the backup step.

At each session start, every seeded memory the model cannot currently read is injected into the conversation as a user/message carrying this plugin's source and the recall context form. Only metadata.seed selects a memory; a top-level seed key is ignored.

Injection is deduplicated against the durable session log, not against an in-process variable, so it survives restarts and resumes. Three states are distinguished:

State of a seeded memory What happens at session start
Never injected in this session Injected
Injected and still model-visible Not injected again
Injected, then hidden by compaction Injected again — the model can no longer read the earlier copy

Messages already queued for the next step count as injected, which closes the window between agent.inject() and the step that commits the message to the log.

Seeded bodies share the seedMaxBytes budget in listing order. A memory that would exceed the remaining budget is skipped whole; a later, smaller one may still fit.

On-demand recall

A memory that is not seeded still enters the conversation when the current step is about it. Declare what recalls it:

---
name: deploy-ritual
metadata:
  triggers:
    - deploy
    - release checklist
---

Never reorder the migration step ahead of the backup step.

At each step, the text entering that step is matched against every memory's triggers. Matching is literal substring matching — case-insensitive by default, and non-ASCII triggers work as-is. A memory with no declared triggers falls back to two strings it already has: its frontmatter name and its file name without the extension.

Recalled memories are appended after the step's own messages, so the prompt being answered comes first. They are deduplicated exactly as seeds are, through the same three states and the same key space: a memory that was seeded at session start is not recalled again while it is still visible.

Text this plugin injected itself is excluded from matching, so one memory cannot drag in another with no user intent behind it.

Refresh semantics

The directory is read once at plugin load, then re-read at each agent/session-start. That snapshot is what the injected section and the seeded bodies come from, so edits to the index, to the file set, or to a seeded memory become visible at the next session start. A failed refresh keeps serving the last good snapshot and logs a warning.

Recall differs on purpose: which memories are recallable comes from the snapshot, but a recalled body is read from disk at the moment it is recalled. Editing a memory mid-session therefore changes what a later recall carries, while a copy already injected stays as it was — the log records what the model actually read, not what the file says now. A memory deleted after the snapshot is simply not recalled; the step proceeds without it.

Model Experience

Memory index section

What the model sees

One system prompt section named markdown-memory:index at order 120:

# Long-term memory

A persistent memory directory is mounted at: /home/user/memory
Each memory is one markdown file with YAML frontmatter (`name`, `description`) followed by the fact.
The index below lists one line per memory. When an index line is relevant to the current task,
read that memory file with your file tools before relying on it. `[[name]]` references link to
the memory file whose frontmatter `name` matches.

## Index (MEMORY.md)

- [Project layout](project-layout.md) — where each subsystem lives
- [Deploy ritual](deploy-ritual.md) — the release steps that must not be reordered
- [Alpha](projects/alpha.md) — the one project with a non-standard release

## Memory files (3)

Paths are relative to the memory directory above.

deploy-ritual.md, project-layout.md, projects/alpha.md

Token effect

The section costs the index file size plus a fixed header and the file listing, on every request, bounded by maxBytes (default 32 KiB ≈ 8k tokens; a typical index is far smaller). The listing grows with the vault: at the maxFiles default of 500, paths alone run roughly 1–2k tokens, so a large vault should be budgeted through maxFiles rather than left to maxBytes truncation. Memory file bodies cost nothing until the model chooses to read one.

KV Cache effect

The section lives in the system prompt and its text is stable across requests within a session, so it extends the reusable request prefix rather than breaking it. When a session-start refresh picks up changed content, the prefix diverges once at the next request and is stable again afterwards.

Seeded memories

What the model sees

One user-role message per seeded memory, before the first turn:

Memory: deploy-ritual.md

Never reorder the migration step ahead of the backup step.

The first line is the memory's identity and is deliberately model-visible: the durable message source carries no per-message fields, so the log records which memory a message holds only through the text the model reads.

Token effect

Each seeded body enters the conversation once and is re-sent with every later request in the session, as all conversation history is. It costs nothing when no memory is marked. seedMaxBytes (default 16 KiB ≈ 4k tokens) bounds the total, and the bound drops whole memories rather than half of one.

KV Cache effect

Seeds append to the conversation rather than rewriting it, so they extend the reusable prefix instead of breaking it. A seed injected before the first request is part of the prefix for the whole session. After a compaction rewrites the history, a re-injected seed appends to the new tail; the prefix break belongs to the compaction, not to this plugin.

Recalled memories

What the model sees

The same shape a seed uses, appended to the messages of the step that triggered it:

Memory: deploy-ritual.md

Never reorder the migration step ahead of the backup step.

Token effect

A recalled body enters the request that triggered it and is re-sent with every later request in the session. At most maxRecallPerStep memories enter one step, so the worst case per step is that many bodies. Steps whose text matches no trigger cost nothing.

KV Cache effect

Recall appends to the conversation, so the prefix built by earlier requests stays reusable and the recalled body extends it. Because recall happens at pre-step, the memory is part of the request that needed it rather than of the system prompt, which keeps unmatched sessions from carrying it at all.

Testing

pnpm test        # builds lib/, then runs unit tests and the Loader smoke
pnpm test:unit   # unit tests only

The smoke boots the built lib/ through a real cordis.yml and a real Loader in a separate process, drives two turns against a deterministic adapter, and asserts against the persisted JSONL session log: that the seed and the recall each appear exactly once with this plugin's source, that the seed precedes the first model request, and that the module exposes no default export. That last check is not cosmetic — a default export makes the Loader's unwrapExports treat it as the module body and drop inject, which fails the boot with cannot get property "systemPrompt" without inject.

Known Limitations and Deferred Work

  • Host filesystem only — reads use node:fs directly, not the ctx.fs provider seam; remote or sandboxed filesystem products are not covered.
  • No live watching — changes are picked up at session start, not mid-session; there is no fs.watch integration.
  • Triggers are literal, not semantic — recall matches declared strings as substrings; a paraphrase that shares no trigger string does not recall the memory. There is no embedding or synonym expansion.
  • Untriggered memories rarely recall themselves — the fallback is the memory's own two names, which a natural question seldom contains verbatim. Declaring metadata.triggers is what makes recall work.
  • The per-step recall bound is silent — when more memories match than maxRecallPerStep allows, the extras are skipped without telling the model, because a notice would cost tokens in the step it is trying to protect.
  • Seeding is all-or-nothing per memory — a memory is either injected at every session start or never; there is no per-task or per-agent seed set.
  • No write path — the model cannot create or update memories yet; a guarded write tool is planned after recall.

License

MIT

CLASSIFICATION EVIDENCE

分类依据

项目类型插件
功能分类Agent 与会话
规则置信度

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: context-engineering、memory。