# dsh-memory-protocol v1 > 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](schemas/dsh-memory-protocol-v1.schema.json); > the conformance suite lives in [`test/protocol-conformance/`](../test/protocol-conformance/README.md). > 中文版见 [protocol-v1.zh.md](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](https://github.com/NousResearch/hermes-agent/issues/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 | 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 `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: | 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`/`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 `-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). | 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 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=`) 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](adapters-guide.md). ## 10. Conformance Any provider claiming `dsh-memory-protocol/v1` compatibility implements the provider surface in [`test/protocol-conformance/README.md`](../test/protocol-conformance/README.md) and passes [the conformance suite](../test/protocol-conformance/) — 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](upstream-proposal.md).