dsh-memory is an installable memory bundle: it watches the conversation for signals, distills what is worth keeping into a store, and hands the relevant memories back to the model in every future session. This page tears down the full pipeline — from capture and storage to retrieval, injection, and decay.
Memory is not a static table — it is a loop with intake, outflow, and decay.
Every session produces signals — a "remember this", a correction, a debugging session where repeated failures finally succeed. dsh-memory collects those signals, distills them into entries via an LLM, and pushes them through a security gate into a durable KV store; in the next session, the retrieval plane hands the relevant ones back into the context. Memories that keep getting used stay fresh; the neglected ones quietly fade out.
In practice it is seven Cordis composition rows: six functional plugins, each with one job — memory-store opens the storage domain and registers the ctx.memory service; tool-memory exposes the nine model tools; memory-review drives automatic extraction; memory-context manages system-prompt injection; memory-notes renders conventions and pitfalls into the project-notes prompt section (it writes no files into your repo); memory-remote backs the memory manager in the settings UI (remote writes default to off and must be enabled explicitly) — plus a no-op memory-root entry that exists only for client-module discovery. The storage medium is a single file, ~/.dsh/storages/memory.json — uninstalling the plugins never loses your memories.
A purely synchronous projection accumulator folds the session event stream — and only disturbs the LLM once enough signal has accumulated.
Every user message and tool result flows through a zero-LLM-cost projection accumulator (session-projection). It collects just three kinds of candidate fragments — the collection layer deliberately keeps the funnel wide; the conservatism about what is worth keeping is the extraction prompt's job: a missed signal costs a little, a false one costs almost nothing.
The user directly asks to remember. Patterns like "remember" / "from now on" / "going forward" put the message into the candidate pool.
The user overturns an earlier statement. "No, actually…" / "I meant…" — a correction means the old belief was wrong, and the new conclusion is worth keeping.
The same tool call fails repeatedly (default ≥ 2, same signature) and then finally succeeds — that success is a verified workaround, distilled into a pitfall entry.
Two flush paths guarantee no signal is lost: compaction end (compaction/end) extracts from the raw events that compaction shadowed; session dispose (session/disposed) runs one final extraction over the full un-compacted session (5 s timeout, fire-and-forget).
Periodic review, compaction flush, dispose flush, and the low-frequency curator share one 20-extraction-per-session budget: each drain counts as 1 — the multiple LLM calls inside a batch and the dedup judge calls are not counted; set 0 for unlimited. Once exhausted, no more automatic extraction in this session. That is the hard cost ceiling.
The extraction prompt is itself a gate: anything the repository already records, ephemeral state and unverified hypotheses, workflows not validated by a tool are all excluded; preference entries require an explicit user request or repeated topic recurrence. Date prefixes are stripped at the parse layer — timestamps always come from the program; output follows the scope: content line protocol, fragments are flattened to single lines first, so forged lines cannot pollute the protocol.
Every write — whether from a model tool or from background extraction — must pass through the same gate.
The main entry table. Reads hit the in-memory authoritative state synchronously; writes are serialized on the domain write chain, applied to the backend before the in-memory copy updates. Writing past the soft cap (entriesCap, configurable) evicts by use signal: pinned entries are exempt, the lowest accessCount / lastRecalledAt goes first; if everything is exempt the table may exceed the cap — healthy data is never hard-deleted.
One audit row per successful change: the operation, the source (tool/review/flush/ui/janitor), a monotonic seq, and a content preview. Who wrote what, and when — always knowable.
The pending queue in human-review mode. A suggestion is not a memory — not injected, not searched, not decayed; it only waits for a human verdict (see 06).
The store ships one more layer: cross-process single-writer detection. The host storage backend assumes one writer per process — two DSH processes sharing a storage root each hold their own in-memory authoritative state and republish the whole unit file, silently clobbering each other. So on every boot, memory-store writes an owner stamp (pid + bootId) into the domain's global slot and re-reads it periodically: if its own stamp comes back as someone else's, a concurrent publisher exists and is reported at once. It detects, never locks — a foreign stamp whose pid is dead, or one closed cleanly, is no live threat, so restarts stay silent.
No vector store, no embedding model — a ~270-line dependency-free Okapi BM25 with CJK-aware tokenization and a conservative Latin stemmer.
Retrieval in two steps: structured filtering first (scope / category / projectName), then BM25 scoring over the surviving candidates. The tokenizer splits Latin text by word and strips one conservative rule suffix to collapse inflections — uses→use, studies→study, while stems like class and basis are never damaged (one shared tokenizer for documents and queries); for CJK it emits single-character unigrams plus adjacent bigrams — the bigrams tighten matches, the unigrams keep single-character queries recallable. Summaries join the index alongside content, so a short summary recalls the entry too. Ranking proceeds by: relevance ↓, pinned ↓, recency ↓.
How memory enters the context decides every token it saves the model.
Each session freezes a memory snapshot at creation time and reuses it for the whole session — that keeps the KV-cache prefix stable: same prefix, full cache hits. The only moment it is broken is compaction (the prompt is being rebuilt anyway), and memories learned mid-session are injected exactly there. The snapshot has a dual budget: 5000 characters + 20 entries, with a trailing ≈N tokens estimate so injection cost is always visible.
Index mode — the factory default since 0.8 — is the core of progressive disclosure: each entry renders as a one-line existence index (scope/category · id · summary), ordered by the project → user → global relevance tier; when the budget runs out, the tail collapses into per-category counts (project/convention ×12) — index size grows with the number of categories, not entries. The model sees "what exists" and fetches full text with memory_get on demand.
Optional step-level auto recall takes a different road: on each agent step, a BM25 search keyed on that step's user text appends the hit entries as a fenced <recalled-memory> user message — the system prompt is untouched, the prefix cache does not move a single byte. On load, corrected entries carry a conflict annotation (⚠ conflicts with a newer correction) so the model weighs old vs. new itself.
The janitor patrols on every session creation. Drag the timeline to watch three entries meet different fates.
"Deploy script lives in scripts/deploy.sh, port 8080"
"Behind a proxy, pnpm needs the registry mirror set first"
"Review comments must give a reason per item"
The counterpart to decay is distillation: every 20 sessions, the curator picks oversized entries (≥ 400 chars) — at most 5 per pass — and has the LLM rewrite them into a concise single line; low-frequency, budget-constrained, and it can enter the human-review queue too. The memory store thus forgets, and it distills.
Memories get read back into future contexts — so the store is itself an attack surface, and must be defended on both sides.
[BLOCKED: …] placeholderFlip on confirmBeforeWrite and the system's trust model changes completely: every extraction and tool write is downgraded to a proposal in the pending queue. The same proposal observed repeatedly accumulates hits and floats to the top — frequency is the signal. A human can edit before adopting, or reject with one click; a proposed change to an existing entry never touches the original before adoption. The model never self-promotes.
Finally, memories flow back into every session: convention / preference entries and the failure / procedure / tool-quirk pitfall log render into the project-notes system-prompt section. Nothing is written into your repository — memory lives in the host-side store and is managed in the Memory settings UI (since 0.6; the ≤0.5.x in-repo notes files are cleaned up automatically). Entries already rendered into notes are excluded from the memory section — the same content never appears twice in the prompt. The store is the source of truth; the notes section is its read-only projection.
Proposals observed repeatedly accumulate hits and float up; entries recalled repeatedly stay fresh automatically. The trail of use is the most important metadata.
The snapshot freezes the session for the KV-cache — rather than jitter the prefix, it accepts one beat of delay: what cache hits save far outweighs the immediate gain.
Every injection surface declares: the current user request, the repo files, and tool outputs always outrank memory. On conflict, trust the present.
37 scan rules, 200-row rolling caps on audit and suggestions, a 500-entry soft cap, an optional human-review queue — a memory system must always be able to answer "who wrote this memory, when, and why".