OpenViking
volcengine
Self-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:SYMlp/dsh-markdown-memory
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
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.
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.
| 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.
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
| 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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
node:fs directly, not the ctx.fs provider seam; remote or sandboxed filesystem products are not covered.fs.watch integration.metadata.triggers is what makes recall work.maxRecallPerStep allows, the extras are skipped without telling the model, because a notice would cost tokens in the step it is trying to protect.MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: context-engineering、memory。