BackAPI Reference

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?

You run it yourself. LongMemory is a single static binary with its storage engine built in — it runs on your own machine, not a hosted endpoint. Every example below assumes it's listening on 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:

bash
go build -o bin/longmemory ./cmd/longmemory
./bin/longmemory        # :8080, ./longmemory.db, open single-tenant mode

Remember two preferences:

bash
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:

bash
curl -s localhost:8080/v1/recall -d '{"owner_id": "u1"}'

The response carries a prompt-ready context_block:

text
### 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.

ModeWhenTenant routesAdmin routes
Open single-tenantLM_ADMIN_KEYNo auth; implicit tenant "default"501
Multi-tenantLM_ADMIN_KEY setAuthorization: Bearer <key>X-Admin-Key
Open mode serves every route with no authentication. The binary refuses to start in open mode on a public address, and downgrades a wildcard bind to loopback — set 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

OwnerYour end user. Every row is scoped to an owner_id (and a tenant). Owners never see each other's data.
PersonAnyone the owner talks about, including themselves ("self"). Resolved by id, name, alias or relation — "my wife" matches Priya. Unknown refs auto-create a person.
AssertionOne typed preference fact: person × domain × key × value × polarity × validity window × context. This is the unit of memory.
Polarityrule (hard constraint — diet, allergy; pinned first in recall), like/dislike (soft taste), fact (neutral attribute, default).
Conflict group(person, domain, key, context-signature). A new indefinite assertion supersedes others in its group; multi-valued keys add the value, so “likes pizza” and “likes sushi” coexist.
Temporary vs indefiniteNo 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.
Provenancestated (confidence 1.0), imported (0.8), inferred (0.5, decays). Inferred facts get promoted to stated the moment the user says it out loud.
EpisodeA raw event — an order, a ride, a message. Episodes distill into signals; repeated signals become inferred preferences.

Memory operations

POST/v1/ops

Apply 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.

json
{
  "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

opRequiredNotes
assertperson, domain, key, valueCreates, reinforces on repeat, or supersedes conflicting facts
reinforceassertion_id or person+keyBumps observed_count; inferred confidence +0.1
retractassertion_id or person+keyMarks matching active assertions retracted (audit kept)
person_upsertnameMatches existing person by name/alias; sets relation, aliases
person_aliasperson, aliasesAdds aliases to an existing person
person_mergeinto, fromReassigns all data, tombstones the merged-from person
forget_personpersonHard-deletes a person and everything about them

assert fields

FieldTypeDescription
personstring"self", a p_… id, or a name/alias/relation. Auto-creates if unknown.
domainstringfood, dining, grocery, ride, place, schedule, general…
keystringsnake_case: diet, allergy, cuisine, budget_per_meal, home_address…
valueanyUsually a short string.
polarityenumrule | like | dislike | fact (default fact)
contextobjecttime_of_day[], days[], location, occasion — only when genuinely conditional
valid_fromRFC3339For preferences that start in the future
valid_untilRFC3339Makes it temporary: shadows, then auto-reverts
provenanceenumstated (default) | inferred | imported
confidencefloat0–1; overrides the provenance default
cardinalityenumsingle | multi — whether values replace or accumulate
relationstringSets the relation when auto-creating the person

Response

json
{"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

POST/v1/recall

Everything 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.

json
{
  "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

json
{
  "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
}
Ambiguous person references are never guessed. They come back in 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

POST/v1/episodes

Log 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.

json
{
  "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 fieldBecomes key
restaurant | place | venue | storefrequent_place
destinationfrequent_destination
ride_typeride_type_affinity
items[].cuisinecuisine_affinity
items[].namedish_affinity

Anything else in the payload is stored but ignored for inference. Only text is full-text indexed.

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.

bash
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.

EndpointPurpose
GET /v1/memory/toolTool descriptor + wiring instructions (no auth)
POST /v1/memory/searchRank an owner’s memory files (lexical + optional local vectors)
GET /v1/owners/{owner}/memoryList the whole /memories tree

Free-text notes

POST/v1/memories

When 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.

json
{"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:

json
{"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)

POST/v1/extract

Text 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.

json
{"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

EndpointDescription
GET /v1/owners/{owner}/personsAll persons with aliases
GET /v1/owners/{owner}/profile?person=wife&history=1One person grouped by domain; history includes superseded/expired/retracted
GET /v1/owners/{owner}/exportFull 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.

EndpointDescription
POST /v1/admin/tenantsCreate a tenant → returns api_key once (only its SHA-256 is stored)
GET /v1/admin/tenantsList tenants
DELETE /v1/admin/tenants/{id}Disable — revokes access, keeps data
Rate limits are per tenant, not per end user, and default to off. Set LM_RATE_RPS as instance self-protection; keep per-user quotas in your own gateway.

Configuration

Env / flagDefaultMeaning
LM_ADDR:8080Listen address
LM_DB./longmemory.dbDatabase file path
LM_ADMIN_KEYunsetSet ⇒ multi-tenant mode with API-key auth
LM_RATE_RPS0 (off)Per-tenant rate limit, burst 2×
LM_EMBED_MODELunsetPath to a local static-embedding model for semantic search
LM_ANTHROPIC_KEYunsetOptional: enables BYO-key POST /v1/extract
LM_ALLOW_PUBLIC_OPENunsetOverride the open-mode public-bind guard
LM_LOGinfodebug 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.

bash
pip install longmemory
python
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: vegetarian

Methods

MethodWraps
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.

OperationLatencyThroughput
Recall (3 persons, 240-assertion owner, ranked + rendered)0.78 ms~1,275/s per core
Assert, batched 50/request0.15 ms/op~6,600 writes/s
Assert, single op per request1.1 ms~925/s
Episode ingest (signals + inference check)4.0 ms~250/s
FTS search over 5k episodes0.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 DBRecall p50p90
2400.83 ms0.98 ms
2,4000.84 ms1.02 ms
24,0000.84 ms1.00 ms
240,0000.83 ms1.04 ms
2,400,000 (~1.2 GB)0.83 ms1.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.