dsh-native-memory
August 16, 2026 · View on GitHub
Status: approved for implementation (prepared 2026-08-15, verified against deepseek-harness 0.1.0-rc.5 @ commit 47f9438).
1. Problem statement
DeepSeek Harness persists every session's event log and offers manual
cross-session references, but it has no automatic long-term memory: a new
session starts from an empty log. The community answer so far
(dsh-hermes-memory) works but was evaluated in this environment and found
wanting on four counts:
| Weakness of dsh-hermes-memory | How this design answers it |
|---|---|
| User-global memory, no project scoping; cross-project contamination | Per-workspace memory domains, exact-cwd authorization |
| Model writes are injected verbatim into future prompts — a standing prompt-injection vector | Every write passes the host approval stack; injected text is framed as untrusted data |
| Hard total caps, everything always injected, no retrieval | Bounded always-on profile + on-demand deterministic recall + FTS over past sessions |
Persists into settings.yaml; skills inflate one YAML file | Dedicated storage-domain unit (~/.dsh/storages/dsh_memory.json), no settings coupling |
The name states the strategy: native — every capability rides a seam the harness already ships (storage-domain, session-query, approval, tools, systemPrompt). No external server, no extra runtime dependency, no vendored imports, no custom SQLite.
2. Planes
Memory crosses sessions, so by the harness's own plane rule
("anything crossing sessions stays host-side") the plugin row lives in the
HOST composition, contributed by the bundle patch — exactly like
session-persistence-jsonl and tool-todo in dsh-base. The plugin publishes
no service, only consumes host registries, so no isolate realm is needed.
No agent preset is shipped: tools are available to every session, and the
prompt profile is toggled by config (injectProfile), which a user's own
cordis.patch.yml can override per deployment.
3. Data model (storage-domain)
One domain, JSON backend (web profile default), unit file
~/.dsh/storages/dsh_memory.json:
global:{ initialized: true }— creation marker.- table
facts— keyid(uuid). Value:{ id, workspacePath, kind, text, tags[], sessionId, seq, createdAt, updatedAt, accessCount?, lastAccessedAt?, state }.kind ∈ {preference, fact, convention, decision};accessCount/lastAccessedAt(v0.3.0, optional — rows written before them parse unchanged) feed the within-tier recall tie-breaks. - table
profiles— keyworkspacePath. Value:{ workspacePath, entries[], updatedAt }— the always-injected, bounded workspace profile. - table
proposals(v0.3.0) — keyid. Pending LLM-distilled candidates; consumed by the approval-gatedmemory_rememberon exact-text match, expired by TTL. - table
alarms(v0.3.0) — keyid. Compaction drift alarms (dropped anchors), rendered as verify-data, expired by TTL.
Zod v4 schemas (matching DSH's own storage-domain dependency). Validation at the durable boundary is the facility's job; writes await backend durability before mutating memory.
Provenance: every fact carries (sessionId, seq) — cited memory,
reconstructable from the lossless session log. This is the audit trail
behind the approval gate: ask/outcome pairs land on the requesting session's
log, and each fact names the session and log position that justified it.
Caps (config): facts ≤ 300/workspace, fact ≤ 2000 chars, profile ≤ 8 entries × 240 chars. Recall over the facts table is a bounded in-memory scan (the JSON backend has no FTS) — deterministic, zero-token-waste, and the cap keeps it cheap. Proposal and alarm caps are per workspace, oldest-first.
Write amplification (accepted): memory_recall bumps the access
counters of every returned fact (up to 50 metadata-only puts per call on the
JSON backend); best-effort — a failed counter write never fails the read.
Soft deletes: facts/proposals/alarms are archived/expired in place, never
hard-deleted, so the tables grow slowly over time; accepted for
reconstructability (provenance stays in the session log and the domain unit
keeps the row).
4. Recall paths
- Always-on profile —
systemPromptsection, order 88, provider form. Renders only the caller session's workspace profile, framed as untrusted persisted notes. ~600 tokens worst case, typically far less. memory_recall— read-only scan over active facts of the caller's workspace. Deterministic three-tier keyword scoring (v0.2.0): exact tag match > case-insensitive text substring (CJK bigram tokenization, whole- query boost) > fuzzy tag overlap; recency tiebreak. Within one tier (v0.3.0), freshness (updatedAt age: fresh ≤recallFreshWindowDays, current ≤recallStaleWindowDays, else stale, weights 1.0/0.8/0.5) and access frequency (accessCount/10, capped at 1) break ties — each signal contributes at most 0.4, so a lower text tier can never outrank a higher one. Recall bumps the access counters through a metadata-only write-back (content fields untouched — no approval). Literal, not semantic — cross-language queries need the query language to match the fact's.memory_search— FTS over past sessions viactx.sessionQuery.searchSessionswith an exact-cwd session filter (the same API and authorization rule dsh-tool-session-query'ssession_searchapplies — §9's earliersearchEventsname is the within-one-session variant, not the cross-session shape). Requires the bundle's FTS patch (below).memory_profile— read the workspace profile; propose changes, then land them throughmemory_remember(gated).- Browser page (v0.3.0) — a read-only
settings.sectionpage served by the client half (./clientexport). It readsGET /dsh-native-memory/factsfrom the host's optionalwebServer(packages/host/webserver), listing every workspace's active facts with secrets masked server-side. The page never writes: the approval service refuses requests outside an open turn (packages/interaction/user-approval src/index.ts:261) and a web request carries no agent, so deletions stay in the chat — the page copies amemory_forget id: "…"instruction and the tool's own approval gate does the human check. - Session-end proposals (v0.3.0, OFF by default —
proposeOnSessionEnd) — onsession/disposed, one bounded transcript (16k chars tail) is distilled with ONE cheap LLM call (ctx.llm.prepareCall→stream, provider/model from config) into ≤8 candidate facts. Candidates land in the proposals table as PENDING and are rendered in the next sessions' prompt (≤3 shown); they become facts ONLY through the approval-gatedmemory_remember(exact-text match consumes the proposal). The LLM proposes, the human approves — the半自动 stance holds; storing a proposal needs no approval because it is not memory content yet (recall and the profile never see it). - Compaction drift guard (v0.3.0, ON by default —
compactionGuard) — onsession/eventforcompaction/summaryevents (packages/compaction/compaction-basic/src/region.ts:442+), re-derive the shadowed turns' text by seq, extract deterministic literal anchors (quoted literals, path-like runs, key=value pairs, error tokens), and record the ones the summary dropped as bounded alarms (≤5 anchors, ≤3 alarms, 24h TTL). Rendered in the next sessions' prompt as DATA to verify — deterministic, zero LLM, zero extra model call.
5. Tools
memory_remember / memory_edit / memory_forget — writes, approval-gated
when approvalWrites: true (default). memory_recall / memory_search /
memory_profile — reads, never gated. memory_consolidate (v0.2.0) —
read-only near-duplicate merge suggestions plus the remaining cap budget;
merges land through the gated edit/forget tools. memory_import (v0.2.0) —
import candidate facts from a past session's log by literal query match, one
approval ask per fact, stored with the original (sessionId, seq) provenance.
memory_expand (v0.3.0) — read-only: expand one fact's citation (or an
explicit session_id + seq) back to the original session-log excerpt around
the cited event, exact-cwd authorized, zero LLM. memory_export (v0.3.0) —
read-only projection: writes a git-friendly, secret-masked Markdown mirror
to <cwd>/.dsh-memory/memory.md, deterministic and idempotent
(content-addressed, atomic temp+rename); the storage domain stays the single
source of truth and the file is never synced back, so no approval is
involved — the export exposes nothing memory_recall / memory_search could
not already reveal.
Tool count trades prompt cost against capability; keep the set closed until usage data says otherwise.
6. Bundle patch (cordis.patch.yml)
Two entries (a patch replaces whole configs; the user's layer wins):
session-query-sqlite→{ path: dshHomePath('storages/session-search.sqlite'), openAt: first-search }— enables FTS (shipped default:openAt: never,:memory:).insert→ thedsh-native-memoryplugin row with the config block.
dshHomePath and !!js are available to bundle patches (dsh-base uses both).
7. Failure & degradation
storageDomainabsent (headless profile) → plugin stays mounted, memory tools answer a disabled error; prompt section omitted. No hang, no crash.sessionQueryabsent or search disabled →memory_searchreportsSESSION_QUERY_SEARCH_DISABLED; recall/profile keep working.approvalabsent or answerers unavailable → writes fail closed.- Domain version mismatch → loud error at open; memory offline until migrated.
8. Security model
- Writes: human-approved, session-log audited.
- Reads: exact-cwd workspace authorization — a session can only touch facts and profiles for its own workspace path.
- Injection: bounded, framed as data, order 88; hardening backlog below.
- Secret redaction (v0.3.0): every content write runs the deterministic
secret detectors FIRST (before caps and before the approval ask) under
secretPolicy: reject|mask|off(default reject — the write fails with MEMORY_SECRET_REJECTED, and the error names kinds only, never the secret). Injection and tool echo are masked ALWAYS, independent of the policy, so a row stored underoff(or by an older version) never re-enters the model context verbatim. - The bundle runs third-party code with the user's own permissions — README must carry the same warning the awesome list requires.
9. Verified compatibility facts (0.1.0-rc.5 @ 47f9438)
ctx.tools.registerToolDefinition: name/description/parameters,output: {schema, render},execute(args, exec)— packages/core/tools/src/index.ts:222.ctx.systemPrompt.section({name, order, text}), text accepts a provider — packages/core/system-prompt/src/index.ts:53.ctx.storageDomain.open({name, version, tables}),domainTable(schema)zod v4 — packages/storage/storage-domain.ctx.approval.request({agent, toolName, callId?, reason?})— packages/interaction/user-approval/src/index.ts:153.ctx.sessionQuery.searchSessions+SESSION_QUERY_SEARCH_DISABLED— packages/session-query; sqlite configopenAt: startup|first-search|never.- Bundle mechanism:
"dsh": {"bundle": {"patch": "./cordis.patch.yml"}}, installed viadsh plugin add(npm / git / path / tarball), joined todsh.profile.bundles— apps/cli/src/plugin.ts. - External PRs into deepseek-harness are NOT accepted (CONTRIBUTING.md);
ecosystem route: standalone repo +
dsh-plugintopic + awesome-dsh-plugin.
10. Competitive landscape (awesome-dsh-plugin "Memory" section)
Distinctive claim among ~20 memory plugins: the only zero-dependency, per-workspace, approval-gated, cited memory built entirely on the harness's own persistence seams — no external server (vs dsh-mnemon, sgme, memoria), no custom SQLite of its own (vs dsh-memento, dsh-mneme), not user-global (vs dsh-hermes-memory, dsh-memory-vault), no settings.yaml bloat.
11. Roadmap
- v0.1.0 (this repo): domain + tools + approval gate + profile section + bundle patch + tests + docs. Local verification on this environment first (see docs/handoff.md), then community release.
- v0.2.0 (shipped):
<memory-profile>framing hardening (delimiter tags,\u003cescaping, mirror session-reference),memory_consolidate,memory_import, three-tier recall scoring with CJK bigrams. - v0.3.0 (shipped):
memory_expand(citation → original excerpt), within-tier freshness/access recall signals, secret redaction (write policy + always-on injection masking), read-only browser settings page, opt-in session-end proposals (LLM proposes, human approves), compaction drift guard,memory_exportMarkdown mirror. - Later: semantic recall tier (opt-in embeddings) — deliberately deferred; cross-workspace/multi-agent sharing — out of scope by design.
12. Contribution path (target: DeepSeek community)
- Publish
dsh-native-memoryto npm. - GitHub repo +
dsh-plugintopic. - PR to awesome-dsh-plugin (one line each in README.md + README.zh.md).
- Optional: dsh-market listing, discussion #525-style announcement in deepseek-harness Discussions.