API Reference
LongMemory is person-scoped, temporally-aware preference memoryfor AI agents. It answers one question fast and cheaply: given who's involved and when it is, what do we know about what they want?
localhost:8080.All bodies are JSON. Unknown fields are rejected with a 400; max body size is 1 MB. Times are RFC3339 — send an offset to control local time-of-day semantics (2026-07-08T20:00:00+05:30→ “evening”).
The core path makes zero LLM calls and zero embedding calls. Preferences are typed assertions; recall is indexed SQL plus deterministic ranking. Extraction, when you want it, piggybacks on the agent call you already make.
Quickstart
Build and run the engine:
go build -o bin/longmemory ./cmd/longmemory ./bin/longmemory # :8080, ./longmemory.db, open single-tenant mode
Remember two preferences:
curl -s localhost:8080/v1/ops -d '{
"owner_id": "u1",
"ops": [
{"op":"assert","domain":"health","key":"allergy",
"value":"peanuts","polarity":"rule"},
{"op":"assert","domain":"comms","key":"channel","value":"slack",
"polarity":"like","context":{"time_of_day":["morning"]}}
]}'Recall before your agent acts:
curl -s localhost:8080/v1/recall -d '{"owner_id": "u1"}'The response carries a prompt-ready context_block:
### Memory (as of 2026-07-22 09:15, tue morning) - You: [RULE] allergy: peanuts | likes slack (@morning)
Authentication
Two modes, chosen by whether LM_ADMIN_KEY is set.
| Mode | When | Tenant routes | Admin routes |
|---|---|---|---|
| Open single-tenant | LM_ADMIN_KEY | No auth; implicit tenant "default" | 501 |
| Multi-tenant | LM_ADMIN_KEY set | Authorization: Bearer <key> | X-Admin-Key |
LM_ADMIN_KEY for anything internet-facing, behind TLS.Errors are {"error":{"code":"...","message":"..."}} with status 400/401/404/429/500/501. Rate limiting is a per-tenant token bucket (rate_rps, burst 2×) returning 429 plus Retry-After.
Core concepts
owner_id (and a tenant). Owners never see each other's data."self"). Resolved by id, name, alias or relation — "my wife" matches Priya. Unknown refs auto-create a person.rule (hard constraint — diet, allergy; pinned first in recall), like/dislike (soft taste), fact (neutral attribute, default).valid_until means it holds until contradicted. With one, it shadows conflicting indefinite facts while current, then stops matching — the old one resurfaces with no cron job.stated (confidence 1.0), imported (0.8), inferred (0.5, decays). Inferred facts get promoted to stated the moment the user says it out loud.Memory operations
/v1/opsApply a batch of operations atomically — all or nothing, up to 200 ops. This is the write path, and the endpoint your agent'semit_memory_ops tool output goes to.
{
"owner_id": "u_42",
"ops": [
{"op":"person_upsert","name":"Priya","relation":"wife","aliases":["wife"]},
{"op":"assert","person":"wife","domain":"food","key":"diet",
"value":"vegetarian","polarity":"rule"},
{"op":"assert","person":"self","domain":"food","key":"diet","value":"no sugar",
"polarity":"rule","valid_until":"2026-07-31T23:59:59Z"},
{"op":"retract","person":"self","domain":"food","key":"spice_tolerance"}
]
}Operations
| op | Required | Notes |
|---|---|---|
assert | person, domain, key, value | Creates, reinforces on repeat, or supersedes conflicting facts |
reinforce | assertion_id or person+key | Bumps observed_count; inferred confidence +0.1 |
retract | assertion_id or person+key | Marks matching active assertions retracted (audit kept) |
person_upsert | name | Matches existing person by name/alias; sets relation, aliases |
person_alias | person, aliases | Adds aliases to an existing person |
person_merge | into, from | Reassigns all data, tombstones the merged-from person |
forget_person | person | Hard-deletes a person and everything about them |
assert fields
| Field | Type | Description |
|---|---|---|
person | string | "self", a p_… id, or a name/alias/relation. Auto-creates if unknown. |
domain | string | food, dining, grocery, ride, place, schedule, general… |
key | string | snake_case: diet, allergy, cuisine, budget_per_meal, home_address… |
value | any | Usually a short string. |
polarity | enum | rule | like | dislike | fact (default fact) |
context | object | time_of_day[], days[], location, occasion — only when genuinely conditional |
valid_from | RFC3339 | For preferences that start in the future |
valid_until | RFC3339 | Makes it temporary: shadows, then auto-reverts |
provenance | enum | stated (default) | inferred | imported |
confidence | float | 0–1; overrides the provenance default |
cardinality | enum | single | multi — whether values replace or accumulate |
relation | string | Sets the relation when auto-creating the person |
Response
{"results":[
{"index":0,"action":"person_created","person_id":"p_01hv…"},
{"index":1,"action":"created","assertion_id":"a_01hv…","superseded":["a_01hu…"]}
]}Actions: created, reinforced, retracted, person_created, person_updated, merged, forgotten, not_found (selector matched nothing — the batch still succeeds).
The piggyback contract
GET /v1/schema/ops returns the JSON Schema of the emit_memory_ops agent tool. Register it verbatim on the agent call you already make, then POST its output here — extraction costs you nothing extra.
Recall
/v1/recallEverything known about the mentioned persons, valid now (or at at), matching the moment's context, ranked — plus a rendered context block. Pure SQL and arithmetic: no LLM, ~0.78 ms.
{
"owner_id": "u_42",
"persons": ["self", "wife"], // default ["self"]; "*" = everyone
"domains": ["food", "dining"], // default all; "general" always included
"at": "2026-07-08T20:00:00+05:30", // default server now
"location": "home", // optional
"occasion": "date_night", // optional
"max_per_person": 12, // default 12
"token_budget": 800 // context_block budget, default 800
}Response
{
"persons": [
{"ref":"wife",
"person":{"id":"p_…","name":"Priya","relation":"wife"},
"assertions":[
{"id":"a_…","key":"diet","value":"vegetarian","polarity":"rule",
"score":1.1,"effective_confidence":1.0,"provenance":"stated"}
]}
],
"unresolved": [
{"ref":"rohit","candidates":[{"id":"p_…","name":"Rohit Sharma"},
{"id":"p_…","name":"Rohit Verma"}]}
],
"context_block": "### Memory (as of 2026-07-08 20:00, wed evening)\n- You: …",
"at":"2026-07-08T14:30:00Z", "time_of_day":"evening", "day":"wed",
"elapsed_ms":0.8
}unresolvedwith candidates, so your agent can ask “which Rohit?” instead of remembering the wrong person's allergy.Ranking
Rules sort first and survive token-budget trimming. Everything else ranks by provenance weight × effective confidence, plus a context-specificity bonus and a recency term. Inferred facts decay on a 180-day half-life and drop out below 0.35 confidence. Identical inputs always produce identical output.
Episodes
/v1/episodesLog raw events (≤500 per batch). Recognized payload fields distill into behavioural signals; three observations of the same pattern within 90 days materialize an inferred preference — time-of-day scoped when ≥80% of the observations agree. No LLM involved.
{
"owner_id": "u_42",
"episodes": [{
"kind": "order_completed",
"domain": "food",
"persons": ["self"],
"text": "Ordered Chicken Biryani from Meghana Foods",
"payload": {"restaurant":"Meghana Foods",
"items":[{"name":"Chicken Biryani","cuisine":"biryani"}],
"total":540},
"occurred_at": "2026-07-07T20:05:00+05:30"
}]
}Recognized payload fields
| Payload field | Becomes key | |
|---|---|---|
restaurant | place | venue | store | frequent_place | |
destination | frequent_destination | |
ride_type | ride_type_affinity | |
items[].cuisine | cuisine_affinity | |
items[].name | dish_affinity |
Anything else in the payload is stored but ignored for inference. Only text is full-text indexed.
Free-text search
/v1/searchLexical full-text search over logged episodes and active assertions. Multi-word queries AND their terms, falling back to OR when that finds nothing.
{"owner_id":"u_42","query":"goa trip","types":["episode"],
"kinds":["message"],"domains":[],"limit":10}{"hits":[{"type":"episode","id":"e_…","text":"…","kind":"message",
"occurred_at":"…","rank":1.9}],"elapsed_ms":0.6}Claude memory tool
LongMemory also implements Anthropic's client-side memory_20250818 tool contract, so Claude can manage its own notes and have them be durable, multi-user and searchable. Forward each memory tool_use block's input verbatim, plus an owner_id.
POST /v1/memory
{"owner_id":"u1","command":"create",
"path":"/memories/user.md","file_text":"name: AJ\n"}
→ {"content":"File created successfully at: /memories/user.md",
"is_error":false}Commands: view, create, str_replace, insert, delete, rename. Paths are jailed under /memories. Command-level failures return HTTP 200 with is_error: true — relay that straight back as the tool result.
| Endpoint | Purpose | |
|---|---|---|
GET /v1/memory/tool | Tool descriptor + wiring instructions (no auth) | |
POST /v1/memory/search | Rank an owner’s memory files (lexical + optional local vectors) | |
GET /v1/owners/{owner}/memory | List the whole /memories tree |
Free-text notes
/v1/memoriesWhen you just want to store a sentence and search it later — no schema, no paths. The server derives a path from the content, so re-sending identical text is idempotent. 1–100 items per batch.
{"owner_id":"u_42","memories":[
{"text":"Allergic to shellfish; carries an epipen.","topic":"health"},
{"text":"Works at Google as a software engineer."}
]}The server files each memory under an optional topic folder (default notes) plus a content hash, so identical text always lands on the same path:
{"stored":[
{"path":"/memories/health/9f2a41c83d10.md"},
{"path":"/memories/notes/5b11d02e77a4.md"}
]}Retrieve with POST /v1/memory/search. Configure LM_EMBED_MODEL to add a local static-embedding arm for paraphrase matches; without it, search is keyword-only and honest about it.
Extraction (optional)
/v1/extractText in, ops out, using the server's own Anthropic key (LM_ANTHROPIC_KEY; model LM_ANTHROPIC_MODEL, default claude-haiku-4-5-20251001). Returns 501 when not configured — by design.
{"owner_id":"u_42",
"text":"my wife Priya is vegetarian and I'm off sugar this month",
"at":"2026-07-07","apply":true}Response: {"ops":[...], "results":[...]} — results only with apply:true. This is the one endpoint that calls a model; the piggyback contract above is the recommended integration and costs nothing.
Owner data & GDPR
| Endpoint | Description | |
|---|---|---|
GET /v1/owners/{owner}/persons | All persons with aliases | |
GET /v1/owners/{owner}/profile?person=wife&history=1 | One person grouped by domain; history includes superseded/expired/retracted | |
GET /v1/owners/{owner}/export | Full dump: persons, assertions (all statuses), episodes | |
DELETE /v1/owners/{owner} | Erase everything for an owner (right to erasure) | |
DELETE /v1/owners/{owner}/persons/{id} | Erase one person and all their data |
Multi-tenant admin
Available when LM_ADMIN_KEY is set. A tenant is the trust boundary — give each customer their own tenant; never share one key across mutually untrusted users.
| Endpoint | Description | |
|---|---|---|
POST /v1/admin/tenants | Create a tenant → returns api_key once (only its SHA-256 is stored) | |
GET /v1/admin/tenants | List tenants | |
DELETE /v1/admin/tenants/{id} | Disable — revokes access, keeps data |
LM_RATE_RPS as instance self-protection; keep per-user quotas in your own gateway.Configuration
| Env / flag | Default | Meaning |
|---|---|---|
LM_ADDR | :8080 | Listen address |
LM_DB | ./longmemory.db | Database file path |
LM_ADMIN_KEY | unset | Set ⇒ multi-tenant mode with API-key auth |
LM_RATE_RPS | 0 (off) | Per-tenant rate limit, burst 2× |
LM_EMBED_MODEL | unset | Path to a local static-embedding model for semantic search |
LM_ANTHROPIC_KEY | unset | Optional: enables BYO-key POST /v1/extract |
LM_ALLOW_PUBLIC_OPEN | unset | Override the open-mode public-bind guard |
LM_LOG | info | debug logs every request with latency |
Also: GET /healthz (no auth) and GET /v1/stats for tenant-scoped counts.
Python package
The longmemory package bundles the compiled binary and a stdlib-only client. No dependencies, no server to start — the engine boots on loopback and stops with your process.
pip install longmemory
from longmemory import LongMemory
mem = LongMemory() # binary auto-starts on 127.0.0.1
mem.person("Priya", relation="wife")
mem.remember("wife", "food", "diet", "vegetarian", polarity="rule")
mem.remember("self", "food", "cuisine", "biryani", polarity="like",
context={"time_of_day": ["evening"]})
print(mem.context(persons=["self", "wife"], domains=["food"]))
# ### Memory (as of 2026-07-08 20:00, wed evening)
# - You: likes biryani (@evening)
# - Priya (wife): [RULE] diet: vegetarianMethods
| Method | Wraps | |
|---|---|---|
person(name, relation, aliases) | person_upsert | |
remember(person, domain, key, value, …) | assert | |
retract(person, key, …) | retract | |
apply_ops([…]) | raw /v1/ops batch | |
recall(persons, domains, at, …) | /v1/recall | |
context(**kwargs) | recall’s context_block | |
log_episode(kind, …) | /v1/episodes | |
search(query, …) | /v1/search |
Pass base_url to talk to an engine you already run, and api_key for multi-tenant mode. Prebuilt for macOS and Linux; elsewhere it raises EngineNotFound and you build from source.
Performance
Apple M2, single process. Reproduce with make bench.
| Operation | Latency | Throughput |
|---|---|---|
| Recall (3 persons, 240-assertion owner, ranked + rendered) | 0.78 ms | ~1,275/s per core |
| Assert, batched 50/request | 0.15 ms/op | ~6,600 writes/s |
| Assert, single op per request | 1.1 ms | ~925/s |
| Episode ingest (signals + inference check) | 4.0 ms | ~250/s |
| FTS search over 5k episodes | 0.62 ms | ~1,600/s |
Scaling
Recall is an indexed lookup bounded by one owner's set, so total corpus size barely matters. Measured with the recalled owner held at a realistic 240 assertions while the database grew around it:
| Total assertions in DB | Recall p50 | p90 |
|---|---|---|
| 240 | 0.83 ms | 0.98 ms |
| 2,400 | 0.84 ms | 1.02 ms |
| 24,000 | 0.84 ms | 1.00 ms |
| 240,000 | 0.83 ms | 1.04 ms |
| 2,400,000 (~1.2 GB) | 0.83 ms | 1.03 ms |
~0.5 KB per assertion on disk; ~30 MB RSS. If a single owner ever accumulated hundreds of thousands of assertions, that owner's recall would grow — cost tracks the size of the set being recalled, not the table.