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-memoryHow this design answers it
User-global memory, no project scoping; cross-project contaminationPer-workspace memory domains, exact-cwd authorization
Model writes are injected verbatim into future prompts — a standing prompt-injection vectorEvery write passes the host approval stack; injected text is framed as untrusted data
Hard total caps, everything always injected, no retrievalBounded always-on profile + on-demand deterministic recall + FTS over past sessions
Persists into settings.yaml; skills inflate one YAML fileDedicated 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 — key id (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 — key workspacePath. Value: { workspacePath, entries[], updatedAt } — the always-injected, bounded workspace profile.
  • table proposals (v0.3.0) — key id. Pending LLM-distilled candidates; consumed by the approval-gated memory_remember on exact-text match, expired by TTL.
  • table alarms (v0.3.0) — key id. 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

  1. Always-on profilesystemPrompt section, order 88, provider form. Renders only the caller session's workspace profile, framed as untrusted persisted notes. ~600 tokens worst case, typically far less.
  2. 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.
  3. memory_search — FTS over past sessions via ctx.sessionQuery.searchSessions with an exact-cwd session filter (the same API and authorization rule dsh-tool-session-query's session_search applies — §9's earlier searchEvents name is the within-one-session variant, not the cross-session shape). Requires the bundle's FTS patch (below).
  4. memory_profile — read the workspace profile; propose changes, then land them through memory_remember (gated).
  5. Browser page (v0.3.0) — a read-only settings.section page served by the client half (./client export). It reads GET /dsh-native-memory/facts from the host's optional webServer (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 a memory_forget id: "…" instruction and the tool's own approval gate does the human check.
  6. Session-end proposals (v0.3.0, OFF by default — proposeOnSessionEnd) — on session/disposed, one bounded transcript (16k chars tail) is distilled with ONE cheap LLM call (ctx.llm.prepareCallstream, 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-gated memory_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).
  7. Compaction drift guard (v0.3.0, ON by default — compactionGuard) — on session/event for compaction/summary events (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 → the dsh-native-memory plugin row with the config block.

dshHomePath and !!js are available to bundle patches (dsh-base uses both).

7. Failure & degradation

  • storageDomain absent (headless profile) → plugin stays mounted, memory tools answer a disabled error; prompt section omitted. No hang, no crash.
  • sessionQuery absent or search disabled → memory_search reports SESSION_QUERY_SEARCH_DISABLED; recall/profile keep working.
  • approval absent 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 under off (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.register ToolDefinition: 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 config openAt: startup|first-search|never.
  • Bundle mechanism: "dsh": {"bundle": {"patch": "./cordis.patch.yml"}}, installed via dsh plugin add (npm / git / path / tarball), joined to dsh.profile.bundles — apps/cli/src/plugin.ts.
  • External PRs into deepseek-harness are NOT accepted (CONTRIBUTING.md); ecosystem route: standalone repo + dsh-plugin topic + 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, \u003c escaping, 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_export Markdown 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)

  1. Publish dsh-native-memory to npm.
  2. GitHub repo + dsh-plugin topic.
  3. PR to awesome-dsh-plugin (one line each in README.md + README.zh.md).
  4. Optional: dsh-market listing, discussion #525-style announcement in deepseek-harness Discussions.