# 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 `<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).

| 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=<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](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).
