# Memory Portability Spec v1

**Status:** Draft v1 (v0.6.3.1) — single-implementation reference.
Multi-implementation interop is a v2.0 deliverable per ROADMAP §7.6.

**Canonical URL:** `https://alphaonedev.github.io/ai-memory-mcp/spec/v1/`

**Source of truth for the schema:** the SQLite schema in
[`src/storage/`](../../src/storage/) (formerly the monolithic
`src/db.rs`; the legacy `db::*` names survive as an alias) and
migration files in
[`migrations/sqlite/`](../../migrations/sqlite/). When this document and
the code disagree, the code wins and this document is a bug.

---

## 0. Purpose

The Memory Portability Spec defines an **export envelope** that any
ai-memory deployment can produce and any ai-memory deployment can ingest
without data loss. It exists so that:

1. Operators can switch toolchains (move a store from machine A to
   machine B, or from one OSS distribution channel to another) without
   re-running mining or losing embeddings.
2. Third parties can write tools — diff-against-prod, git-history of a
   memory store, archive auditors — that consume the envelope rather
   than scraping live SQLite.
3. The OSS spec is **byte-stable enough to round-trip**: `ai-memory
   export` followed by `ai-memory import` into a fresh DB produces a
   store that is byte-equivalent (modulo `created_at` / `updated_at`
   timestamps when the importer is told to restamp them).

This is **v1**. v2 will add multi-implementation interop tests, a
formal grammar for the embedding magic-byte family, and federation
identity attestation. None of that is in scope for v0.6.3.1.

---

## 1. Conformance levels

| Level | Conformance |
|---|---|
| **Producer-conformant** | A tool MUST emit the envelope structure in §3, MUST encode embeddings per §4, MUST stamp `schema_version="v1"`, MUST include all required tables. MAY omit optional tables it doesn't store. |
| **Consumer-conformant** | A tool MUST accept any v1 envelope. MUST preserve fields it does not understand (forward-compat). MUST reject unknown `schema_version`. SHOULD verify embedding magic byte and reject unknown values with a typed error. |
| **Round-trip-conformant** | A tool that is both producer- and consumer-conformant MUST round-trip an envelope byte-equivalently when given `--preserve-timestamps`. |

ai-memory v0.6.3.1 is producer- and consumer-conformant. Round-trip is
demonstrated by the test plan in §10.

---

## 2. Encoding choices

The envelope is delivered in **one of two encodings**:

- **JSON** (RFC 8259) — default for the HTTP `/api/v1/export` endpoint
  and the CLI `ai-memory export --format=json` command.
- **TOON** (Token-Oriented Object Notation) — opt-in for token-efficient
  transfer to LLM-driven importers; selected via `--format=toon`. See
  [TOON reference](https://www.tensorlake.ai/blog-posts/toon-vs-json) and
  [`src/toon.rs`](../../src/toon.rs).

Both encodings express the **same envelope** — the field names, types,
and semantics in this document are encoding-agnostic. JSON is the
**normative reference** for byte-equivalence in §10. Two TOON files are
considered round-trip-equivalent if and only if they decode to the same
envelope and that envelope re-encodes byte-identically as JSON.

JSON encoding rules:

- UTF-8.
- No leading or trailing whitespace at the document level.
- Object keys sorted lexicographically (so byte-equivalence is
  trivially testable).
- Numbers: integers as JSON `number` without exponent; floating point
  with at most 17 significant digits (round-trip-safe for f64).
- Booleans, null: standard JSON.
- Embeddings: see §4.

TOON encoding rules:

- One TOON document per table in the envelope (the envelope wrapper is
  emitted as a JSON header even in TOON mode).
- Column order is fixed per §6 (semantics column-by-column).
- Pipe (`|`) is the column delimiter; embedded pipes are escaped as
  `\|`.
- Embeddings are emitted as the same base64 string as JSON (§4) — TOON
  does not re-encode them.

---

## 3. Envelope structure

```json
{
  "schema_version": "v1",
  "source": "ai-memory-v0.6.3.1",
  "exported_at": "2026-04-29T12:00:00.000Z",
  "db_schema_version": 17,
  "namespaces": [ /* §6.1 */ ],
  "memories":   [ /* §6.2 */ ],
  "links":      [ /* §6.3 */ ],
  "archived":   [ /* §6.4 */ ],
  "agents":     [ /* §6.5 */ ],
  "entities":   [ /* §6.6 */ ],
  "subscriptions": [ /* §6.7 */ ]
}
```

### 3.1 Required envelope fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `schema_version` | string | yes | Must be `"v1"` for this spec. Importers MUST reject other values. |
| `source` | string | yes | Identifies the producer. SHOULD include version and implementation name. Format: `"ai-memory-vMAJOR.MINOR.PATCH[.BUILD]"`. |
| `exported_at` | string | yes | RFC 3339 / ISO 8601 timestamp, UTC, millisecond precision, `Z` suffix. |
| `db_schema_version` | integer | yes | The internal SQLite schema version that produced this export. v0.6.3.1 stamps `17` (post-P2). v0.6.3 stamped `16`. Consumers MUST tolerate any value ≥7 and apply migrations on import. `(post-P2)` |
| `memories` | array | yes | Top-level memory rows. May be empty. |
| `links` | array | yes | Memory-link rows. May be empty. |
| `namespaces` | array | yes | Namespace metadata. May be empty (defaults to `global`). |

### 3.2 Optional envelope fields

| Field | Type | Notes |
|---|---|---|
| `archived` | array | `archived_memories` table rows. Omit if archives aren't being exported. |
| `agents` | array | Agent registrations (see §6.5). Omit if agent registry isn't enabled. |
| `entities` | array | Entity aliases (see §6.6). Omit if entity registry isn't populated. |
| `subscriptions` | array | Webhook subscriptions **with secrets stripped** (see §6.7). |

### 3.3 Forward-compat rule

Importers MUST preserve unknown fields at all levels of the envelope and
re-emit them on subsequent export. This is how v1 producers can be
upgraded by v1.1 / v2 producers without losing data when round-tripping
through a v1 consumer.

---

## 4. Embedding encoding (the part that bytes you in production)

ai-memory v0.6.3 stored embeddings as raw little-endian f32 BLOBs with
**no header**. v0.6.3.1's P2 migration v17 prepends a 1-byte magic
header to every new write. `(post-P2)`

| Magic byte | Meaning | Reader behaviour |
|---|---|---|
| `0x01` | Little-endian f32 (Intel / Apple Silicon) | Accept and decode normally. |
| `0x02` | Big-endian f32 | **REJECT** with `EmbeddingFormatError` until v0.7 adds endianness conversion. |
| (no header, legacy) | Implicit little-endian f32 (pre-v17 row) | Accept; treat as `0x01`. Tolerated for one major version. |
| any other | Unknown | **REJECT** with `EmbeddingFormatError`. |

### 4.1 JSON encoding

Embeddings are JSON strings encoding **base64 of (magic byte + raw
LE-f32 bytes)** with standard base64 alphabet (RFC 4648 §4) and `=`
padding.

A 384-dim MiniLM embedding is `1 + 384*4 = 1537` bytes raw, base64-encoded
to a string of ceil(1537/3)*4 = 2052 characters.

```json
"embedding": "AT8AAACzAAAAd1qF... (base64)"
```

The leading magic byte is part of the base64 payload. Importers MUST
decode the base64, read byte 0 as the magic byte, and validate before
interpreting the remaining bytes as f32 chunks of size `4 *
embedding_dim`.

### 4.2 Embedding dimension guard `(post-P2)`

Migration v17 adds an `embedding_dim` column. Exports include this
field as `embedding_dim` per memory row. On import:

- First write to an empty namespace **establishes** the namespace's
  embedding dim.
- Subsequent writes with a different dim are **rejected** (the
  reference implementation surfaces mixed-dim rows to operators via
  the `dim_violations` counter in `stats` — see `src/storage/mod.rs`;
  `MixedDimError` is the spec-reserved label for the refusal, not a
  code identifier) to prevent silent zero-cosine recall collapse.

If `embedding_dim` is absent on a row (legacy v0.6.3 export), the
importer MUST infer it from the BLOB length: `(len_bytes - 1) / 4` for
embeddings carrying a magic byte, or `len_bytes / 4` for headerless
legacy embeddings.

### 4.3 TOON encoding

Same base64 string as JSON — TOON does not re-encode embeddings.
Embedding cells appear in the column position fixed by §6.2.

---

## 5. Round-trip semantics

A round-trip is **`export → import → re-export`**. The two exports MUST
be byte-equivalent under JSON encoding (§2) when:

- `--preserve-timestamps` is passed to the importer (so `created_at` /
  `updated_at` / `last_accessed_at` are not restamped).
- `--preserve-agent-id` is the default and is not overridden (so
  `metadata.agent_id` is preserved across the import).
- `--preserve-archived` is passed when `archived` is non-empty.
- The intermediate store is **fresh** (no other writers).

Timestamps and access counters that are part of the live recall touch
loop (`last_accessed_at`, `access_count`) are byte-equivalent only if
the imported store is not queried between import and re-export.

### 5.1 Identity preservation

The `id` column is preserved verbatim across round-trip. v0.6.3.1
recommends UUIDv7 for new ids; older ids (any string ≤128 chars
matching `^[A-Za-z0-9_\-:@./]{1,128}$`) are preserved as-is.

`metadata.agent_id` is preserved per `CLAUDE.md` Agent Identity rules.
The importer's caller agent id appears as `imported_from_agent_id` on
each row (unless `--trust-source` is passed).

---

## 6. Field-by-field semantics

Field types are JSON types unless otherwise noted. SQLite types are
shown in parentheses for cross-reference with `src/storage/mod.rs`.

### 6.1 `namespaces[]` — namespace metadata

| Field | JSON type | SQLite | Required | Notes |
|---|---|---|---|---|
| `namespace` | string | TEXT PK | yes | The namespace path (e.g., `org/team-a`). |
| `standard_id` | string \| null | TEXT | no | Curator standard reference. |
| `parent_namespace` | string \| null | TEXT | no | For hierarchical inheritance (post-G1). |
| `updated_at` | string | TEXT | yes | RFC 3339 UTC. |

### 6.2 `memories[]` — top-level memories

Column order for TOON (and the order semantics follow):

`id | tier | namespace | title | content | tags | priority | confidence | source | access_count | created_at | updated_at | last_accessed_at | expires_at | metadata | embedding | embedding_dim`

| Field | JSON type | SQLite | Required | Notes |
|---|---|---|---|---|
| `id` | string | TEXT PK | yes | UUIDv7 recommended. Preserved verbatim. |
| `tier` | string | TEXT | yes | One of `"short"`, `"mid"`, `"long"`. |
| `namespace` | string | TEXT | yes | Defaults to `"global"`. |
| `title` | string | TEXT | yes | Logical key — UNIQUE per namespace. |
| `content` | string | TEXT | yes | The memory body. UTF-8. |
| `tags` | string[] | TEXT (JSON) | yes | Array of strings. SQLite stores as JSON-encoded text. |
| `priority` | integer 1-10 | INTEGER | yes | Default 5. |
| `confidence` | number 0.0-1.0 | REAL | yes | Default 1.0. |
| `source` | string | TEXT | yes | Origin tag (e.g., `"api"`, `"mine"`, `"consolidate"`). |
| `access_count` | integer ≥0 | INTEGER | yes | Default 0. |
| `created_at` | string | TEXT | yes | RFC 3339 UTC. |
| `updated_at` | string | TEXT | yes | RFC 3339 UTC. |
| `last_accessed_at` | string \| null | TEXT | no | RFC 3339 UTC or null. |
| `expires_at` | string \| null | TEXT | no | RFC 3339 UTC or null. Long-tier memories MUST have null. |
| `metadata` | object | TEXT (JSON) | yes | Free-form JSON object; SQLite stores as JSON text. Defaults `{}`. |
| `embedding` | string \| null | BLOB | no | Base64 per §4. Null when no embedding. |
| `embedding_dim` | integer \| null | INTEGER | no | Embedding dimension. `(post-P2)`. |
| `agent_id_idx` | (omitted) | virtual | — | Generated column; not exported. Recomputed from `metadata.agent_id` on import. |
| `scope_idx` | (omitted) | virtual | — | Generated column; not exported. Recomputed from `metadata.scope` on import. |

#### 6.2.1 Reserved metadata keys

Producers MUST preserve these keys verbatim; consumers MUST NOT
overwrite them.

| Key | Producer | Consumer rule |
|---|---|---|
| `agent_id` | Set on first write | Preserve verbatim across import / sync / consolidate. |
| `imported_from_agent_id` | Set by importer when restamping `agent_id` | Stamp once; subsequent imports DO NOT recurse. |
| `consolidated_from_agents` | Array set by `memory_consolidate` | Preserve verbatim. |
| `mined_from` | Set by `ai-memory mine` | Preserve verbatim. |
| `scope` | Optional visibility marker (`private` / `team` / `unit` / `org` / `collective`) | Preserve verbatim. |

> **Read-path enforcement (v0.7.0 #1468 / #1469).** On the MCP read tools
> (`memory_session_start`, `memory_list`, `memory_search`, `memory_recall`),
> a missing `scope` key is treated as `private`. When the daemon resolves a
> visibility caller from `AI_MEMORY_AGENT_ID`, rows whose `scope` is
> `private` (or absent) AND whose `agent_id` is a *different* agent are
> dropped before the response is serialized; `collective` rows and
> caller-owned rows always pass. With `AI_MEMORY_AGENT_ID` unset, the read
> path is trust-all (single-tenant) and returns every matching row. The
> stored/exported bytes are unchanged either way — this is a read-time
> filter, not a mutation.

### 6.3 `links[]` — memory links

| Field | JSON type | SQLite | Required | Notes |
|---|---|---|---|---|
| `source_id` | string | TEXT FK | yes | References `memories.id`. |
| `target_id` | string | TEXT FK | yes | References `memories.id`. |
| `relation` | string | TEXT | yes | One of: `related_to`, `supersedes`, `contradicts`, `derived_from`, `reflects_on` (recursive-learning Task 1/8, v0.7.0), `derives_from` (WT-1-A atomisation, v0.7.0). The set is closed — six variants at v0.7.0, validators reject anything else. Default `"related_to"`. |
| `created_at` | string | TEXT | yes | RFC 3339 UTC. |
| `valid_from` | string \| null | TEXT | no | Temporal-validity start (v0.6.3 KG schema). |
| `valid_until` | string \| null | TEXT | no | Temporal-validity end. |
| `observed_by` | string \| null | TEXT | no | Agent id that observed this link. |
| `signature` | string \| null | base64(BLOB) | no | Optional Ed25519 signature over canonical link bytes. |

### 6.4 `archived[]` — archived memories

Same column set as `memories[]`, plus:

| Field | JSON type | Required | Notes |
|---|---|---|---|
| `archived_at` | string | yes | RFC 3339 UTC. |
| `archive_reason` | string | yes | Default `"ttl_expired"`. Other values: `"manual"`, `"gc_overflow"`. |
| `original_tier` | string | yes `(post-P2)` | Tier at time of archival. |
| `original_expires_at` | string \| null | yes `(post-P2)` | Expiry at time of archival; null for long-tier. |
| `embedding` | string \| null | no `(post-P2)` | Embedding preserved through archive. |
| `embedding_dim` | integer \| null | no `(post-P2)` | |

`(post-P2)` fields default to null on legacy exports; consumers MUST
tolerate their absence and treat archived rows as restoring to `long`
with no expiry (the v0.6.3 behaviour).

### 6.5 `agents[]` — agent registrations

| Field | JSON type | Required | Notes |
|---|---|---|---|
| `agent_id` | string | yes | Matches `^[A-Za-z0-9_\-:@./]{1,128}$`. |
| `agent_type` | string | yes | E.g., `"human"`, `"llm"`, `"daemon"`. |
| `capabilities` | string[] | yes | Capability tags. |
| `registered_at` | string | yes | RFC 3339 UTC. |
| `attestation` | object \| null | no | Reserved for v0.7 attestation. v1 importers MUST preserve verbatim. |

### 6.6 `entities[]` — entity aliases

| Field | JSON type | Required | Notes |
|---|---|---|---|
| `entity_id` | string | yes | Stable entity identifier. |
| `alias` | string | yes | Human-readable alias. |
| `created_at` | string | yes | RFC 3339 UTC. |

The `(entity_id, alias)` pair is the primary key.

### 6.7 `subscriptions[]` — webhook subscriptions (secrets stripped)

| Field | JSON type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | UUIDv7 recommended. |
| `url` | string | yes | Webhook target URL. |
| `events` | string | yes | Comma-separated event whitelist; `"*"` for all. |
| `secret_hash` | (omitted) | — | **NEVER exported.** Producers MUST omit. Consumers MUST NOT trust if present. |
| `namespace_filter` | string \| null | no | |
| `agent_filter` | string \| null | no | |
| `created_by` | string \| null | no | |
| `created_at` | string | yes | RFC 3339 UTC. |
| `last_dispatched_at` | string \| null | no | |
| `dispatch_count` | integer | no | Default 0. |
| `failure_count` | integer | no | Default 0. |

After import, the operator MUST re-supply the webhook secret out-of-band.
This is intentional — secret-bearing exports should not exist.

---

## 7. JSON example (small)

```json
{
  "schema_version": "v1",
  "source": "ai-memory-v0.6.3.1",
  "exported_at": "2026-04-29T12:00:00.000Z",
  "db_schema_version": 17,
  "namespaces": [
    {"namespace": "global", "updated_at": "2026-04-29T11:59:00.000Z"}
  ],
  "memories": [
    {
      "id": "01JCG3FY2WJ7H6ZEXAMPLE",
      "tier": "long",
      "namespace": "global",
      "title": "Capital of France",
      "content": "The capital of France is Paris.",
      "tags": ["geo", "europe"],
      "priority": 7,
      "confidence": 1.0,
      "source": "api",
      "access_count": 3,
      "created_at": "2026-04-29T11:00:00.000Z",
      "updated_at": "2026-04-29T11:00:00.000Z",
      "last_accessed_at": "2026-04-29T11:55:00.000Z",
      "expires_at": null,
      "metadata": {"agent_id": "ai:claude@host:pid-12345"},
      "embedding": "AT8AAACzAAAAd1qFAQ...",
      "embedding_dim": 384
    }
  ],
  "links": [],
  "archived": [],
  "agents": [],
  "entities": [],
  "subscriptions": []
}
```

---

## 8. CLI surface

```bash
# Producer
ai-memory export --format=json   > store.v1.json
ai-memory export --format=toon   > store.v1.toon
ai-memory export --include=archived,agents > store.v1.json

# Consumer
ai-memory import --format=json --preserve-timestamps < store.v1.json
ai-memory import --format=toon < store.v1.toon

# Round-trip self-test (see §10)
ai-memory export | ai-memory import --db /tmp/copy.db --preserve-timestamps
```

Importers default to `--preserve-timestamps=false` (i.e., restamping
`created_at` / `updated_at` to the import time and stamping
`imported_from_agent_id`). Pass `--preserve-timestamps` for
round-trip-conformant operation.

---

## 9. Backward compatibility with v0.6.3 exports

A v0.6.3 export looks like:

```json
{ "memories": [...], "links": [...], "count": N, "exported_at": "..." }
```

It lacks `schema_version`, `source`, `db_schema_version`, and the
optional tables. v0.6.3.1 importers MUST accept this shape by treating
the missing envelope fields as defaults:

- Missing `schema_version` → assume `"v1"` for legacy exports.
- Missing `source` → assume `"ai-memory-v0.6.3"`.
- Missing `db_schema_version` → assume `16`.
- Missing optional arrays → empty.
- Missing per-memory `embedding_dim` → infer from BLOB length.
- Missing per-memory `metadata` → `{}`.

The v0.6.3.1 producer always emits the full v1 envelope; the legacy
shape is a consumer-side compatibility concern only.

---

## 10. Round-trip test plan

The acceptance gate for the spec is: a fresh DB seeded with N memories
of varied tier / embedding state survives an `export → import →
re-export` cycle byte-equivalently under JSON encoding when the
importer is given `--preserve-timestamps`.

### 10.1 Test harness (sketch)

```bash
# 1. Seed
ai-memory --db /tmp/src.db store --title "T1" --content "C1" --tier long
ai-memory --db /tmp/src.db store --title "T2" --content "C2" --tier mid
# ...repeat with embeddings populated, links, archived rows

# 2. Export
ai-memory --db /tmp/src.db export --format=json > /tmp/round-1.json

# 3. Import to a fresh DB
ai-memory --db /tmp/dst.db import --format=json --preserve-timestamps < /tmp/round-1.json

# 4. Re-export
ai-memory --db /tmp/dst.db export --format=json > /tmp/round-2.json

# 5. Diff — MUST be empty modulo `exported_at`
jq 'del(.exported_at)' /tmp/round-1.json > /tmp/r1.normalized
jq 'del(.exported_at)' /tmp/round-2.json > /tmp/r2.normalized
diff /tmp/r1.normalized /tmp/r2.normalized
```

The Rust integration test that automates this lives at
`tests/portability_v1_round_trip.rs` (PENDING-RUN: this test is
specified by the spec; the implementation test depends on P2's `v17`
schema landing in the worktree to populate `embedding_dim` and
`original_tier` on archived rows, and on a CLI `--preserve-timestamps`
flag landing on the importer. Both are tracked by the v0.6.3.1 release
checklist).

### 10.2 Test matrix

| Case | Memories | Embeddings | Archived | Links | Status |
|---|---|---|---|---|---|
| Empty store | 0 | — | 0 | 0 | testable today |
| Long-tier only, no embeddings | 100 | 0 | 0 | 0 | testable today |
| Mixed tiers, MiniLM embeddings | 100 | 100 (384d) | 0 | 50 | testable today |
| Mixed tiers + archive | 100 | 100 (384d) | 50 | 50 | needs P2 (preserves embedding through archive) |
| Mixed dim across namespaces | 50 + 50 | 50 (384d) + 50 (768d) | 0 | 0 | needs P2 (`embedding_dim` enforcement) |
| With agents + entities + subscriptions | 100 | 100 | 0 | 0 | testable today |
| Endianness magic byte | 10 with `0x01` | 10 | 0 | 0 | needs P2 (writer prepends magic byte) |
| Forward-compat unknown field | 10 with `metadata.future_field` | 10 | 0 | 0 | testable today (forward-compat rule §3.3) |

Cases that "need P2" become testable once schema v17 lands. The spec is
written so the test matrix doesn't depend on which session lands P2 —
v0.6.3 exports remain consumer-compatible per §9.

---

## 11. What's deliberately out of scope for v1

- **Multi-implementation interop.** v1 is a single-implementation
  reference. A second implementation (e.g., a Python or Go reader) is a
  v2 deliverable per ROADMAP §7.6.
- **Federation identity attestation.** The `agents[].attestation`
  field is reserved but unspecified at v1.
- **Endianness conversion.** Big-endian f32 is REJECTED at v1; a
  future spec revision may add converters and reclassify magic byte
  `0x02` from `EmbeddingFormatError` to `EndiannessConversionRequired`
  (neither error name shipped in v0.7.0 — both are spec-reserved
  labels, not code identifiers).
- **AgenticMem-specific fields.** This is the OSS spec; commercial-
  edition extensions are out of scope.
- **Streaming exports.** v1 envelopes are batch documents. A streaming
  variant (NDJSON / chunked) is a v2 deliverable.
- **Compression.** A v1 envelope is plain JSON or TOON. Operators may
  gzip the bytes in transit; that's not part of the spec.
- **Encryption.** A v1 envelope is plaintext. Operators using SQLCipher
  for at-rest encryption MUST treat the export as cleartext and store
  it accordingly. v2 may add an encrypted envelope wrapper.

---

## 12. Versioning and stability

- The string `"v1"` in `schema_version` is **stable** for all v0.6.x
  releases.
- New optional fields MAY be added in v1.x — consumers MUST tolerate
  unknown fields per §3.3.
- A breaking change to envelope structure (renaming a required field,
  removing a table, changing embedding encoding) bumps to `"v2"`.
- The `db_schema_version` field is the migration version of the
  internal SQLite schema and is independent of `schema_version`.

---

## 13. References

- Code: [`src/storage/`](../../src/storage/),
  [`src/handlers/admin.rs`](../../src/handlers/admin.rs)
  (`export_memories`, `import_memories`),
  [`src/toon.rs`](../../src/toon.rs).
- Migrations: [`migrations/sqlite/`](../../migrations/sqlite/) — v17 is
  the canonical reference schema for v1 `(post-P2)`.
- TOON: <https://www.tensorlake.ai/blog-posts/toon-vs-json>
- ROADMAP §7.6 (multi-impl interop deferral to v2).
- LongMemEval variant disclosure: [`benchmarks/longmemeval/results.md`](../../benchmarks/longmemeval/results.md).

---

## 14. Handoff notes (P8 → P2 dependency)

Three sections of this spec are tagged `(post-P2)` because they describe
fields that the migration v17 (Phase P2) introduces:

1. **§3.1 `db_schema_version`** — v0.6.3.1 stamps `17` once P2 lands.
2. **§4.2 `embedding_dim`** + magic-byte semantics — depends on P2's
   `set_embedding` writer prepending `0x01`.
3. **§6.4 `archived[].original_tier` / `original_expires_at` /
   `embedding` / `embedding_dim`** — depends on P2's `archive_memory`
   and `restore_archived` updates.

Until P2 merges into `release/v0.6.3.1`, consumers MAY read v0.6.3
exports per the §9 backward-compat rules. Once P2 merges, this spec
becomes the canonical contract for v0.6.3.1 exports without further
revision — the spec was written against the P2 design intent, not its
implementation, so no spec text needs to change when P2 lands.

Subsequent maintenance:

- After P2 merges, add a follow-up PR that adds the
  `tests/portability_v1_round_trip.rs` integration test (cases marked
  "needs P2" in §10.2).
- Once two reference implementations exist (v2.0), promote this
  document to `docs/spec/v2.md` and freeze v1 here for legacy
  consumers.
