dsh-memory-protocol v1
August 16, 2026 · View on GitHub
Status: community rehearsal — a candidate shape for an official
ctx.memoryseam. The normative machine-readable schema is schemas/dsh-memory-protocol-v1.schema.json; the conformance suite lives intest/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/askedwith the full payload +approval/decidedwith 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
| Field | Value |
|---|---|
| Protocol id | dsh-memory-protocol |
| Version | 1 (URI form: dsh-memory-protocol/v1) |
| Entry schema version | stored per entry (version, starts at 1, increments on each replace) |
| Store schema version | monotonic 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
versionso 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:
| Field | Type | Meaning |
|---|---|---|
id | string (UUID v4) | stable cross-session identity, generated by the provider |
track | user | agent | user = facts about the user (preferences, style, landmines); agent = environment facts, conventions, lessons |
scope | user-global | workspace | user-global applies to every workspace; workspace applies only to the session's normalized cwd |
workspaceKey | string | normalized absolute cwd key for workspace entries; '' for user-global (Windows case-insensitive) |
agentKey | string | normalized agentPreset key; '' = the shared layer visible to every agent |
text | string, non-empty | the memory content; budget counting unit = JS string length |
source | string | provenance label (dsh-memento, memory-tool, claude, an adapter id, …) |
tags | string[] | short labels, ≤16 entries, each ≤32 chars, trimmed, deduped, no control chars |
version | integer ≥ 1 | starts at 1; replace increments it; consolidate/seed/import create fresh version-1 entries |
createdAt / updatedAt | integer (epoch ms) | updatedAt >= createdAt |
lastRecalled | integer | null | last query hit time (epoch ms) |
recallCount | integer ≥ 0 | query hit count (ranking: high frequency = important) |
sessionId | string | null | id 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.
| Operation | Input | Semantics |
|---|---|---|
add | entry input | inserts a new entry (fresh id, version 1). Not deduplicated by value: duplicate text is legal; consolidation is the caller's tool. |
replace | unique substring match, new text, optional tags | rewrites exactly one entry located by a case-insensitive unique substring of its text. Id is stable; version increments; tags update when provided, otherwise preserved. |
remove | unique substring match | deletes exactly one entry located by a unique substring. |
consolidate | 1..20 matches, new text, optional tags | atomically deletes all targets and inserts one new entry (version 1) — one approval, one transaction. |
seed | entry input list | batch 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/consolidateare conditional writes keyed by unique-substring matching: re-running the same operation after success fails withENTRY_NOT_FOUND(the match no longer exists), so double-apply cannot happen by retry. Zero hits →ENTRY_NOT_FOUND; multiple hits →AMBIGUOUS_MATCHwith 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
awaitbetween them, so there is no stale-write window. consolidateresolves 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 arecalledaudit 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 + 1andlastRecalledupdated; query results order byrecall_count DESC, updated_at DESC.
5. Budget model
- Hard character budgets per track × scope (reference defaults: user 2000 / agent 4000 per layer).
Budgets count
textonly —tagsand metadata are outside the budget. - An over-budget write fails with
BUDGET_EXCEEDEDcarrying{track, scope, used, limit, needed}; the caller consolidates/removes and retries. Never truncate, never auto-compact. seedpre-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}.outcomenames the real decision source (allowed-once (via approval, writePolicy ask)/… (via write gate)). - Every denied/cancelled/unavailable write lands a
<action>-deniedrow before theWRITE_DENIEDerror propagates — turn-outside gate paths have no approval audit pair, so the denied row is the sole evidence chain there. - Read recalls land
recalledrows; injected snapshots landsnapshotrows whose text is byte-identical to what the model saw. - Together with the approval pair (
approval/askedfull payload +approval/decidedoutcome), 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).
| Code | Trigger | Details |
|---|---|---|
INVALID_INPUT | bad track/scope/text/tags/match/matches/envelope | — |
WRITE_REQUIRES_AGENT | write without an owning agent session | — |
BUDGET_EXCEEDED | over-budget write or seed batch | track, scope, used, limit, needed |
ENTRY_NOT_FOUND | zero-hit match | track, scope, match |
AMBIGUOUS_MATCH | multi-hit match | candidates, sample |
WRITE_DENIED | rejected/cancelled/unavailable approval | outcome |
PROPOSAL_NOT_FOUND | proposal decision on non-pending id | id |
STORE_CORRUPT / STORE_UNSUPPORTED_VERSION | unreadable store / newer schema | path |
ADAPTER_NOT_FOUND / ADAPTER_PAYLOAD | unknown adapter id / unconvertible payload | adapterId |
8. Import / export envelope
/memory exportproduces 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(andimport --adapter=<id>) restores entries throughseed— one approval, full budget pre-check, one atomic transaction, per-entry audit rows. Imported entries get fresh ids/timestamps,version1, 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.