Available nowBrains · Managed agent memory

Your agent already forgot. Brains is where it stops.

On Monday the assistant asked a customer which region their account was in. On Thursday it asked again. Nothing threw an error, nothing showed up in the logs and the customer quietly filed the product under toy. That's the failure, and it happens weeks before anyone writes a ticket about it.

Everyone patches it the same way. You stuff the last N messages into the prompt, which works until the conversation is longer than N. Then you dump every transcript into a vector store, which works until the store starts confidently returning a decision that got reversed four months ago. Brains is the third attempt, built as a service: you write facts, or you hand it a transcript and let it decide what was worth keeping, and it hands back a ranked set at call time.

Availability. Brains is live in production and every account can call /v1/brains right now. There is no per-account switch and no waitlist, so an API key is the only thing you need. If those routes answer 404, the RINGSIDE_BRAINS_ENABLED flag is off in that environment, which is what a self-hosted or staging deployment looks like.

What actually happens on a turn

Start at the moment a user says something. You either post the transcript to observe, or you put two headers on an ordinary chat completion and let Ringside do both ends. Either way the turn hits a cheap gate first. That gate is regex patterns you supply plus a short rubric, and its whole job is to throw away greetings, restatements of the request, routine lookups and ephemeral status before anything expensive runs. Most turns die here, which is the point.

What survives goes to a reconcile step. It reads the memories that already exist at that scope, capped at the most recent 60, and returns the smallest diff that makes the store correct: entries to create, entries to rewrite, entries to drop, the ids it actually leaned on, the entities and relationships it spotted and any claim that contradicts something already stored. If that step fails or comes back unparseable, nothing is written. Fail closed, every time, because a memory store that half-applies a bad plan is worse than one that missed a turn.

Creates and rewrites are split into paragraph chunks at 1100 characters, then embedded and written as rows against the scope path. The ids the plan says it used get their use_count bumped, which matters later. Contradictions land in a conflicts table instead of silently overwriting anything, and when a scope crosses 12 accumulated memories a consolidation pass merges the duplicates into one entry.

Then the next call arrives. Recall embeds the query, pulls every chunk in reach of the requested scope, scores each one and returns the top K. Default K is 8, clamped to 100. That is the whole loop.

Memory is not search

A vector index has no opinion about whether a fact is still true. The pricing decision your team reversed in March embeds just as cleanly as the one that replaced it, sits about the same distance from the question and comes back with the same confidence. Rank on closeness alone and you've built an agent that is fluent and wrong, which is a worse product than one that admits it doesn't know.

So closeness is one term out of five in the score, and the other four are all about time and standing. A decision carries no clock decay and ages only when something supersedes it. A status update halves in strength every four days on its own. Every recall can be asked to read the store as it stood at a past timestamp, which is what as_of does: entries whose validity window had already closed by that moment are skipped, entries that had not opened yet are skipped too.

Forgetting on a curve you choose, and knowing what replaced what, is the product. Retrieval is the easy half.

The score, in full

final = cosine × weight × recency × usage × graph_boost

Send explain: true on a recall and every hit comes back with that decomposition under factors, so when a customer tells you the agent surfaced the wrong thing you can point at the term that did it.

cosine

How close the query embedding sits to the chunk embedding. This is the only term a plain vector search has.

weight

The tier weight for the entry type. Tier A sits at 1.3, tier B at 1.2, tier C at 1.0 and tier D at 0.7, so an access detail outranks a passing status update at equal closeness.

recency

The forgetting curve for that type, measured from the policy decay anchor (last_used by default). Tier C halves every 60 days, tier D every 4. Tiers A and B carry no clock decay at all and age only when something replaces them.

usage

Bounded reinforcement, 1 + min(0.5, 0.15 * ln(1 + use_count)). A memory the agent keeps leaning on gets up to 50% more pull, and no more than that, so one hot fact can never bury the rest.

graph_boost

A flat 1.15 when the entry is reachable through the entity graph built from the same writes. That is what flips source from "vector" to "both" on the hit.

Types map to tiers through a taxonomy that ships with ten defaults and that you can replace wholesale per brain. Tier A is the stuff that breaks a deploy when it goes missing: infrastructure notes, access details, the gotchas nobody wrote down. Tier B is where decisions and commitments and the constraints you agreed to sit. C is softer, preferences and working relationships. D is the throwaway layer, incidents and current state, which is why it halves every four days. Nothing about those labels is baked in. Define type_taxonomy with your own types and your own curve per type and the engine is yours. Curve families available are exponential half-life, power, linear, cliff, an interpolated set of control points, or none.

Tier weights also tune themselves while the brain is in tuning: "auto", with a 5% exploration rate that occasionally promotes a below-rank candidate into the results so a useful memory that started underweighted gets a chance to prove itself. When you like where the weights landed, pin them with POST /v1/brains/{id}/autotune and action: "freeze". Reset puts you back on the baseline.

The calls

# /v1/brains is live and every account can call it today. A 404 on these
# routes means RINGSIDE_BRAINS_ENABLED is off in that environment.

# 0. The customer the memory belongs to (same object the rest of Ringside bills)
curl https://api.fightclub.pro/v1/customers \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"acme","display_name":"Acme Ltd","monthly_budget_usd":250}'
# => { "id": "cus_...", "display_name": "Acme Ltd", "monthly_budget_usd": 250, ... }

# 1. Create a brain
curl https://api.fightclub.pro/v1/brains \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"acme-support","encryption":"standard"}'
# => { "id": "brn_...", "object": "brain",
#      "embedding_model": "openai/text-embedding-3-small", "embedding_dims": 1536,
#      "encryption": "standard", "tuning": "auto", "autotune_frozen": false, ... }

# 2. Write something you already know. PUT, not POST.
curl -X PUT https://api.fightclub.pro/v1/brains/brn_abc/entries \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "FC-Customer: acme" \
  -d '{
    "scope": "/acme/billing",
    "class": "reference",
    "type": "constraint",
    "title": "Invoicing terms",
    "body": "Acme is invoiced annually in EUR. Never quote a monthly USD figure.",
    "trust": "high"
  }'
# => { "object": "brain.entry", "entry_id": "ent_...", "chunks": 1 }

# 3. Let it learn the rest from a real turn
curl https://api.fightclub.pro/v1/brains/brn_abc/observe \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "FC-Customer: acme" \
  -d '{
    "scope": "/acme/billing",
    "transcript": "user: we moved the renewal to March\n\nassistant: noted, March it is",
    "trust": "high"
  }'
# => 202 { "object": "brain.observe", "status": "learning" }
#    add "wait": true to block and get { changed, proposed, conflicts, used, costUsd }

# 4. Recall at call time, with the score broken out
curl https://api.fightclub.pro/v1/brains/brn_abc/recall \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "when does Acme renew, and in what currency?",
    "scope": "/acme/billing",
    "top_k": 6,
    "explain": true
  }'
# => { "object": "brain.recall", "data": [
#      { "entry_id": "ent_...", "scope": "/acme/billing", "class": "memory",
#        "type": "commitment", "content": "Renewal moved to March.",
#        "score": 1.132416, "trust": "high", "source": "both",
#        "factors": { "cosine": 0.79, "tier": "B", "weight": 1.2,
#                     "recency": 1, "usage": 1.104, "graph_boost": 1.15,
#                     "final": 1.132416 } } ] }

Beyond those: GET /v1/brains lists them, PATCH and DELETE /v1/brains/{id} edit and remove one, GET /v1/brains/{id}/entries pages the store with scope, class, limit and offset, and GET/PATCH/DELETE on a single entries/{entryId} read, edit or drop one learning. A PATCH that only moves the trust level does not re-embed. Passing entry_id on the PUT replaces an existing entry in place rather than adding a second copy of the same fact.

Scoping, and why one tenant cannot read another

A scope is an N-level path, /acme/billing/q3-audit, and it is the whole isolation model. A recall at that path sees exactly that path plus every ancestor above it, so /, /acme and /acme/billing. It never sees a sibling. Your platform-wide policy sits at root and reaches everybody, a tenant's facts sit under their own branch and reach only them, and a single engagement gets its own leaf. There is no ACL to configure and no filter to forget, because the path is the query.

Two switches change the reach. inherit: false pins recall to the exact scope and cuts the ancestors off, which is what you want when a leaf must not be coloured by defaults from above. subtree: true extends it downward across everything beneath the scope, bounded to the brain you own, which is how an internal operator tool searches the whole branch at once.

Above the scope tree sits the account boundary. Brains belong to a developer account, and a brain id that is not yours returns 404 rather than 403, the same rule the rest of the Ringside API follows, so a buggy agent in a multi-tenant app cannot probe for the existence of another account's brain. Attribution rides on the FC-Customer header or a user field in the body, and it does real work: embedding spend on writes and on recall is debited against that customer's wallet and counts toward the monthly_budget_usd you set on them. An unknown customer reference returns 404 with customer_not_found.

What is stored, and how

Postgres, jsonb, brute-force cosine

Embeddings live in a jsonb column and recall scores them in process. There is no pgvector dependency and no separate vector database to operate, which is a deliberate call: a brain holds one tenant's working memory rather than a document corpus, so the candidate set is small enough that an exact scan beats an approximate index and never returns a near-miss. Default embedding model is openai/text-embedding-3-small at 1536 dimensions, settable per brain at create.

Three encryption modes

none stores plaintext. standard seals all stored text with AES-256-GCM under a per-brain key. strict seals the embedding vectors as well, and recall decrypts the corpus once into a warm in-memory index rather than per query. Raw embeddings can be partially inverted back toward the text that produced them, which is the reason strict exists. This is encryption at rest under a key we hold, not a zero-knowledge boundary, and the API surface is identical in all three modes.

Three classes of entry

reference is material you wrote in and own, the handbook and the contract terms. memory is what the agent learned from turns. policy is the standing rules. Filter recall to any subset with classes. They share one store and compete in one ranking, so a chunk from an uploaded document and a fact learned last Tuesday are judged by the same score, and the closer one wins.

Deletion that actually deletes

POST /v1/brains/{id}/forget takes one of scope, source or entity. Forget by scope cascades every descendant and reports how many rows went, which is the shape a data-subject erasure request needs: give the customer their own branch, and one call empties it. Deleting the brain cascades everything under it at the database level.

When you don't trust the source

Learning from end-user conversations means a stranger can try to write to your agent's memory. Mark those turns trust: "low" and two things change. Low-trust facts cannot promote themselves into a protected tier, and when they come back on recall the injected context puts them in a separate low-confidence block that tells the model not to act on them. The recalled block itself is fenced and labelled as data rather than instructions, which is the cheap defence against a user who writes their prompt injection into your memory store and waits for it to be read back.

If autonomous writes are too much, set learn_mode: "review" on the whole brain or per tier. Staged writes queue at GET /v1/brains/{id}/proposals and you approve or reject each one by id. Contradictions surface the same way at /conflicts, carrying the id of the entry being contradicted plus the claim and the detail, and you close one with a conflict_id. A brain that quietly resolves every contradiction on its own is one you can't audit, so it asks.

Brain Packs, coming soon

Not yet available

Ten thousand tenants who all need the same thousand chunks of your platform knowledge underneath their private material is a copy problem. Seed that base into every brain and one edited sentence means rewriting ten thousand copies, each sealed under a different key. Packs let you publish the shared base once as a versioned artifact and mount it into each brain, either by reference against the single stored copy or materialized into the consumer's own rows. Mounted content scores evergreen, skipping clock decay and the consumer's reinforcement, because curated knowledge isn't episodic. Native memories shadow a mounted row they contradict.

Packs are built and sit behind a second flag, RINGSIDE_BRAIN_PACKS_ENABLED, which is off. The publish and mount routes return 404 today even though Brains itself is live. Build against the contract if you want, but expect the 404 until we flip it.

Brains is live

Get an API key, create a brain and recall works on the next request. No slot to request, no flag to ask for.