Skip to main content
REST surface for the shared memory tier: search the 2-tier store (declarative facts + versioned procedures), save a fact or a procedure, browse what is stored, and inspect which embedding provider backs vector/hybrid search. This is the UI-facing half of the memory dual surface; the model-facing half is the Memory MCP server. Both halves share one SqliteMemoryStore over the same SQLite file, so a fact saved here is searchable from a runtime’s Memory tool and vice versa.
Memory is always on; these routes are not flag-gated. The store is FTS5 (full-text) plus an optional vector index. Vector and hybrid search require a reachable embedding provider; when none resolves, they degrade to FTS automatically. See GET /api/memory/provider to inspect the active provider.
The save route (POST /api/memory) reads a JSON body parsed by express.json({ limit: '2mb' }). The two GET routes read their inputs from the query string, then validate them against the same zod schemas the save body uses, so an out-of-range limit or empty query is a 400.

Routes

Save scrubs secrets at the write boundary: a fact’s title/content and a procedure’s content are passed through a secret scrubber before they are embedded and inserted. A credential can never land in a durable, searchable, or auto-injectable fact regardless of who wrote it.

GET /api/memory

Searches stored facts. The handler reads query, mode, limit, teamId, and agentId from the query string, assembles a { query, mode, limit, scope: { teamId, agentId } } object, and validates it with the same schema the MCP memory_search tool uses. Each result is a fact annotated with a 0..1 score and a matchedVia field recording how it matched.
  • Query params
  • Request body: none.
Scope is inclusive. A scoped query (a teamId and/or agentId) also sees globally-scoped facts (rows with a null scope), so global memory is always visible to a scoped search. A tenantId scope, if supplied, is strict; but tenantId is a dormant multi-tenant seam (a single implicit tenant today).

Responses

200 OK: the (possibly empty) result list:
matchedVia reflects the mode that actually ran, not the mode requested: a vector/hybrid request with no embedding provider runs as fts and reports matchedVia: 'fts'. In hybrid mode the score blends the cosine similarity (60%) and an FTS-hit signal (40%). 400 Bad Request: the assembled query object failed validation (e.g. blank query, limit out of 1..100, or an unknown mode):
500 Internal Server Error: any failure constructing the store or running the search:

Example


POST /api/memory

Saves a memory entry. The body is a discriminated union: a fact (the default, when kind is absent or "fact") or a procedure (kind: "procedure"). A fact is a durable declarative statement (“User prefers concise responses”); a procedure is a versioned, SKILL-style “how” kept out of the fact store. Saving a procedure under a name+scope that already exists creates a new version (the prior max version +1) rather than overwriting.
  • Path/query params: none.
  • Request body: one of:
Fact (default):
Procedure:

Responses

200 OK: a fact was saved (it is embedded if a provider was available, but the embedding is best-effort and never blocks the write):
200 OK: a procedure was saved (kind: 'procedure'):
400 Bad Request: the body failed the discriminated-union validation (e.g. an over-length title/content, too many tags, or a procedure missing name):
500 Internal Server Error: any failure constructing the store or writing the row:

Example


GET /api/memory/browse

Lists the most recent facts and procedures (facts newest-first by updatedAt), scoped the same inclusive way as search. The handler reads limit, teamId, and agentId from the query string, validates them, then fetches facts and procedures in parallel.
  • Query params
  • Request body: none.

Responses

200 OK: facts and procedures side by side:
400 Bad Request: limit out of the 1..200 range:
500 Internal Server Error: any failure constructing the store or reading:

Example


GET /api/memory/provider

Reports the embedding provider that backs vector/hybrid search, resolved once at boot (a one-time network probe) and reused. The resolution order is: a reachable Ollama instance (the offline-first default, probed at http://localhost:11434), then an OpenAI key (OPENAI_API_KEY), then null. A null provider means the store is FTS-only; vector and hybrid search silently fall back to FTS. The response shape is provider-independent ({ id, dimensions }) so the UI can warn when vector search is degraded.
  • Path/query params: none.
  • Request body: none.

Responses

200 OK: a provider resolved:
200 OK: no provider reachable (FTS-only):
500 Internal Server Error: an unexpected failure resolving the provider:

Example


Error envelope

Errors on these routes use the standard envelope { error: string }. The two validating GET routes (/api/memory, /api/memory/browse) and the save route add a details field carrying the zod flatten() output on a 400, e.g. { "error": "invalid query", "details": { … } }.

See also

Last modified on June 26, 2026