ai-memory v0.8.0

ai-memory HTTP API Reference

Complete reference for every endpoint the ai-memory serve daemon exposes. All endpoints are prefixed with /api/v1/ unless noted.

Base URL

Default: http://127.0.0.1:9077.

Configure via ai-memory serve --host <host> --port <port>. Production deployments should always bind TLS: --tls-cert + --tls-key.

Authentication

API key

When an api_key is configured (the top-level api_key = "…" field in config.toml, or injected via AI_MEMORY_API_KEY by the Plan-C container entrypoint — there is no --api-key CLI flag on serve), every endpoint except /api/v1/health requires one of:

Failure → 401 {"error": "missing or invalid API key"}.

When mTLS fingerprint pinning is enforced (--mtls-allowlist), the /api/v1/sync/* federation paths bypass the api-key check — the mTLS handshake plus the X-Memory-Sig signed-message gate (#791/#1031) are the stronger authentication step there.

AI_MEMORY_REQUIRE_API_KEY=1 hard-refuses a keyless daemon start on ANY bind host, including loopback (#1458).

Agent identity — X-Agent-Id

Optional on writes. Identifies the caller for governance + attribution.

X-Agent-Id: ai:claude-opus-4.7@host.local
X-Agent-Id: alice
X-Agent-Id: host:prod-web-01:pid-12345-a1b2c3d4

Resolution (v0.7.0, header-first — the pre-v0.7.0 body-wins precedence was the #874-class spoof vector):

  1. The X-Agent-Id header is the authoritative identity slot.
  2. A body agent_id field (or ?agent_id= query param where accepted) is a refinement that MUST match the header-resolved id; a mismatch is rejected with 403 (agent_id_body_header_mismatch / agent_id_query_header_mismatch).
  3. With no header and no body claim, a per-request anonymous id (anonymous:req-<uuid8>) is synthesized and logged at WARN.

Validation pattern: ^[A-Za-z0-9_\-:@./]{1,128}$.

Admin-gated endpoints (v0.7.0 #943/#945/#946 cluster)

Corpus-scale endpoints require an admin caller: GET /stats, POST /gc, GET /export, POST /import, GET /agents, POST /forget, GET /namespaces (list form), GET /taxonomy, GET /archive, GET /archive/stats, the seven /skill/* routes, and PUT /agents/{id}/pubkey (#1539 — bind an agent’s Ed25519 attestation public key: body {"pubkey_b64": "<base64 32-byte key>"}, response {"bound": true, "agent_id": "..."}; the pubkey is validated as a real curve point and the agent must already be registered. Gives attesting clients under AI_MEMORY_REQUIRE_AGENT_ATTESTATION=1 a first-party enrollment surface instead of an out-of-band DB write). The admin allowlist is [admin] agent_ids = [...] in config.toml (plus AI_MEMORY_ADMIN_AGENT_IDS); when empty (the default) these endpoints return 403 to every caller. Per #1570, on a deployment with no api_key configured, a bare self-asserted X-Agent-Id naming an admin id is NOT trusted for admin-role resolution (boot emits a WARN); set AI_MEMORY_ADMIN_HEADER_TRUST=1 only to restore the legacy header-trust posture on isolated/mTLS-fronted deployments.

mTLS (Layer 2 peer mesh)

When --mtls-allowlist is set, every TCP connection must present a client certificate whose SHA-256 fingerprint appears (hex, optional : separators, # comments) on the allowlist file. Peers without a listed cert cannot even open the TCP connection.

See docs/ADMIN_GUIDE.md § “Peer-mesh security” for setup.

Response envelopes

Compression (v0.7.0, #1579 B4)

The daemon gzip-compresses responses when the request advertises Accept-Encoding: gzip (standard tower-http CompressionLayer; measured ~4.6× smaller recall payloads). Requests without the header receive identity-coded responses, byte-identical to earlier releases. The SSE surface (GET /api/v1/approvals/stream) is never compressed — text/event-stream is exempted so events flush immediately.

Success (2xx)

JSON body, shape depends on endpoint. Common patterns:

{ "memory": {  } }
{ "memories": [  ], "count": 5 }
{ "id": "abc123" }
{ "ok": true }

Error (4xx, 5xx)

Uniform envelope:

{ "error": "descriptive message" }

Status codes you’ll commonly encounter:

Code Meaning
200 OK
201 Created
202 Accepted — governance pending
400 Bad request — validation, parse, or limit error
401 Unauthorized — missing / invalid API key
403 Forbidden — governance denied
404 Not found
409 Conflict — duplicate (title, namespace)
500 Internal server error
503 Service unavailable

Limits

Admission control — overload shedding (v0.8.0, #1733 Pillar-4 4.A)

A global in-flight concurrency cap is opt-in via [limits].max_inflight_requests / AI_MEMORY_MAX_INFLIGHT_REQUESTS (precedence: env > config > compiled default 0 = disabled). When set to a positive n, the daemon admits at most n concurrent in-flight requests and sheds the rest at the outermost layer (before timeout, body decode, or handler work) with a typed 503:

{ "error": "server_overloaded", "code": "OVERLOADED", "max_inflight": 64 }

The shed response carries a Retry-After: 1 header. GET /api/v1/health, GET /api/v1/metrics, and the bare GET /metrics are EXEMPT from the cap so liveness/readiness probes and Prometheus scrapes survive an overload (otherwise the orchestrator’s health probe would be shed, the node killed, and graceful shedding would become a crash-loop). Shed events increment the ai_memory_admission_shed_total Prometheus counter and emit a sampled WARN. When the knob is unset / 0 / non-positive, no admission layer is composed and behaviour is byte-identical to a build without admission control.

The Memory object

{
  "id": "uuid-v4",
  "tier": "mid",
  "namespace": "global",
  "title": "Memory title",
  "content": "Memory body",
  "tags": ["tag1", "tag2"],
  "priority": 5,
  "confidence": 0.95,
  "source": "api",
  "access_count": 3,
  "created_at": "2026-04-19T10:30:00Z",
  "updated_at": "2026-04-19T10:30:00Z",
  "last_accessed_at": "2026-04-19T12:00:00Z",
  "expires_at": "2026-04-26T10:30:00Z",
  "metadata": {
    "agent_id": "ai:claude-opus-4.7",
    "scope": "private",
    "custom_field": "value"
  }
}

tier is one of "short" | "mid" | "long" (see Tier enum in src/models/memory.rs). last_accessed_at and expires_at are omitted from the JSON when not set — they are NOT serialized as null.

Fields marked in metadata are preserved across update / upsert / sync / consolidate.


Health + metrics

GET /api/v1/health

No authentication required. Returns daemon liveness.

Response

{ "status": "ok", "service": "ai-memory" }
curl http://127.0.0.1:9077/api/v1/health

GET /metrics and GET /api/v1/metrics

Prometheus text exposition format. Scrape from Prometheus, alertmanager, or Grafana Agent.

curl http://127.0.0.1:9077/metrics

GET /api/v1/stats

Structured database stats (counts by tier/namespace, links, size, last GC). Admin-gated (#946 cluster).

{
  "total": 150,
  "by_tier": [{"tier":"short","count":20},{"tier":"mid","count":100},{"tier":"long","count":30}],
  "by_namespace": [{"namespace":"global","count":90}],
  "expiring_soon": 5,
  "links_count": 23,
  "db_size_bytes": 524288
}

Memory CRUD

POST /api/v1/memories — create

{
  "title": "Quick note",
  "content": "Content",
  "tier": "mid",
  "namespace": "global",
  "tags": ["urgent"],
  "priority": 7,
  "confidence": 0.9,
  "source": "api",
  "ttl_secs": 604800,
  "expires_at": "2026-05-08T10:30:00Z",
  "metadata": {"custom": "data"},
  "agent_id": "alice",
  "scope": "private",
  "signature": "base64-std-detached-ed25519-sig",
  "created_at": "2026-05-08T10:30:00Z"
}

ttl_secs is HTTP-only — the MCP memory_store tool exposes expires_at instead (also accepted on this HTTP endpoint). See the HTTP ↔ MCP parameter coverage table at the bottom of this document.

An optional kind field is also accepted. Omitting it keeps the observation default; a supplied value MUST be one of the canonical variants (observation, reflection, persona, concept, entity, claim, relation, event, conversation, decision) or the request is rejected with 400 (#1467 — this endpoint previously coerced an unknown kind to observation; it now rejects to match the CLI and MCP surfaces). See docs/memory-kind-vocab.md.

Agent attestation (signature + created_at) — #626 Layer-3

A caller MAY present a detached Ed25519 signature to upgrade the write from a claimed agent_id to a cryptographically attested one. The signature is computed over the canonical SignableWrite envelope (agent_id + namespace + title + kind + created_at + sha256(content)) and encoded as standard base64. When signature is present, created_at (RFC 3339) is required — it is the exact timestamp that was signed.

This wire is identical across the three store surfaces (MCP memory_store, this HTTP endpoint, and the CLI --sign path).

curl -X POST http://127.0.0.1:9077/api/v1/memories \
  -H "X-API-Key: KEY" -H "X-Agent-Id: alice" \
  -H "Content-Type: application/json" \
  -d '{"title":"Meeting notes","content":"Q2 roadmap","tier":"mid"}'

GET /api/v1/memories — list

Query params: namespace, tier, limit (default 20, capped at max_page_size — compiled default 1000), offset, min_priority, since, until, tags (comma list), agent_id.

{ "memories": [  ], "count": 1 }

GET /api/v1/memories/{id} — get

UUID or unique prefix. Returns memory + its links.

{
  "memory": {  },
  "links": [{"source_id":"…","target_id":"…","relation":"related_to","created_at":"…"}]
}

PUT /api/v1/memories/{id} — update

All fields optional. Tier never downgrades.

{ "title": "New", "priority": 8, "tier": "long" }

DELETE /api/v1/memories/{id} — delete

Archives before delete when archive_on_gc=true.

POST /api/v1/memories/bulk — batch create

Body is a JSON array of CreateMemory objects, max_page_size items (compiled default 1000; operator-tunable via [limits].max_page_size / AI_MEMORY_MAX_PAGE_SIZE). Exceeding the cap returns 400 Bad Request with an error echoing the configured cap.

{ "created": 998, "errors": ["item 17: title is required",  ] }

GET /api/v1/recall and POST /api/v1/recall

Hybrid recall (FTS5 + semantic + blend). Mutates the database (touches, auto-promotes).

Query / body fields: context (required), namespace, limit (default 10, max 50), tags, since, until, as_agent, budget_tokens, format (json default | toon | toon_compact — v0.7.0 #1579 B4; the TOON variants return text/plain rendered by the same encoder the MCP tools use, toon_compact ≈ 79% smaller than the JSON envelope; an unrecognised value is a 400).

{
  "memories": [ { , "score": 0.87 } ],
  "count": 5,
  "tokens_used": 234,
  "budget_tokens": 3000
}
curl -X POST http://127.0.0.1:9077/api/v1/recall \
  -H "Content-Type: application/json" \
  -d '{"context":"quarterly planning","limit":10}'

v0.8.0 §2.5 read-time attested-provenance decoration (#1709). Under verbose provenance (the MCP default; HTTP opts in), each recall row carries read-time-composed fields in addition to score — all decoration-only (the stored confidence ranking contribution is unchanged):

When a confidence_tier filter is requested, the response envelope adds a meta object reporting what the filter dropped, so count: 0 is distinguishable from “no memory”:

{ "memories": [  ], "count": 0,
  "meta": { "confidence_filtered_out": 4, "had_filtered_candidates": true } }

These fields are uniform across MCP memory_recall, HTTP recall, and memory_session_start.

GET /api/v1/search

Read-only FTS5 keyword search. Same filter params as list, plus q (required) and format (json default | toon | toon_compact — v0.7.0 #1579 B4, same semantics as recall above).

{ "results": [  ], "count": 3, "query": "urgent deadline" }

Note (HTTP ↔ MCP parity): The MCP memory_recall, memory_search, and memory_list tools accept the same optional format parameter (json | toon | toon_compact). As of v0.7.0 (#1579 B4) the HTTP recall + search endpoints expose format too (HTTP defaults to json for backwards compat; MCP defaults to toon_compact). HTTP memory_list does not yet accept format. The MCP memory_recall tool requires the context parameter — the query / q alias ladder is HTTP-only (#1606); an MCP call passing query is refused with “context is required”. MCP additionally accepts a context_tokens array (v0.6.0.0 contextual recall — recent conversation tokens biasing the query embedding at 70/30) that the HTTP body does not surface.

Lifecycle

POST /api/v1/memories/{id}/promote

Bump to long tier. 200 / 202 / 404.

POST /api/v1/forget

{ "namespace": "scratch", "pattern": "deprecated", "tier": "short" }

At least one filter required. Admin-gated. Returns {"deleted": N}.

POST /api/v1/consolidate

{
  "ids": ["id1","id2","id3"],
  "title": "Summary",
  "summary": "Merged content",
  "namespace": "global",
  "tier": "long"
}

201 with {"id":"consolidated-uuid","consolidated":3}.

POST /api/v1/gc

Immediate garbage collection. Empty body. Admin-gated. Returns {"expired_deleted":N}.

POST /api/v1/links

{ "source_id": "abc", "target_id": "def", "relation": "supersedes" }

Relations (nine at v0.8.0; was six at v0.7.0, four at v0.6.x): related_to, supersedes, contradicts, derived_from, reflects_on (recursive-learning Task 1/8), derives_from (WT-1-A atomisation — atom row → parent memory), decomposes_into, depends_on, advances (the last three are the v0.8.0 Pillar-2 typed-cognition Goal/Plan/Step relations, #1709). Canonical enum in src/models/link.rs::MemoryLinkRelation (COUNT = 9).

GET /api/v1/links/{id}

Returns inbound + outbound links for a memory under {"links": [...]}. Each row in the array surfaces the full link envelope including the v0.7 temporal-validity columns (valid_from, valid_until, observed_by) and the attestation columns (signature, attest_level, signed_at) — wired through db::get_links per issue #860.

Knowledge Graph + taxonomy (v0.6.3)

These endpoints operate on the temporal-validity knowledge graph (memory_links with valid_from / valid_until / observed_by columns added in schema v15) and the namespace taxonomy. See docs/MIGRATION-v0.6.2-to-v0.6.3.md for the schema changes and docs/USER_GUIDE.md for the matching MCP tools.

GET /api/v1/taxonomy

Walk live (non-expired) memories grouped by namespace into a hierarchical tree. Admin-gated (#945).

Query params: prefix (optional, restricts walk; root is an accepted alias — prefix wins when both are supplied), depth (max 8 = MAX_NAMESPACE_DEPTH, default 8), limit (1-10000, default 1000).

{
  "tree": [
    { "namespace": "alphaone", "count": 0, "subtree_count": 47, "children": [...] }
  ],
  "total_count": 47,
  "truncated": false
}

POST /api/v1/check_duplicate

Embedding cosine-similarity duplicate detection.

{
  "title": "Project uses PostgreSQL 15",
  "content": "The main database is PostgreSQL 15 with pgvector for embeddings.",
  "namespace": "my-app",
  "threshold": 0.85
}

Response:

{
  "is_duplicate": true,
  "threshold": 0.85,
  "nearest": { "id": "...", "title": "...", "namespace": "...", "similarity": 0.92 },
  "suggested_merge": "...",
  "candidates_scanned": 412
}

threshold is clamped to a 0.5 floor. Requires the semantic feature tier or higher — without an embedder the endpoint returns 503 (Service Unavailable); threshold mismatches return 200 with is_duplicate: false.

POST /api/v1/entities

Register an entity-as-typed-memory. Idempotent on (canonical_name, namespace).

{
  "canonical_name": "PostgreSQL",
  "namespace": "my-app",
  "aliases": ["pg", "postgres"],
  "metadata": {}
}

Response: {"entity_id":"ent-...","canonical_name":"PostgreSQL","namespace":"my-app","aliases":["pg","postgres","PostgreSQL"],"created":true}.

Returns 409 if a non-entity memory with the same (title, namespace) exists.

GET /api/v1/entities/by_alias

Resolve an alias to its canonical entity.

Query params: alias (required), namespace (optional; without it, picks the most-recently-created match across namespaces).

{
  "found": true,
  "entity_id": "ent-...",
  "canonical_name": "PostgreSQL",
  "namespace": "my-app",
  "aliases": ["pg", "postgres", "PostgreSQL"]
}

found: false (and null fields) when the alias resolves to nothing.

GET /api/v1/kg/timeline

Ordered timeline of links anchored at a source. Skips links with NULL valid_from.

Query params: source_id (required), since / until (RFC 3339, optional), limit (1-1000, default 200).

{
  "source_id": "...",
  "events": [
    { "target_id": "...", "relation": "depends_on", "valid_from": "...", "valid_until": null, "observed_by": "..." }
  ],
  "count": 1
}

POST /api/v1/kg/invalidate

Mark a link superseded by setting valid_until. Does NOT delete the link — historical queries pinned to valid_at < now still see it. Idempotent.

{
  "source_id": "...",
  "target_id": "...",
  "relation": "depends_on",
  "valid_until": "2026-04-26T03:00:00Z"
}

Response: {"found":true,"valid_until":"...","previous_valid_until":null}.

Federation: invalidations apply locally and propagate asynchronously via the sync-daemon — they are NOT quorum-broadcast.

POST /api/v1/kg/query

Recursive-CTE traversal of the temporal knowledge graph rooted at a source memory.

{
  "source_id": "...",
  "max_depth": 3,
  "valid_at": "2026-04-26T00:00:00Z",
  "allowed_agents": ["ai:claude-code@host:pid-12345"],
  "limit": 200
}

Constraints: max_depth defaults to 1 when omitted and must be in 1..=5 (KG_QUERY_MAX_SUPPORTED_DEPTH; depth 0 errors, depth > 5 errors). allowed_agents: [] (empty array) returns zero rows; omit the field to skip the agent filter entirely.

Response:

{
  "source_id": "...",
  "max_depth": 3,
  "memories": [
    {
      "target_id": "...",
      "title": "...",
      "target_namespace": "my-app",
      "relation": "depends_on",
      "valid_from": "...",
      "valid_until": null,
      "observed_by": "...",
      "depth": 1,
      "path": "src->tgt"
    }
  ],
  "paths": ["src->tgt->..."],
  "count": 1
}

Ordering: depth ASC, COALESCE(valid_from, link_created_at) ASC, link_created_at ASC.

Namespaces

GET /api/v1/namespaces

Lists namespaces with live-memory counts (admin-gated, #945). With ?namespace=<ns> the same route instead fetches that namespace’s standard (query-string twin of the {ns}/standard path form below).

{ "namespaces": [{"namespace":"global","count":50},{"namespace":"project-x","count":30}] }

GET /api/v1/namespaces/{ns}/standard — get namespace standard

Query: inherit (boolean, default false). When true, returns the full N-level resolved chain (global * → ancestors → namespace) instead of the single namespace’s standard.

{ "namespace": "engineering/auth", "standards": [  ], "chain": ["*","engineering","engineering/auth"], "count": 3 }

Returns 200 with count: 0 and an empty standards array when no standard is set. Equivalent MCP tool: memory_namespace_get_standard (src/mcp/tools/namespace.rs).

POST /api/v1/namespaces/{ns}/standard — set namespace standard

Body: { "id": "<memory-id>", "parent": "<optional-parent-namespace>", "governance": { … } }. governance accepts write / promote / delete (each any | registered | owner | approve), approver (ApproverType), and inherit (boolean, default true). Equivalent MCP tool: memory_namespace_set_standard (src/mcp/tools/namespace.rs).

DELETE /api/v1/namespaces/{ns}/standard — clear namespace standard

Removes the namespace’s pinned standard (the standard memory itself is not deleted; only the namespace_meta.standard_id link). Equivalent MCP tool: memory_namespace_clear_standard (src/mcp/tools/namespace.rs).

Archive

GET /api/v1/archive — list archived memories

Admin-gated (#943). Query: namespace, limit (default 50, clamped 1-1000; limit=0 → 400), offset.

{ "archived": [  ], "count": 24 }

Equivalent MCP tool: memory_archive_list (src/mcp/tools/archive.rs). A POST /api/v1/archive form also exists (archive an explicit list of memory ids; ≤ 1000 ids per request).

POST /api/v1/archive/{id}/restore — restore archived memory

Path param: id (archived memory id). On success the row is removed from archived_memories and re-inserted into memories with original_tier and original_expires_at re-applied where present. Equivalent MCP tool: memory_archive_restore (src/mcp/tools/archive.rs).

DELETE /api/v1/archive?older_than_days=30 — purge archived memories

Query: older_than_days (optional). Without the query param, all archived rows are eligible. Returns {"purged": N}. Equivalent MCP tool: memory_archive_purge (src/mcp/tools/archive.rs).

GET /api/v1/archive/stats — archive counters

Admin-gated (#943).

{ "archived_total": 24, "by_namespace": [{"namespace":"global","count":18},  ] }

Equivalent MCP tool: memory_archive_stats (src/mcp/tools/archive.rs).

Agents + governance

POST /api/v1/agents

{ "agent_id": "alice", "agent_type": "human", "capabilities": ["read","write"] }

agent_type accepts human, system, or any ai:<name> form (ai:claude-opus-4.7, ai:gpt-5, etc.).

GET /api/v1/agents

Admin-gated (#946). Returns {"agents":[…],"count":N}.

GET /api/v1/pending — list pending governance actions

Query: status=pending|approved|rejected, limit (default 100, max 1000).

{ "pending": [ { "id": "…", "action_type": "store", "namespace": "…", "status": "pending", "approvals": [  ] } ], "count": 3 }

Equivalent MCP tool: memory_pending_list (src/mcp/tools/pending.rs).

POST /api/v1/pending/{id}/approve — approve pending action

Path param: id. Stamps decided_by with the caller’s X-Agent-Id. 200 if consensus reached (and the governed action is executed). 202 if still collecting approvers. Equivalent MCP tool: memory_pending_approve (src/mcp/tools/pending.rs).

POST /api/v1/pending/{id}/reject — reject pending action

Path param: id. Returns {"rejected":true,"id":"…","decided_by":"alice"}. Equivalent MCP tool: memory_pending_reject (src/mcp/tools/pending.rs).

Sync / federation

POST /api/v1/sync/push

Peer-to-peer push with timestamp-aware merge.

{
  "sender_agent_id": "peer-remote-1",
  "memories": [ {  up to max_page_size (default 1000)  } ],
  "dry_run": false
}

Response includes applied, noop, skipped, receiver_agent_id, receiver_clock.

Federation headers (v0.7.0 secure defaults). Under AI_MEMORY_FED_REQUIRE_SIG=1 (default, #791) the request must carry an Ed25519 X-Memory-Sig over the body, attributed via X-Peer-Id to an enrolled peer key; under AI_MEMORY_FED_REQUIRE_NONCE=1 (default, #922) a fresh per-message X-Memory-Nonce is also required (the signature binds body || 0x00 || nonce), so byte-replays produce 401 x_memory_nonce_replay. GET /api/v1/sync/since enforces the same signed-message gate over canonical GET bytes (method || path || query, #1031).

GET /api/v1/sync/since

Query: since (RFC3339, optional), limit (default 500, max 10000), peer (attribution tag).

{ "count": 5, "limit": 500, "memories": [  ] }

Import / export

GET /api/v1/export

Admin-gated. Returns {"memories":[…],"links":[…],"count":N,"exported_at":"…"}.

POST /api/v1/import

Admin-gated. Body matches export shape. ≤ 1000 memories per call (compiled MAX_BULK_SIZE). Returns {"imported":N,"errors":[…]}. Preserves original metadata.agent_id into metadata.imported_from_agent_id.

Webhooks (v0.6.0.0)

Three endpoints under /api/v1/subscriptions — create them via MCP tools or the REST surface. Dispatch is SSRF-hardened (rejects private-range IPs; requires https:// unless loopback).

POST /api/v1/subscriptions — register webhook

Body: { "url": "https://…", "events": "memory_store,memory_delete", "secret": "<shared-secret>", "namespace_filter": "…", "agent_filter": "…" }. events is a comma-separated string (default "*"). Canonical event types (WEBHOOK_EVENT_TYPES in src/subscriptions.rs): memory_store, memory_promote, memory_delete, memory_link_created, memory_link_invalidated, memory_consolidated, approval_requested. Stores secret as a SHA-256 hash; dispatched events carry an X-AI-Memory-Signature: sha256=<hex> HMAC header. Returns the new subscription id. Equivalent MCP tool: memory_subscribe (src/mcp/tools/subscribe.rs).

DELETE /api/v1/subscriptions?id=<id> — unregister webhook

Returns {"deleted": true}. Equivalent MCP tool: memory_unsubscribe (src/mcp/tools/subscribe.rs).

GET /api/v1/subscriptions — list subscriptions

Returns {"subscriptions":[…],"count":N}. Each entry includes url, events, created_at, dispatch_count, failure_count. Equivalent MCP tool: memory_list_subscriptions (src/mcp/tools/subscribe.rs).

Federation (v0.7, opt-in via --quorum-writes)

When ai-memory serve --quorum-writes N --quorum-peers URL,URL,… is set, every write fans out to peers and returns only once W-1 peer acks land within --quorum-timeout-ms.

The local write is durably committed and never rolled back, so an under-replicated write is reported as a 202 Accepted, not a 5xx (the pre-v0.8.1 503 + Retry-After: 2 misreported a locally-durable write as a service failure). The replication state is carried in the body; the sync-daemon’s eventual-consistency loop + the federation push-DLQ converge peers afterwards (per ADR-0001), so there is no client retry to perform. A genuine local write failure still returns the appropriate error status.

Curl recipes

# Health
curl http://127.0.0.1:9077/api/v1/health

# Store a memory
curl -X POST -H "Content-Type: application/json" \
  http://127.0.0.1:9077/api/v1/memories \
  -d '{"title":"hi","content":"there","tier":"mid"}'

# Recall
curl -X POST -H "Content-Type: application/json" \
  http://127.0.0.1:9077/api/v1/recall \
  -d '{"context":"what did I store","limit":5}'

# Incremental sync pull since a timestamp
curl 'http://127.0.0.1:9077/api/v1/sync/since?since=2026-04-01T00:00:00Z&limit=1000'

# Prometheus scrape
curl http://127.0.0.1:9077/metrics

HTTP ↔ MCP parameter coverage

A small set of parameters are surfaced by only one transport. The MCP tool schema is authoritative via the per-tool <ToolName>Request structs in src/mcp/tools/<name>.rs (schemars-derived; consumed by registered_tools() in src/mcp/registry.rs and projected to tools/list by tool_definitions()). The HTTP body / query types in src/models/ and the route handlers in src/handlers/ are authoritative for HTTP.

Tool Param HTTP MCP Notes
memory_store ttl_secs HTTP-only; the MCP tool exposes expires_at (also accepted by HTTP).
memory_store expires_at (via update) HTTP body accepts; documented in the POST /api/v1/memories example.
memory_store signature #626 Layer-3 — std-base64 detached Ed25519 over the SignableWrite envelope; upgrades agent_id claimed→agent_attested. Same wire on both transports.
memory_store created_at #626 Layer-3 — RFC 3339; required when signature is present (the signed timestamp, adopted verbatim; ±300 s freshness window).
memory_recall format MCP-only; HTTP responses are always JSON.
memory_recall context_tokens MCP-only (v0.6.0.0 contextual recall).
memory_search format MCP-only.
memory_list format MCP-only.

These gaps are intentional for v0.6.3.1 and tracked for parity follow-up — they are NOT drift in the doc surface, just transport-level surface-area differences captured here so operators don’t re-derive them.

v0.7.0 net-new endpoints

The HTTP routes added since v0.6.4. All accept the same auth + agent-identity headers documented above. Wire-shape source of truth is the per-domain handler modules under src/handlers/; the route-path SSOT is src/handlers/routes.rs (#1558 batch 4), registered by the router in src/lib.rs.

Method Path Purpose
POST /api/v1/quota/status K8 quota status — read the calling agent’s daily quota row. Auto-inserts a default row on first call. See docs/k8-quotas.md. MCP: memory_quota_status.
GET /api/v1/approvals/stream K10 SSE approval channel — server-sent events for pending-approval state changes. See docs/k10-sse-approvals.md.
POST /api/v1/approvals/{pending_id} K10 approval decide path — body {"decision":"approve|deny","remember":"once|session|forever"}, HMAC-gated via X-AI-Memory-Signature.
POST /api/v1/auto_tag LLM auto-tag endpoint (v0.7 smart-tier surface; 503 when no LLM is configured).
POST /api/v1/expand_query HTTP parity for the MCP memory_expand_query tool.
POST /api/v1/kg/find_paths KG chain-walk over HTTP; Cypher on AGE / recursive-CTE on SQLite.
POST /api/v1/find_paths Alias for /api/v1/kg/find_paths (#934 — legacy callers).
POST /api/v1/links/verify Ed25519 link verification surface — wire shape: {verified, attest_level, signature_present, observed_by, source_id, target_id, relation, findings}.
DELETE /api/v1/links Delete a link. Returns {"deleted": N}.
GET /api/v1/contradictions Detect contradiction candidates (similar titles in a namespace).
POST /api/v1/memory_load_family HTTP parity for the always-on memory_load_family MCP loader.
POST /api/v1/capture_turn #1416 — L4 layered-capture HTTP mirror of MCP memory_capture_turn (idempotent per-turn write via MemoryStore::capture_turn_idempotent).
POST /api/v1/share #1095 — copy a memory into the recipient agent’s _shared/<from>→<to>/ namespace; body {source_memory_id, target_agent_id}. MCP: memory_share.
POST /api/v1/session/start HTTP parity for memory_session_start (auto-recall session boot).
GET /api/v1/capabilities Capabilities envelope (schema_version "3"; Accept-Capabilities header negotiates v1/v2). MCP: memory_capabilities.
POST /api/v1/notify Agent-to-agent inbox message. Sender resolved from X-Agent-Id only (#901); body agent_id must match or 403. MCP: memory_notify.
GET /api/v1/inbox Read the calling agent’s inbox. MCP: memory_inbox.
GET POST DELETE /api/v1/skill/list, /api/v1/skill/register, /api/v1/skill/{id}, /api/v1/skill/{id}/resource, /api/v1/skill/{id}/export, /api/v1/skill/{id}/promote, /api/v1/skill/{id}/compose Cluster E API-2 (#767) — Agent Skills HTTP parity for the seven memory_skill_* MCP tools. Admin-gated.
POST /api/v1/memory_smart_load, /api/v1/memory_reflect, /api/v1/memory_recall_observations, /api/v1/memory_reflection_origin, /api/v1/memory_dependents_of_invalidated, /api/v1/memory_export_reflection, /api/v1/memory_atomise, /api/v1/memory_calibrate_confidence, /api/v1/memory_verify, /api/v1/memory_replay, /api/v1/memory_subscription_replay, /api/v1/memory_subscription_dlq_list, /api/v1/memory_rule_list, /api/v1/memory_check_agent_action #1111 — 14 thin HTTP wrappers around the same-named MCP substrate handlers (src/handlers/route_1111.rs); wire envelopes are byte-equal across MCP and HTTP.
GET /api/v1/tools/list MCP tools/list mirror for harness ops — returns the live tool surface for the daemon’s profile (101 at full, 7 at core) — SSOT: Profile::full()/core().expected_tool_count() in src/profile.rs.

Total HTTP surface: 78 unique URL paths / 92 production route registrations on the sqlite-backed daemon (and the postgres-backed daemon under --features sal-postgres). Authoritative count: grep -oE '"/[^"]*"' src/handlers/routes.rs | sort -u | wc -l = 78 (77 /api/v1/* paths + the bare /metrics), pinned by EXPECTED_PRODUCTION_UNIQUE_PATHS_COUNT in src/lib.rs. The two v0.8.0 net-new paths were the #1718 coordination write surfaces below; the v0.9.0 #1859 G13-mem lineage read surface (GET /api/v1/memories/{id}/lineage) added the 78th unique path.

v0.8.0 net-new endpoints

The #1718 Pillar-1 distributed-coordination write surfaces. Only these two coordination paths are exposed over HTTP; the rest of the coordination toolset (action CRUD, leases, checkpoints, routines) is MCP-only.

Method Path Purpose
POST /api/v1/actions/{id}/transition Coordination action-state transition (handlers::transition_action) — local CAS write + W-of-N federation fanout. MCP: memory_action_transition.
POST /api/v1/signals Signed inter-agent signal send (handlers::send_signal) — local write + W-of-N fanout. MCP: memory_signal_send.

v0.7.0 net-new MCP tools

The 31 MCP tools added since v0.6.4 are documented inline in src/mcp/registry.rs and enumerated in docs/MIGRATION_v0.7.md §”New MCP tools”. Highlights for HTTP-equivalent surfaces:

MCP tool HTTP equivalent Notes
memory_load_family POST /api/v1/memory_load_family Always-on.
memory_quota_status POST /api/v1/quota/status K8.
memory_find_paths POST /api/v1/kg/find_paths J7.
memory_verify POST /api/v1/links/verify H4.
memory_pending_list / memory_pending_approve / memory_pending_reject GET /api/v1/pending, POST /api/v1/pending/{id}/approve, POST /api/v1/pending/{id}/reject K10. The MCP tool names changed from the v0.7-alpha drafts (memory_approval_pending / memory_approval_decide); the HTTP paths are stable.
memory_agent_register / memory_agent_list POST /api/v1/agents, GET /api/v1/agents meta family. Register an NHI agent (agent_type, capabilities) in _agents (refreshes last_seen_at, preserves registered_at) and list every registered agent (ordered by registered_at). agent_id is CLAIMED, not attested — pair with attestation (#626 Layer-3) for a security boundary.

For the canonical full inventory (101 entries advertised at --profile full — 100 callable + the always-on memory_capabilities): grep -oE 'crate::mcp::[a-z_]+::[A-Za-z]+Tool' src/mcp/registry.rs | sort -u | wc -l returns 101 — the registered_tools() iterator in src/mcp/registry.rs is the source of truth. The v0.8.0 net-new tools were the coordination families memory_action_*, memory_lease_*, memory_signal_*, memory_checkpoint_*, and memory_routine_*.

See also