dsh-memory-protocol v1

August 16, 2026 · View on GitHub

Status: community rehearsal — a candidate shape for an official ctx.memory seam. The normative machine-readable schema is schemas/dsh-memory-protocol-v1.schema.json; the conformance suite lives in test/protocol-conformance/. 中文版见 protocol-v1.zh.md

dsh-memory-protocol/v1 is an interoperability protocol for bounded, layered, approval-gated, auditable cross-session memory in DeepSeek Harness. dsh-memento is the reference implementation; any other memory plugin can claim conformance by implementing the same provider surface and passing the same conformance suite.

Design anchors (non-negotiable for any conforming provider):

  • The write gate lives inside the service, not the tool layer. Every write path (add/replace/remove/consolidate/seed) is forced through an approval transport inside the provider. No model path can bypass it (the Hermes issue #48181 lesson).
  • Model-visible ⟺ reconstructable. Any write must be reconstructable from audit evidence: the approval pair (approval/asked with the full payload + approval/decided with the outcome) plus a provider-side audit ledger. Denied writes leave a denied row too.
  • Local-first. Zero network, zero credentials; storage is a local file owned by the user.
  • Bounded and honest. Hard per-track/per-layer character budgets; over-budget writes fail with a structured error. Never truncate, never silently drop.

1. Protocol identity and versioning

FieldValue
Protocol iddsh-memory-protocol
Version1 (URI form: dsh-memory-protocol/v1)
Entry schema versionstored per entry (version, starts at 1, increments on each replace)
Store schema versionmonotonic integer (SCHEMA_VERSION = 4 in the reference provider)
Export envelope{plugin: "dsh-memento", schema: "memory-export-v1", …}

Rules:

  • The protocol version changes only when the contract changes (new required fields, new error semantics). Additive optional fields do not bump the protocol version.
  • Store schema versions migrate forward step-by-step; a store with a newer version than the provider understands is rejected loudly (STORE_UNSUPPORTED_VERSION) — never read blindly, never downgrade silently.
  • Entries carry their own version so audit trails can reconstruct the evolution history of one id without diffing text.

2. Entry model

An entry is the protocol's unit of memory:

FieldTypeMeaning
idstring (UUID v4)stable cross-session identity, generated by the provider
trackuser | agentuser = facts about the user (preferences, style, landmines); agent = environment facts, conventions, lessons
scopeuser-global | workspaceuser-global applies to every workspace; workspace applies only to the session's normalized cwd
workspaceKeystringnormalized absolute cwd key for workspace entries; '' for user-global (Windows case-insensitive)
agentKeystringnormalized agentPreset key; '' = the shared layer visible to every agent
textstring, non-emptythe memory content; budget counting unit = JS string length
sourcestringprovenance label (dsh-memento, memory-tool, claude, an adapter id, …)
tagsstring[]short labels, ≤16 entries, each ≤32 chars, trimmed, deduped, no control chars
versioninteger ≥ 1starts at 1; replace increments it; consolidate/seed/import create fresh version-1 entries
createdAt / updatedAtinteger (epoch ms)updatedAt >= createdAt
lastRecalledinteger | nulllast query hit time (epoch ms)
recallCountinteger ≥ 0query hit count (ranking: high frequency = important)
sessionIdstring | nullid of the session that last wrote the entry

Visibility: a session sees (and replace/remove/consolidate can only target) the shared layer (agentKey === '') plus entries of its own agentPreset, and workspace entries only for its own cwd. Management surfaces (commands, panels) and explicit agent-less provider calls keep the full cross-agent view.

3. Write operations

All writes share one pipeline — budget pre-check → approval transport → budget re-check → atomic persist → audit row — and every failure leaves zero partial writes.

OperationInputSemantics
addentry inputinserts a new entry (fresh id, version 1). Not deduplicated by value: duplicate text is legal; consolidation is the caller's tool.
replaceunique substring match, new text, optional tagsrewrites exactly one entry located by a case-insensitive unique substring of its text. Id is stable; version increments; tags update when provided, otherwise preserved.
removeunique substring matchdeletes exactly one entry located by a unique substring.
consolidate1..20 matches, new text, optional tagsatomically deletes all targets and inserts one new entry (version 1) — one approval, one transaction.
seedentry input listbatch insert under one approval; all-or-nothing (any entry over budget rejects the whole batch); fresh ids and version 1 per entry.

Idempotency and conflict arbitration:

  • replace/remove/consolidate are conditional writes keyed by unique-substring matching: re-running the same operation after success fails with ENTRY_NOT_FOUND (the match no longer exists), so double-apply cannot happen by retry. Zero hits → ENTRY_NOT_FOUND; multiple hits → AMBIGUOUS_MATCH with the candidate count and text samples — the caller must supply a longer, unique substring.
  • The authoritative target is re-resolved after approval returns (concurrent writers may have changed the store during the approval wait); the final budget check and the mutation happen with no await between them, so there is no stale-write window.
  • consolidate resolves all targets inside one transaction: any mismatch rolls the whole operation back.

Approval payloads (approve-what-you-see): the approval request carries the complete change, never an abstract action: add/seed carry the new text; replace carries from: (full previous entry) + to: (new text); remove carries the full text being deleted; consolidate carries each target's resolved text (300-char excerpt cap per target) + the new text.

4. Read operations

  • query(filter?, opts?) — substring search (case-insensitive ASCII fold; correct for CJK). No approval. Options: track / scope / text / limit (provider hard-caps at 1000), opts.sessionId (records a recalled audit row), opts.agentKey (session-visibility filter).
  • budgets() — per-track×scope usage report ({track, scope, used, limit} rows).
  • Ranking: entries that hit a query get recallCount + 1 and lastRecalled updated; query results order by recall_count DESC, updated_at DESC.

5. Budget model

  • Hard character budgets per track × scope (reference defaults: user 2000 / agent 4000 per layer). Budgets count text only — tags and metadata are outside the budget.
  • An over-budget write fails with BUDGET_EXCEEDED carrying {track, scope, used, limit, needed}; the caller consolidates/removes and retries. Never truncate, never auto-compact.
  • seed pre-checks the whole batch; any single entry over budget rejects the entire batch before anything is written.

6. Audit and reconstruction

  • Every allowed write lands one audit row: {seq, ts, action, track, scope, entryId, text, outcome, source, sessionId}. outcome names the real decision source (allowed-once (via approval, writePolicy ask) / … (via write gate)).
  • Every denied/cancelled/unavailable write lands a <action>-denied row before the WRITE_DENIED error propagates — turn-outside gate paths have no approval audit pair, so the denied row is the sole evidence chain there.
  • Read recalls land recalled rows; injected snapshots land snapshot rows whose text is byte-identical to what the model saw.
  • Together with the approval pair (approval/asked full payload + approval/decided outcome), any state change is reconstructable from the session log + the provider audit ledger.

7. Error codes

Structured errors expose a stable code; tools and models branch on the code, not the message (messages stay English across languages by design — they are audit contracts).

CodeTriggerDetails
INVALID_INPUTbad track/scope/text/tags/match/matches/envelope
WRITE_REQUIRES_AGENTwrite without an owning agent session
BUDGET_EXCEEDEDover-budget write or seed batchtrack, scope, used, limit, needed
ENTRY_NOT_FOUNDzero-hit matchtrack, scope, match
AMBIGUOUS_MATCHmulti-hit matchcandidates, sample
WRITE_DENIEDrejected/cancelled/unavailable approvaloutcome
PROPOSAL_NOT_FOUNDproposal decision on non-pending idid
STORE_CORRUPT / STORE_UNSUPPORTED_VERSIONunreadable store / newer schemapath
ADAPTER_NOT_FOUND / ADAPTER_PAYLOADunknown adapter id / unconvertible payloadadapterId

8. Import / export envelope

  • /memory export produces one JSON document {plugin: "dsh-memento", schema: "memory-export-v1", exportedAt, budgets, entries} — a complete backup/migration round-trip. Export is read-only (no approval, no audit row).
  • /memory import (and import --adapter=<id>) restores entries through seed — one approval, full budget pre-check, one atomic transaction, per-entry audit rows. Imported entries get fresh ids/timestamps, version 1, reset recall counts; unknown envelope schema versions are rejected loudly; one import is capped at 1000 entries.

9. Adapter registry (ctx.memoryAdapters)

Third-party memory plugins adapt their own store into the protocol by registering an adapter (register(adapter) returns a disposer — registration is reversible and belongs to the plugin's own ctx.effect). An adapter is a pure data converter: adapt(payload) → {entries} and export(entries) → payload; it never runs model extraction. Unknown adapter ids fail with ADAPTER_NOT_FOUND; unconvertible payloads fail with ADAPTER_PAYLOAD. Reference adapters ship with dsh-memento: mem0, hermes-memory-md, claude-code-memory-md. See adapters-guide.md.

10. Conformance

Any provider claiming dsh-memory-protocol/v1 compatibility implements the provider surface in test/protocol-conformance/README.md and passes the conformance suite — the same cases dsh-memento's own provider runs as the golden reference in CI. The suite is distributable (copies run against any provider factory) and self-contained (node:assert only).

11. Relationship to the official seam

The protocol is a normalization and extension of dsh-memento's existing ctx.memory seam — not a rewrite; every behavior in 0.3.x remains compatible. What it adds on top: per-entry tags/version, a machine-readable JSON Schema, a distributable conformance suite, and the adapter registry. Why the official seam should adopt it, and the migration path, is argued in upstream-proposal.md.