# Brain Memory API — complete agent context
# Brain Memory API

> Keep a sourced history of changing product facts. Retrieve the records that apply, with
> citations, gaps, conflicts, processing state, budget, and cost attached.

## Start

- [Documentation](https://docs.brain.new/docs.html)
- [Product in Markdown](https://docs.brain.new/home.md)
- [Quickstart](https://docs.brain.new/quickstart.md)
- [Agent integration rules](https://docs.brain.new/agent.md)
- [OpenAPI 3.1 contract](https://docs.brain.new/openapi.yaml)
- [Complete guide as Markdown](https://docs.brain.new/full-guide.md)

## Build

- [API reference](https://docs.brain.new/api.md)
- [Errors](https://docs.brain.new/errors.md)
- [SDKs](https://docs.brain.new/sdks.md)
- [MCP](https://docs.brain.new/mcp.md)
- [Connect Brain](https://docs.brain.new/integrate.md)

## Trust

- [Status checks](https://docs.brain.new/status.md)
- [Known limitations](https://docs.brain.new/limits.md)
- [Measurements](https://docs.brain.new/measurements.md)
- [Changelog](https://docs.brain.new/changelog.md)

Start read-only. The public sandbox uses InMemoryStore and proves contract behavior, not durable
SpineStore memory.

---

# Give your product a Brain.

Write what happened. Ask what matters. Get current context with the sources attached.

## What goes in. What comes back.

Records keep the original input. Brain tracks what changes over time. Context returns the
evidence that applies, its citations, and any gaps.

## Two calls. Records you can trace.

Remember a Record. Request Context. The response carries the relevant source text, citations,
missing evidence, conflicts, and cost.

## Five parts, each with one job

- **Brain** — the account boundary for one product.
- **Record** — a source item with content, origin, and time.
- **Memory** — a versioned finding drawn from source Records.
- **Object** — a fact your application has accepted.
- **Context** — the evidence returned for one request.

## When facts change, the record shows it.

New evidence does not silently erase old evidence. Time, source, ownership, sensitivity, and
processing state remain part of what can be returned.

## Same API, different products

Brain Mac is the first read-only integration. BookWoof, AiCMO, Pinnie Health, finance, video
agents, and GTM products can use the same API with different source Records and application-owned
Objects.

## What remains open

Customer SpineStore is live behind issued keys. Context now returns selected, cited Memories.
Conflict population, accepted-Object arbitration, complete quality/latency SLOs, and the visible
Brain Mac Ask/Search migration remain open. The public sandbox is disposable and contains no
customer memory.

---

# Documentation

Start with one Brain, save one Record, and retrieve its evidence. Then add the operations your
product needs.

## Start

- [Quickstart](quickstart.html) — save a Record and retrieve its evidence in two calls.
- [Playground](playground.html) — run `createContext` against this origin.
- [Project Console](dashboard.html) — connect a key and inspect one Brain through existing reads.
- [Complete Markdown guide](full-guide.md) — one downloadable guide for an agent or IDE.

## Build

- [API reference](api.html) — all five resources, fifteen operations, parameters, and schemas.
- [OpenAPI](openapi.yaml) — the canonical machine contract.

## Integrate

- [Connect Brain](integrate.html) — install, connect an agent, verify citations, repair, and revoke.
- [SDKs](sdks.html) — generated TypeScript and Python clients.
- [MCP](mcp.html) — bounded memory tools for agents.
- [Agents & IDEs](agents.html) — safe context for Codex, Claude, Cursor, Copilot, and Windsurf.
- [Use cases](use-cases.html) — the same primitives across personal, health, finance, agent,
  operations, and GTM workloads.

## Trust

- [Errors](errors.html) — stable error shape and anchored remediation.
- [Measurements](measurements.html) — dated evidence with dataset tags.
- [Status checks](status.html) — customer view, doctor, deployment identity, and release gates.
- [Known limitations](limits.html) — what is incomplete today.

---

# Quickstart — from nothing to a cited Context

**Date:** 2026-07-27 · Ten minutes, three calls, no database.
Every response on this page is **pasted from a real run**. The run is reproduced at the bottom.

---

## The whole thing

```
POST /v1/brains                    a Brain, and who it is a memory OF
POST /v1/brains/{id}/records       remember anything
POST /v1/brains/{id}/context       cited, budgeted, honest context
```

Everything else here is detail you can skip until you need it.

---

## 0. Start a server — 30 seconds

From this repo:

```bash
cd your checkout/brain-platform
.venv/bin/python -m uvicorn memory_api.app:app --port 8788
```

Anywhere else — Python 3.12+, two packages, no database, no key to sign up for:

```bash
pip install fastapi uvicorn
python -m uvicorn memory_api.app:app --port 8788
```

Wait for `Uvicorn running on http://127.0.0.1:8788`. Interactive docs are at `/docs`.

> **What you are running, plainly.** `memory_api.app` mounts `InMemoryStore`: the conformance
> store. It is process-local (restart and it is empty), and its retrieval is a deliberately boring
> substring scan. It implements the *guarantees* — citations, ownership, unknowns, watermarks,
> idempotency, tenant isolation — and none of the intelligence. The engine that does the real
> retrieval is `SpineStore` (`memory_api/spine.py`) over Postgres and the lanes; both satisfy the
> one `Store` protocol, so switching is a single line in `app.py` and no route changes. That is why
> the shapes on this page are real even though its recall is not — and why pointing `BRAIN` at
> another deployment of the same contract changes nothing below.

---

## 1. Your key is your tenant

```bash
export BRAIN=http://localhost:8788/v1
export KEY=sk_test_quickstart_ada
```

The format is `sk_<test|live>_<project>_<secret>`. The project segment is in the **credential**, not
in a request field, because a request field is something a caller can change. Two keys with
different project segments cannot see each other's Brains — proven in step 9. Any
well-formed key works against the local server; `test` and `live` are separate namespaces, so test
data cannot reach live data even by mistake.

Responses below are pretty-printed. Pipe to `jq` or `python3 -m json.tool` to get the same.

---

## 2. Create a Brain — with `self`



```bash
curl -s -X POST $BRAIN/brains \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"id":"ada","name":"Ada","self":{"type":"person","name":"Ada Lovelace"}}'
```

```json
{
    "id": "ada",
    "object": "brain",
    "created_at": "2026-07-27T18:43:42Z",
    "self": {
        "type": "person",
        "name": "Ada Lovelace"
    },
    "name": "Ada"
}
```

`self` is the principal this Brain is a memory **of** — `{type: person | org, name}`. It is what
makes the word "my" resolvable. **Omit it and every possessive question abstains.** That is the
ownership guarantee, not a bug, and step 5 shows both sides of it.

---

## 3. Remember something

Content is a plain string, or a typed object — `{kind: message | document | event | fact}`:

| kind | required | keeps |
|---|---|---|
| `message` | `text` | `speaker`, `thread` — a thread is what a session IS here; you never create one |
| `document` | `text` | `title` |
| `event` | `title` | `starts_at`, `ends_at`, `participants`, `place` |
| `fact` | `subject`, `predicate`, `value` | nothing to extract; it is already a fact |

A string is accepted and costs you the structure: flatten a message to prose and the speaker and the
thread are gone, and no later stage can get them back.


```bash
curl -s -X POST $BRAIN/brains/ada/records \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: qs-msg-1" \
  -d '{"content":{"kind":"message","text":"my home address is 12 Analytical Way, Turin","speaker":"Ada Lovelace","thread":"sms:+15550100"}}'
```

```json
{
    "id": "rec_01KYJE7NQ44HR4RJ31VDV0DKKE",
    "object": "record",
    "status": "ready",
    "occurred_at": "2026-07-27T18:43:42Z",
    "visibility": "shareable",
    "memory_ids": [
        "mem_01KYJE7NQ4Y35T92H30MKDPQFD"
    ],
    "created_at": "2026-07-27T18:43:42Z"
}
```

**Read `status`, do not assume it.** `202` means durably accepted, not finished. The local store
derives synchronously so you get `ready` immediately; a server that derives in the background
returns `processing`, and the Record becomes `ready` when its Memories are admitted. Poll
`GET /brains/{id}/records/{record_id}`.

Two more, so there is something to ask about:


```bash
curl -s -X POST $BRAIN/brains/ada/records \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":{"kind":"fact","subject":"Ada Lovelace","predicate":"prefers","value":"an aisle seat"}}'

curl -s -X POST $BRAIN/brains/ada/records \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":{"kind":"event","title":"Turin standup","starts_at":"2026-07-31T09:00:00Z","participants":["Ada Lovelace","Charles Babbage"]}}'
```

Set `occurred_at` for anything historical. It defaults to now, and "now" is wrong for every import
you will ever do — it is what `as_of` reads later.

---

## 4. Get Context — this is the product


```bash
curl -s -X POST $BRAIN/brains/ada/context \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"query":"what is my home address?","include":["memories","citations","unknowns"]}'
```

```json
{
    "id": "ctx_01KYJE7NVFGKA11WPG28CTVRNA",
    "object": "context",
    "status": "ready",
    "query": "what is my home address?",
    "watermark": "wm_000000000004",
    "text": "my home address is 12 Analytical Way, Turin",
    "citations": [
        {
            "record_id": "rec_01KYJE7NQ44HR4RJ31VDV0DKKE",
            "quote": "my home address is 12 Analytical Way, Turin"
        }
    ],
    "conflicts": [],
    "unknowns": [],
    "budget": {
        "requested_tokens": 4000,
        "estimated_tokens": 10,
        "truncated": false
    },
    "cost": {
        "generative_calls": 0,
        "processing_units": 0.0
    },
    "memories": [
        {
            "id": "mem_01KYJE7NQ4Y35T92H30MKDPQFD",
            "object": "memory",
            "statement": "my home address is 12 Analytical Way, Turin",
            "subject": "record",
            "owner": {
                "state": "self",
                "entity": "Ada Lovelace"
            },
            "valid_from": "2026-07-27T18:43:42Z",
            "valid_to": null,
            "version": 1,
            "status": "active",
            "citations": [
                {
                    "record_id": "rec_01KYJE7NQ44HR4RJ31VDV0DKKE",
                    "quote": "my home address is 12 Analytical Way, Turin"
                }
            ]
        }
    ]
}
```

Four things in that response earn their place:

- **`text`** is the block you paste straight into a prompt. Not chunks for you to assemble.
- **`citations`** name a `record_id` you can fetch. Every assertion traces to something that entered.
- **`cost` is three numbers, and this page's server is the fixture.** The response above comes from
  `InMemoryStore`, which calls no model at all, so every cost field on it is a true `0`. On the
  hosted spine the default `mode=fast` reports **`model_calls_total: 1 · embedding_calls: 1 ·
  generative_calls: 0`** — exactly one model call, the query embedding, and nothing generated.
  Read from the ledger, never hardcoded: this was a hardcoded `0` until 2026-07-29 while a
  generative query rewrite ran on every request, and then briefly a `1` under a field named
  *generative* for a call that was an embedding. One integer could not say both "it called a model"
  and "it composed nothing", so there are three. Assert the ones you care about in your tests; they
  are a contract, not an implementation detail.
- **`budget.truncated`** is visible. When context is cut to fit, you are told, not quietly served less.

You did not configure any of it. Citations on, conflicts surfaced, unknowns explicit, attribution
enforced — the defaults are the product. If a default needs a knob to become correct, the default
is wrong.

---

## 5. The two honest answers

This is the part worth reading twice. Most memory APIs have exactly one failure mode: they answer
anyway.

### A miss is not a denial


```bash
curl -s -X POST $BRAIN/brains/ada/context \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"query":"what is my passport number?"}'
```

```json
{
    "id": "ctx_01KYJE7NWV74QAQWJS0MWXDW13",
    "object": "context",
    "status": "ready",
    "query": "what is my passport number?",
    "watermark": "wm_000000000004",
    "text": null,
    "citations": [],
    "conflicts": [],
    "unknowns": [
        "what is my passport number?"
    ],
    "budget": {
        "requested_tokens": 4000,
        "estimated_tokens": 0,
        "truncated": false
    },
    "cost": {
        "generative_calls": 0,
        "processing_units": 0.0
    },
    "memories": []
}
```

`unknowns` names the question that could not be answered. Nothing was invented, and nothing was
coerced to `false`, `0`, or `""` — "we have no evidence" and "it is not so" are different claims and
this API never conflates them.

### No `self`, no possessive answer

Create a second Brain, omitting `self`, and give it the **same record**:


```bash
curl -s -X POST $BRAIN/brains \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"id":"anon"}'

curl -s -X POST $BRAIN/brains/anon/records \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":{"kind":"message","text":"my home address is 12 Analytical Way, Turin","speaker":"Ada Lovelace","thread":"sms:+15550100"}}'
```

Now ask the question that worked a moment ago:


```bash
curl -s -X POST $BRAIN/brains/anon/context \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"query":"what is my home address?"}'
```

```json
{
    "id": "ctx_01KYJE7P16KWMZ8AYD5GRRC0XR",
    "object": "context",
    "status": "ready",
    "query": "what is my home address?",
    "watermark": "wm_000000000006",
    "text": null,
    "memories": [],
    "citations": [],
    "conflicts": [],
    "unknowns": [
        "ownership of: what is my home address?"
    ],
    "abstained": {
        "reason": "no_attribution"
    },
    "budget": {
        "requested_tokens": 4000,
        "estimated_tokens": 0,
        "truncated": false
    },
    "cost": {
        "generative_calls": 0,
        "processing_units": 0.0
    }
}
```

Same evidence, same question, no answer — because nobody said whose Brain this is. The unknown is
not the address, it is the **ownership** of the question, and the API says so.

This is the guarantee: a Memory carries `owner: {state: self | other | unresolved}`, and
`unresolved` abstains rather than guesses. Somebody else's address in your inbox is not your
address, and a system that returns it as yours is confidently wrong — the failure that costs a
customer, and the one nobody else's scoreboard has a column for. `abstained.reason` is one of
`no_evidence · conflicting_evidence · no_attribution · out_of_policy`, so you can branch on which
kind of silence you got.

Pass `self` at create time and the whole class disappears. It is one field.

---

## 6. Read your own write

Every write returns a `Brain-Watermark`:


```bash
curl -s -D- -o /dev/null -X POST $BRAIN/brains/ada/records \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content":"I switched to a standing desk"}' | grep -i -E "^(HTTP|brain-watermark)"
```

```
HTTP/1.1 202 Accepted
brain-watermark: wm_000000000007
```

Send it back on the next Context and the response is guaranteed to be at or after that boundary.
If the server cannot serve it yet you get `202` with `status: "processing"` — never older evidence
labelled current:


```bash
curl -s -w "\nHTTP %{http_code}\n" -X POST $BRAIN/brains/ada/context \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Brain-Watermark: wm_000000009999" \
  -d '{"query":"what is my home address?"}'
```

```
{"id":"ctx_01KYJE7P305B6PH67CCYHD7H33","object":"context","status":"processing","query":"what is my home address?","watermark":"wm_000000000007","citations":[],"unknowns":["what is my home address?"],"budget":{"estimated_tokens":0,"truncated":false}}
HTTP 202
```

Retry until `status` is `ready`. Omit the header and you get the freshest thing available, which is
the right default for reads that are not chasing a write you just made.

---

## 7. Retry without fear

Replay step 3 verbatim — same `Idempotency-Key`, same body:

```json
{
    "id": "rec_01KYJE7NQ44HR4RJ31VDV0DKKE",
    "object": "record",
    "status": "ready",
    "occurred_at": "2026-07-27T18:43:42Z",
    "visibility": "shareable",
    "memory_ids": [
        "mem_01KYJE7NQ4Y35T92H30MKDPQFD"
    ],
    "created_at": "2026-07-27T18:43:42Z"
}
```

The **original** Record, same id, no second Memory. Send a key on every write from a retrying
client; on the spine it is a `UNIQUE` constraint, so the database is the lock and there is no window
where two retries both win.

---

## 8. See what the system inferred


```bash
curl -s "$BRAIN/brains/ada/memories?limit=1" -H "Authorization: Bearer $KEY"
```

```json
{
    "data": [
        {
            "id": "mem_01KYJE7NQ4Y35T92H30MKDPQFD",
            "object": "memory",
            "statement": "my home address is 12 Analytical Way, Turin",
            "subject": "record",
            "owner": {
                "state": "self",
                "entity": "Ada Lovelace"
            },
            "valid_from": "2026-07-27T18:43:42Z",
            "valid_to": null,
            "version": 1,
            "status": "active",
            "citations": [
                {
                    "record_id": "rec_01KYJE7NQ44HR4RJ31VDV0DKKE",
                    "quote": "my home address is 12 Analytical Way, Turin"
                }
            ]
        }
    ],
    "has_more": true,
    "next_cursor": "mem_01KYJE7NRHXP1D8HR3GE856Y39"
}
```

`citations` is never empty: a Memory that cannot name its evidence is not written. `valid_to: null`
means current — supersession writes a new version and keeps the old one, so history is never
overwritten. Page with `?cursor=<next_cursor>`.

Four layers, and no layer silently acquires the authority of the layer after it:

```
Record  = what entered        Memory  = what the system inferred
Object  = what YOUR APP accepted (POST /objects — the only place authority moves; a model may never call it)
Context = what is eligible for this task
```

---

## 9. When something goes wrong

One shape, every failure, no exceptions — `{type, code, message, request_id, docs_url}`:

```
$ curl -s $BRAIN/brains/ada                                          # no key
{"type":"authentication","code":"invalid_api_key","message":"expected `sk_<test|live>_<project>_<secret>`",...}   401

$ ... -d '{"content":{"kind":"note","text":"hello"}}'                # unknown kind
{"type":"invalid_request","code":"invalid_request_body","message":"content: Input tag 'note' found using 'kind' does not match any of the expected tags: 'message', 'document', 'event', 'fact'",...}   400

$ ... -d '{"content":"hello","visibility":"pubic"}'                  # typo in an enum
{"type":"invalid_request","code":"invalid_request_body","message":"visibility: Input should be 'local_only', 'redacted_summary' or 'shareable'",...}   400

$ curl -s $BRAIN/brains/ada -H "Authorization: Bearer sk_test_someoneelse_zzz"
{"type":"not_found","code":"brain_not_found","message":"no Brain 'ada'",...}   404

$ curl -s -X POST $BRAIN/brains -d '{"id":"ada"}' ...                # already exists
{"type":"conflict","code":"brain_exists","message":"Brain 'ada' exists",...}   409
```

`type` is the branch you write code against — `invalid_request · authentication · permission ·
not_found · conflict · rate_limit · policy · internal`. `request_id` is what a support ticket
quotes. A typo'd query parameter is refused rather than ignored (`?limitt=5` → `400
unknown_query_parameter`), because a bound you asked for and did not get is worse than an error.

Note the fourth one: another project's key gets `404`, not `403`. A different tenant is not told
that `ada` exists.

---

## The same thing in TypeScript

No dependencies — `fetch` is built in. Run it with `node quickstart.ts`: Node 24 strips the types
itself, no flags and no build step (that is what was run below). On older Node, drop the two type
annotations and save it as `.mjs`.

```ts
const BASE = "http://localhost:8788/v1";
const KEY = "sk_test_quickstart_ada";

async function brain(path: string, init: RequestInit = {}): Promise<any> {
  const res = await fetch(BASE + path, {
    ...init,
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", ...init.headers },
  });
  const body = await res.json();
  // Every failure is the same shape, so there is one error branch to write, not one per endpoint.
  if (!res.ok) throw new Error(`${body.type}/${body.code}: ${body.message}`);
  return body;
}

const post = (path: string, payload: unknown) =>
  brain(path, { method: "POST", body: JSON.stringify(payload) });

// 1. A Brain, WITH self. Omit `self` and every possessive question below abstains.
await post("/brains", { id: "ada-ts", self: { type: "person", name: "Ada Lovelace" } });

// 2. Remember. A typed record keeps the speaker and the thread; a string would lose both.
await post("/brains/ada-ts/records", {
  content: {
    kind: "message",
    text: "my home address is 12 Analytical Way, Turin",
    speaker: "Ada Lovelace",
    thread: "sms:+15550100",
  },
});

// 3. Context — cited, budgeted, and honest when it has nothing.
const ctx = await post("/brains/ada-ts/context", {
  query: "what is my home address?",
  include: ["citations", "memories"],
});

console.log("text:      ", ctx.text);
console.log("abstained: ", ctx.abstained ?? null);
console.log("unknowns:  ", ctx.unknowns);
for (const c of ctx.citations) console.log("cited:     ", c.record_id, "|", c.quote);
console.log("generative calls:", ctx.cost.generative_calls);
```

```
$ node quickstart.ts
text:       my home address is 12 Analytical Way, Turin
abstained:  null
unknowns:   []
cited:      rec_01KYJE930AKFMCK6BEDPB7JQXX | my home address is 12 Analytical Way, Turin
generative calls: 0
```

---

## The same thing in Python

Standard library only, so there is nothing to install. `httpx` or `requests` work the same way.

```python
import json
import urllib.error
import urllib.request

BASE, KEY = "http://localhost:8788/v1", "sk_test_quickstart_ada"


def brain(path, payload=None, method="GET"):
    req = urllib.request.Request(
        BASE + path, method=method,
        data=json.dumps(payload).encode() if payload is not None else None,
        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req) as res:
            return json.load(res)
    except urllib.error.HTTPError as exc:
        # Every failure is the same shape, so there is one error branch to write, not one per call.
        err = json.load(exc)
        raise SystemExit(f"{err['type']}/{err['code']}: {err['message']}")


def post(path, payload):
    return brain(path, payload, "POST")


# 1. A Brain, WITH self. Omit `self` and every possessive question below abstains.
post("/brains", {"id": "ada-py", "self": {"type": "person", "name": "Ada Lovelace"}})

# 2. Remember. A typed record keeps the speaker and the thread; a string would lose both.
post("/brains/ada-py/records", {"content": {
    "kind": "message",
    "text": "my home address is 12 Analytical Way, Turin",
    "speaker": "Ada Lovelace",
    "thread": "sms:+15550100",
}})

# 3. Context — cited, budgeted, and honest when it has nothing.
ctx = post("/brains/ada-py/context", {
    "query": "what is my home address?",
    "include": ["citations", "memories"],
})

print("text:      ", ctx["text"])
print("abstained: ", ctx.get("abstained"))
print("unknowns:  ", ctx["unknowns"])
for c in ctx["citations"]:
    print("cited:     ", c["record_id"], "|", c["quote"])
print("generative calls:", ctx["cost"]["generative_calls"])
```

```
$ python quickstart.py
text:       my home address is 12 Analytical Way, Turin
abstained:  None
unknowns:   []
cited:      rec_01KYJE932SGWXWC0ZQFDNZ4FCW | my home address is 12 Analytical Way, Turin
generative calls: 0
```

---

## What the local server will not do for you

So you do not mistake the reference for the engine:

- **Retrieval is a substring scan.** Your question must share a word with the evidence. `"where do
  I live?"` misses the address record on this server and returns `unknowns` — verified. The lanes
  that make that question work (state, vector, person, graph, fused) are the engine's, not the
  contract's.
- **One Memory per Record, `subject: "record"`.** No extraction, no entity resolution, no
  supersession, no conflicts.
- **`include: ["explain"]` now returns a real lane breakdown — the fixture emits it honestly (it was empty until the trace work landed, so an older copy of this doc said otherwise).** The contract declares it; this store does not
  implement it.
- **Nothing is durable.** Restart the process and you have an empty server.
- **`mode: "thorough"` is accepted and returns exactly what `fast` returns.** ~~and then reports
  `cost.generative_calls: 1` — a call it did not make.~~ *(Fixed 2026-07-29: the fixture used to
  report a placeholder `1` for `thorough` while making no call at all. It now reports
  `model_calls_total: 0 · embedding_calls: 0 · generative_calls: 0` on **both** modes, which is the
  truth about a store that calls no model — verified by running it.)* **Do not calibrate cost
  against this server either way:** on the spine the two modes genuinely differ, `fast` spending one
  embedding and `thorough` adding a generative query rewrite.

What it *does* do is the whole reason it exists: it makes the guarantees testable without a
database, and it is what the conformance fuzzer runs against.

---

## Next

- [`openapi.yaml`](./openapi.yaml) — the contract, and the source of truth. Not the server's
  generated schema.
- [`INTEGRATION_GUIDE.md`](./INTEGRATION_GUIDE.md) — three real applications on the same five
  resources, with no per-customer branch anywhere.
- [`BACKLOG_10X.md`](./BACKLOG_10X.md) — what is being built next, and the measurement behind each.
- The seven read knobs you have not touched yet: `purpose · as_of · budget · include · schema ·
  filter · mode`. You reached a cited answer without any of them, which is the point.

---

## Verified

Every command and response above was executed on 2026-07-27 against
`.venv/bin/python -m uvicorn memory_api.app:app --port 8788` (`InMemoryStore`, Python 3.13.13).
Nothing here is illustrative.

- **Mechanically re-checked.** Every command on this page was re-run in order against a freshly
  started server and each of the seven pasted JSON responses was diffed against the live one.
  All seven match once generated ids and timestamps are normalised — that is the only thing
  that differs between runs.
- The whole curl sequence takes **about a second** of machine time. The ten minutes is you reading.
- **The bare-install path works and has a floor.** `python3.13 -m venv` + `pip install fastapi
  uvicorn` (fastapi 0.140.7 / pydantic 2.13.4 / uvicorn 0.51.0, no repo dependencies) served the
  same cited Context. The same venv on **Python 3.9 fails to start** —
  `Unable to evaluate type annotation 'str | None'`. The package declares `requires-python >=3.12`.
- The TypeScript file ran on Node v24.18.0 via `node quickstart.ts`, no flags, no packages;
  the type-stripped `.mjs` variant ran too. The Python file ran on 3.13.13, standard library only.
- Every claim in prose was checked the same way, including the ones that make this page look worse:
  the `"where do I live?"` miss, `include: ["explain"]` returning nothing, and `thorough`'s
  fabricated `generative_calls: 1`.
- Gates on this code: `pytest memory_api/tests/` 14 passed / 3 skipped ·
  `scripts/check_api_simplicity.py` **GREEN** · schemathesis against the checked-in contract,
  3 known findings — **identical before and after** the fixes this page's first run produced.

---

# Playground

Prepare any of the fifteen operations from the published OpenAPI contract, inspect the exact cURL,
and run it against the API that served the page.

The playground derives its API base from the current origin. A key stays in the password field and
the request: no cookie, analytics, query-string credential, `localStorage`, `sessionStorage`, or
third-party request.

Use the playground for contract exploration. Use the generated clients or direct HTTP from your
server for application code.

Destructive operations require a second confirmation. Selecting an operation never sends it.

---

# Project Console

Connect one API base, Brain ID, and project key. The read-only console verifies the Brain first,
then loads identity, Record census, Memories, and recent Context traces independently.

There is no account login yet. A project key is the session. Create a disposable Brain in the
quickstart, or use the private API base, Brain ID, and revocable key from a design-partner invite.
The public console does not prefill credentials that look usable but point to no Brain.

The key remains in the password field and request. It is never written to a cookie, URL,
`localStorage`, `sessionStorage`, analytics event, or third-party request. Clear session removes it
from the field.

A green console means the existing read operations worked for this credential and Brain. It does
not prove a write becomes retrievable, citations resolve, data survives a restart, or the backing
engine is a private `SpineStore`. Run the integration doctor for that path.

After identity is verified, the page builds a non-secret setup brief and exact Codex, Claude Code,
Cursor, or generic MCP configuration. Company name changes only the setup label and command; it
does not rewrite ontology or authority. Timezone, locale, intended use, retention intent,
sensitivity, and first source remain in the current tab until copied. They are not presented as
server policy.

`brain disconnect` removes Brain-owned client configuration. Server key revocation remains an
operator action for the design-partner release and takes effect on the next request.

---

# Product

Brain keeps a sourced history of the messages, documents, events, and records behind your product.
For each request, it returns the material that applies, where it came from, and what remains
uncertain.

## How Brain works

1. **Keep the record.** Save source material with the time it was recorded and where it came from.
2. **Track the change.** Derive relationships and versions without erasing the source.
3. **Leave decisions with your app.** Keep application-approved Objects separate from derived
   Memories.
4. **Return the evidence.** Return the records a request may use, with citations, gaps, conflicts,
   and cost.

## Built to be inspected

Cloud Run serves one API. Cloud SQL Postgres with pgvector stores Records, Memories, Objects, and
embeddings. Vertex AI derives and ranks. Sources, time, and application decisions remain visible
in the result.

BigQuery can be an upstream source of structured rows, but a managed BigQuery connector is not
shipped. Model and embedding upgrades are tested before they replace the current runtime.

## Source, finding, decision, result

1. **Source:** Records preserve content, stable customer IDs, metadata, and source time.
2. **Finding:** versioned Memories retain citations; Objects carry facts the application accepts.
3. **Result:** Context returns the evidence a request may use and names what remains unknown.

Your warehouse and application remain authoritative for operational data. The customer SpineStore
is live behind issued keys; the public sandbox is disposable.

---

# Use cases

Examples show how the same API fits different products. Named companies are examples unless stated
otherwise. Their source systems remain authoritative; Brain returns the records that apply, with
citations.

| Workload | Brain `self` | Records | Application authority | Returned records |
|---|---|---|---|---|
| **Brain Mac** | person | messages, events, documents, facts | explicit corrections | sourced personal recall |
| **Woof / BookWoof** | business | bookings, messages, policies, customer events | prices and policy Objects | front-desk action |
| **AiCMO / future GTM** | client organization | research, calls, campaigns, experiments | approved claims and offers | planning and review |
| **Pinnie Health** | person or care program | observations, documents, care-plan events | clinical system remains authoritative | sourced care history |
| **Highbeam-like finance** | business or account | transactions, statements, decisions | ledger/account system remains authoritative | reconciliation and explanation |
| **Conversational / video agent** | user, character, or account | turns, sessions, preferences, unresolved questions | product-owned profile fields | session history |

## What stays the same

Each product:

1. keeps raw input as a Record with provenance and time;
2. lets Brain derive versioned Memories;
3. uses Objects only for structure the application accepts;
4. requests task-specific Context and preserves its citations, gaps, conflicts, abstention,
   processing state, budget, and cost.

Brain complements the warehouse, source system, clinical system, or ledger. It does not replace
their authority.

---

# Brain Mac

Brain Mac is the first read-only customer integration. It uses its own API URL, Brain ID, and key
to read identity, profile, memory status, and an optional cited Context canary.

There is no customer login screen yet. For the private beta, Brain issues each design partner a
scoped, revocable project key. The signed Mac app reads the API URL, Brain ID, and key from
owner-only configuration; the customer does not paste a bearer key into the product UI.

## Ready now

The base slice uses four status reads:

1. `getBrain` to verify the Brain for this key.
2. `getProfile` to read current profile counts.
3. `listMemories` to list current Memories.
4. `getMemory` to open one Memory by ID.

When `BRAIN_MEMORY_API_CONTEXT_CANARY_QUERY` names a customer-approved query, the same reversible
connection also calls fast `createContext` and resolves its first citation with `getRecord`. The
app records only status and counts. Ask, Search, synthesis, and visible answers remain unchanged.

The status UI shows only safe state and counts. It does not render memory content, transport errors,
or the bearer key.

## Still on existing paths

The visible Ask/Search loop and write paths remain outside this first slice. Context conflicts and
Object arbitration are still incomplete.

## Customer integration boundary

The customer SpineStore is live. Brain Mac needs its API base, owner-only project key, and Brain ID.
This is a bounded design-partner path, not self-serve signup or broad GA. The public sandbox remains
available for disposable examples and contains no customer memory.

Brain Mac is the first customer integration. The same resources map to operations, health,
finance, video-agent, and GTM products with different Records and Objects.

## Hand this to your coding agent

Give it the API base, Brain ID, and key. It starts read-only, replaces one Context seam, resolves
one citation, runs the app tests, and leaves Ask, Search, and writes alone until you approve the
next cut. Continue with the [migration guide](integrate.html) or give Codex the generated
[`brain-integrate` Skill](downloads/brain-integrate/codex/SKILL.md).

---

# Brain for agents and IDEs

Start read-only.

Use the canonical OpenAPI or a generated SDK. Never invent credentials, endpoints, resources,
operations, fields, enum values, or response guarantees.

## Install the context

There is no package required for direct HTTP. Give an agent the exact API instructions and
contract without copying them into a prompt:

```bash
export BRAIN_API_BASE="https://your-brain.example/v1"
export BRAIN_API_KEY="..."
export BRAIN_ID="..."

BRAIN_ORIGIN="${BRAIN_API_BASE%/v1}"
curl -fsSL "$BRAIN_ORIGIN/agent.md" -o .brain-agent.md
curl -fsSL "$BRAIN_ORIGIN/openapi.yaml" -o .brain-openapi.yaml
```

Add `.brain-agent.md` and `.brain-openapi.yaml` to the agent's project context. Keep
`BRAIN_API_KEY` only in the runtime environment. The generated clients build from OpenAPI, but the
Brain client is **not published to npm or PyPI** yet; do not invent an install command or package
name.

## Integration order

1. Read `/llms.txt`.
2. Read `/agent.md` and `/openapi.yaml`.
3. Create or select a Brain outside the agent if the credential policy requires it.
4. Call `createContext` with a concrete task and a bounded token budget.
5. Preserve `citations`, `unknowns`, `conflicts`, `processing`, `abstained`, and `cost`.
6. Follow a citation with `getRecord` before turning retrieved text into an assertion.
7. Add writes only after the read path and custody boundary are verified.
8. Call `acceptObject` only when a human or deterministic application rule deliberately takes
   authority for the structured value.

This is the risk ladder: **read → verify → write idempotently → accept authority explicitly**.
The model may propose an Object value. It may not silently promote its own Memory into authority.

## Write rules

Use `Idempotency-Key` for retried writes. Do not persist an API key in source control, chat,
generated files, local storage, query strings, or logs. Do not treat a `202` Record as readable
until its processing watermark is ready.

## Truth and authority

Records are inputs. Memories are derived. Objects are application-accepted. Context is task-scoped.
Do not silently promote one layer into another. No read knob may change what is true.

## Response rules

Preserve citations and their `record_id`. Preserve named unknowns and conflicts even if the calling
model could produce a fluent answer without them. Preserve the three-field cost receipt:
`model_calls_total`, `embedding_calls`, and `generative_calls`.

## Deployment truth

The public sandbox uses `InMemoryStore`; it proves the contract and developer experience, not the
real memory engine. A private or local `SpineStore` deployment carries live memory. Never imply
that fixture data is customer data.

## Current limitations

Context `memories` are selected by exact Record lineage and keep tri-state ownership. Context
`conflicts` remain empty. Accepted Objects do not yet arbitrate Context.
The public sandbox's trace storage is process-local; private SpineStore traces are durable.
Operation scopes, forced row-level isolation, and spine deletion are implemented, but they do not
close the Context arbitration gaps.
Read [Known limitations](/guide/limits.html) before presenting the integration as production
complete.

## IDE snippets

### Codex and AGENTS.md

Tell Codex to read `/agent.md` and `/openapi.yaml`, start with `createContext`, keep the work
read-only, and cite every product claim to a returned Record.

### Claude Code and CLAUDE.md

Tell Claude Code that Brain has five resources and fifteen operations, that `openapi.yaml` is
binding, and that writes require an `Idempotency-Key`.

### Cursor, Copilot, and Windsurf

Attach `/llms.txt`, `/agent.md`, and `/openapi.yaml` as project context. Keep the credential in the
runtime environment as `BRAIN_API_KEY`; never paste it into an instruction file.

---

# Brain Memory API reference

Contract version: `0.1.0`. The binding machine contract is [openapi.yaml](/openapi.yaml).

Set `BRAIN` to the API base ending in `/v1` and keep `KEY` in the runtime environment.

## `createBrain`

`POST /brains` — Create a Brain

[Try this operation](playground.html?operation=createBrain)

### Parameters

- `Idempotency-Key` (header, optional) — `string` — Plumbing, not a knob. Replays return the original response body.


### Request

Schema: `BrainCreate`

```json

{
  "id": "ada"
}

```


Fields:

- `id` (required) — `Slug` — No description.

- `name` (optional) — `string` — No description.

- `self` (optional) — `SelfEntity` — No description.

- `self.type` (required) — `string` — No description.

- `self.name` (required) — `string` — No description.


### Responses

- `201` — `Brain` — Created

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X POST \
  "$BRAIN/brains" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: docs-example-1" \
  -H "Content-Type: application/json" \
  --data '{"id":"ada"}'

```

## `getBrain`

`GET /brains/{brain_id}` — Inspect a Brain

[Try this operation](playground.html?operation=getBrain)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.


### Request

No request body.


### Responses

- `200` — `Brain` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada" \
  -H "Authorization: Bearer $KEY"

```

## `forgetBrain`

`DELETE /brains/{brain_id}` — Delete a Brain and everything derived from it

[Try this operation](playground.html?operation=forgetBrain)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.


### Request

No request body.


### Responses

- `200` — `DeletionReceipt` — Deleted, with a receipt

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X DELETE \
  "$BRAIN/brains/ada" \
  -H "Authorization: Bearer $KEY"

```

## `getProfile`

`GET /brains/{brain_id}/profile` — The always-fresh self card — who this Brain is, every line cited

[Try this operation](playground.html?operation=getProfile)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `limit` (query, optional) — `integer` — No description.


### Request

No request body.


### Responses

- `200` — `Profile` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/profile" \
  -H "Authorization: Bearer $KEY"

```

## `getCensus`

`GET /brains/{brain_id}/census` — What is noisy in THIS Brain's own data

[Try this operation](playground.html?operation=getCensus)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.


### Request

No request body.


### Responses

- `200` — `Census` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/census" \
  -H "Authorization: Bearer $KEY"

```

## `listTraces`

`GET /brains/{brain_id}/traces` — Every Context request this Brain served, newest first

[Try this operation](playground.html?operation=listTraces)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `limit` (query, optional) — `integer` — No description.


### Request

No request body.


### Responses

- `200` — `TracePage` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/traces" \
  -H "Authorization: Bearer $KEY"

```

## `getTrace`

`GET /brains/{brain_id}/traces/{trace_id}` — Inspect one Context request — its lanes, scores, citations, budget and cost

[Try this operation](playground.html?operation=getTrace)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `trace_id` (path, required) — `string` — The `id` of the Context being explained, or the `trc_…` id of a failed request.


### Request

No request body.


### Responses

- `200` — `Trace` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/traces/ctx_01K7QEXAMPLE" \
  -H "Authorization: Bearer $KEY"

```

## `rememberRecord`

`POST /brains/{brain_id}/records` — Remember anything

[Try this operation](playground.html?operation=rememberRecord)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `Idempotency-Key` (header, optional) — `string` — Plumbing, not a knob. Replays return the original response body.


### Request

Schema: `RecordCreate`

```json

{
  "content": "The customer prefers annual billing."
}

```


Fields:

- `content` (required) — `string | MessageContent | DocumentContent | EventContent | FactContent | CorrectionContent` — Plain text, or ONE typed shape tagged by `kind`. Text is the two-call quickstart and stays first-class; a typed shape is how structure that already exists survives the trip, because a speaker is a person, a thread is an episode and a participant is an edge — and none of that is…

- `source` (optional) — `Source` — No description.

- `source.name` (optional) — `string` — No description.

- `source.external_id` (optional) — `string` — No description.

- `source.trust` (optional) — `string` — No description.

- `visibility` (optional) — `Visibility` — No description.

- `occurred_at` (optional) — `string` — When it happened. Defaults to now; wrong for anything historical.


### Responses

- `202` — `Record` — Accepted and durably queued

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X POST \
  "$BRAIN/brains/ada/records" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: docs-example-1" \
  -H "Content-Type: application/json" \
  --data '{"content":"The customer prefers annual billing."}'

```

## `getRecord`

`GET /brains/{brain_id}/records/{record_id}` — Inspect a Record and its processing state

[Try this operation](playground.html?operation=getRecord)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `record_id` (path, required) — `string` — No description.


### Request

No request body.


### Responses

- `200` — `Record` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/records/rec_01K7QEXAMPLE" \
  -H "Authorization: Bearer $KEY"

```

## `forgetRecord`

`DELETE /brains/{brain_id}/records/{record_id}` — Delete a Record and cascade to everything derived from it

[Try this operation](playground.html?operation=forgetRecord)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `record_id` (path, required) — `string` — No description.


### Request

No request body.


### Responses

- `200` — `DeletionReceipt` — Deleted, with a receipt

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X DELETE \
  "$BRAIN/brains/ada/records/rec_01K7QEXAMPLE" \
  -H "Authorization: Bearer $KEY"

```

## `listMemories`

`GET /brains/{brain_id}/memories` — Inspect what the system inferred

[Try this operation](playground.html?operation=listMemories)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `subject` (query, optional) — `string` — No description.

- `owner` (query, optional) — `string` — No description.

- `as_of` (query, optional) — `string` — No description.

- `since_watermark` (query, optional) — `string` — Delta read. Returns only Memories that changed after this boundary, oldest change first — the incremental-sync loop is: read a page, apply it, send its `watermark` back, repeat while `has_more`. CHANGED means TRANSACTION time — when this Brain learned or revised the Memory — not…

- `cursor` (query, optional) — `string` — No description.

- `limit` (query, optional) — `integer` — No description.


### Request

No request body.


### Responses

- `200` — `MemoryPage` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/memories" \
  -H "Authorization: Bearer $KEY"

```

## `getMemory`

`GET /brains/{brain_id}/memories/{memory_id}` — Inspect one Memory, its revisions, and its evidence

[Try this operation](playground.html?operation=getMemory)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `memory_id` (path, required) — `string` — No description.


### Request

No request body.


### Responses

- `200` — `Memory` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/memories/mem_01K7QEXAMPLE" \
  -H "Authorization: Bearer $KEY"

```

## `acceptObject`

`POST /brains/{brain_id}/objects` — Accept application truth

[Try this operation](playground.html?operation=acceptObject)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `Idempotency-Key` (header, optional) — `string` — Plumbing, not a knob. Replays return the original response body.


### Request

Schema: `ObjectAccept`

```json

{
  "type": "preference",
  "key": "billing",
  "value": {
    "plan": "annual"
  }
}

```


Fields:

- `type` (required) — `Slug` — No description.

- `key` (required) — `Slug` — No description.

- `value` (required) — `—` — No description.

- `if_version` (optional) — `integer | null` — Optimistic concurrency. Stale writes fail with the current version.


### Responses

- `201` — `Object` — Accepted

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X POST \
  "$BRAIN/brains/ada/objects" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: docs-example-1" \
  -H "Content-Type: application/json" \
  --data '{"type":"preference","key":"billing","value":{"plan":"annual"}}'

```

## `getObject`

`GET /brains/{brain_id}/objects/{object_type}/{object_key}` — Inspect the accepted Object

[Try this operation](playground.html?operation=getObject)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `object_type` (path, required) — `Slug` — No description.

- `object_key` (path, required) — `Slug` — No description.


### Request

No request body.


### Responses

- `200` — `Object` — OK

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X GET \
  "$BRAIN/brains/ada/objects/preference/billing" \
  -H "Authorization: Bearer $KEY"

```

## `createContext`

`POST /brains/{brain_id}/context` — Get task-ready context — cited, budgeted, honest

[Try this operation](playground.html?operation=createContext)

### Parameters

- `brain_id` (path, required) — `Slug` — No description.

- `Brain-Watermark` (header, optional) — `string` — Plumbing, not a knob. Guarantees the Context is at or after this boundary.


### Request

Schema: `ContextRequest`

```json

{
  "query": "What should the agent know right now?"
}

```


Fields:

- `query` (required) — `string` — No description.

- `purpose` (optional) — `string` — One word sets policy, sensitivity, and eligibility for a class of task.

- `as_of` (optional) — `string` — Applies world-time eligibility to returned Memory components. The evidence text is a selected chunk projection and records this boundary but does not yet reconstruct a historical chunk index.

- `budget` (optional) — `object` — No description.

- `budget.tokens` (optional) — `integer` — No description.

- `budget.ms` (optional) — `integer` — No description.

- `include` (optional) — `array` — Select response components. `memories` are cited, temporally eligible Memory atoms whose complete Record lineage is contained in the selected evidence window.

- `schema` (optional) — `object` — Request structured context. Invalid means unavailable, never malformed.

- `filter` (optional) — `object` — Narrow what is eligible. `since`, `until` and `sources` are enforced. `subjects`, `owner` and `sensitivity_lte` are recorded on the receipt and NOT enforced on Context — use `listMemories` for subject and owner filtering, where those fields exist.

- `filter.subjects` (optional) — `array` — No description.

- `filter.owner` (optional) — `string` — No description.

- `filter.sources` (optional) — `array` — No description.

- `filter.since` (optional) — `string` — No description.

- `filter.until` (optional) — `string` — No description.

- `filter.sensitivity_lte` (optional) — `string | null` — No description.

- `mode` (optional) — `string` — `fast` never composes an answer — it assembles cited evidence and asserts nothing, which is what makes it unable to fabricate. A ready `fast` Context on the hosted spine spends ONE model call — a query embedding — and `cost.generative_calls` is therefore `0`, with `embedding_cal…


### Responses

- `200` — `Context` — OK — a single Context, or an SSE stream when `Accept: text/event-stream` met `mode: thorough`

- `202` — `Context` — Not yet assemblable at the requested watermark, or `thorough` exceeded the synchronous budget

- `default` — `Error` — No description.


### Errors

All errors use the shared `Error` schema. See [Errors](errors.md).


### cURL

```bash

curl -sS -X POST \
  "$BRAIN/brains/ada/context" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  --data '{"query":"What should the agent know right now?"}'

```

---

# Brain Memory API — every error code

**Date:** 2026-07-27 · Source of truth for `https://docs.brain.new/errors/{code}`.

Every failure the API can produce returns the same body, and that body carries a `docs_url` built
from its `code`. **This page is what that URL resolves to** — one section per code, anchored at
`#<code>`. A code that is raised and not documented here fails
`.venv/bin/python scripts/check_error_docs.py`, so the link cannot rot back into a 404.

An error page that only restates the failure is worthless. Each section below names **the fix**.

---

## One shape, always

```json
{
  "type": "conflict",
  "code": "stale_version",
  "message": "current version is 1",
  "request_id": "req_01KYJDX64GD6N69K4A5QN649FC",
  "docs_url": "https://docs.brain.new/errors/stale_version",
  "current_version": 1
}
```

- **`type`** — the class of failure. Branch on this. Eight are declared in the contract
  (`invalid_request · authentication · permission · not_found · conflict · rate_limit · policy ·
  internal`), and all eight are reachable.
- **`code`** — the exact reason. Log this. It is stable; a code is never repurposed.
- **`message`** — for a human, and it names the offending field or id. Never parse it.
- **`request_id`** — quote it in a support request. It identifies this one call.
- Some codes add **one extra field** (`stale_version` adds `current_version`). Extra fields are
  additive; ignore ones you do not know.

There is no second error shape. FastAPI's default `{"detail": ...}` is mapped away in
`memory_api/app.py`, because the most common failure an integration hits must not be the one
response a generated SDK cannot parse.

---

## Should I retry?

| `type` | Retry? | What to do |
|---|---|---|
| `invalid_request` | **No** | The request is wrong. Retrying sends the same wrong request. |
| `authentication` | **No** | Fix the credential. |
| `permission` | **No** | Use a key that carries the scope. |
| `not_found` | **No** | Create it, or stop using a stale id. Do not poll a 404 into existence — except `memory_not_found` while a Record is still `processing`. |
| `conflict` | **Once**, after re-reading | Re-read, re-apply on top of what you read, retry. Never by stripping `if_version`. |
| `rate_limit` | **Yes** | Exponential backoff. |
| `internal` | **Once** | Reads are safe. `remember` is safe **if you resend the same `Idempotency-Key`** — it is the write path's whole retry story. |

---

## Every code

| Code | HTTP | `type` | Means | Do this |
|---|---|---|---|---|
| [`invalid_api_key`](#invalid_api_key) | 401 | `authentication` | The credential is missing or malformed | Send `Authorization: Bearer sk_<test\|live>_<project>_<secret>` |
| [`key_revoked`](#key_revoked) | 401 | `authentication` | This key was revoked in the key registry | Mint a new key; a revoked one never returns |
| [`key_registry_unreadable`](#key_registry_unreadable) | 500 | `internal` | The key registry exists but could not be read | Fix or remove `BRAIN_KEYS_FILE`; every key is refused until then |
| [`missing_scope`](#missing_scope) | 403 | `permission` | The key is valid but narrowed | Use a key carrying that scope |
| [`mcp_brain_unbound`](#mcp_brain_unbound) | 403 | `permission` | The key is not bound to one Brain for hosted MCP | Mint a replacement with `--brain` |
| [`mcp_identity_unconfigured`](#mcp_identity_unconfigured) | 401 | `authentication` | Local stdio has no explicit project or Brain | Set both local MCP environment values |
| [`invalid_request_body`](#invalid_request_body) | 400 | `invalid_request` | A body or typed query field failed validation | Read the field name in `message`, fix that field |
| [`unknown_query_parameter`](#unknown_query_parameter) | 400 | `invalid_request` | A query parameter the contract does not declare | Remove it — or move it into the request body |
| [`unknown_kind`](#unknown_kind) | 400 | `invalid_request` | `content.kind` is not one of the four | Use `message`, `document`, `event`, or `fact` |
| [`invalid_watermark`](#invalid_watermark) | 400 | `invalid_request` | `since_watermark` could not be read as a boundary | Send back a `watermark` you received, verbatim |
| [`conflicting_parameters`](#conflicting_parameters) | 400 | `invalid_request` | Two parameters that cannot both apply | Drop one — `message` names them |
| [`streaming_not_applicable`](#streaming_not_applicable) | 406 | `invalid_request` | `Accept: text/event-stream` on a call that does not stream | Use `mode: thorough`, or drop the `Accept` header |
| [`brain_not_found`](#brain_not_found) | 404 | `not_found` | No Brain with that id in this project | Create it, or check test-vs-live |
| [`record_not_found`](#record_not_found) | 404 | `not_found` | No Record with that id in this Brain | Use the id `remember` returned, verbatim |
| [`record_not_erasable`](#record_not_erasable) | 403 | `policy` | The Record is reconciled from an external source and would return | Delete it at the source, or forget the whole Brain |
| [`memory_not_found`](#memory_not_found) | 404 | `not_found` | No Memory with that id in this Brain | Read ids from `memory_ids` / Context citations, never construct them |
| [`memory_pending_rederivation`](#memory_pending_rederivation) | 404 | `not_found` | The Memory lost some supporting Records to a deletion | Wait for re-derivation, then re-read `listMemories` |
| [`object_not_found`](#object_not_found) | 404 | `not_found` | Nothing accepted at that `type`/`key` | Accept it first; keys are exact |
| [`trace_not_found`](#trace_not_found) | 404 | `not_found` | No trace with that id in the buffer | Traces are recent-only; re-run the request |
| [`corrected_memory_not_found`](#corrected_memory_not_found) | 404 | `not_found` | `corrects` names a Memory this Brain does not hold | Send a `mem_…` id you read from this Brain |
| [`brain_exists`](#brain_exists) | 409 | `conflict` | That Brain id is taken | `GET` it instead of creating it |
| [`idempotency_conflict`](#idempotency_conflict) | 409 | `conflict` | The same `Idempotency-Key` was already used for different content | Retry with the SAME body, or use a new key |
| [`stale_version`](#stale_version) | 409 | `conflict` | `if_version` did not match | Re-read, re-apply, retry with `current_version` |
| [`correction_conflict`](#correction_conflict) | 409 | `conflict` | The corrected Memory's slot has a newer active value | Read the current Memory, correct that id |
| [`not_implemented`](#not_implemented) | 501 | `internal` | The route is real; this engine does not serve it yet | Nothing to fix caller-side. Do not retry |
| [`local_only_record`](#local_only_record) | 403 | `policy` | A `local_only` Record met a hosted deployment — on write or on read | Use Brain Local on the device that holds it, or `shareable` |
| [`too_many_requests`](#too_many_requests) | 429 | `rate_limit` | Too many requests from this key in a minute | Back off for `retry_after_seconds` |
| [`unsupported_media_type`](#unsupported_media_type) | 415 | `invalid_request` | The body is not JSON | Send `Content-Type: application/json` |
| [`memory_without_lineage`](#memory_without_lineage) | 500 | `internal` | A stored Memory cannot name its evidence | Report the `request_id`. Retrying will not help |
| [`internal_error`](#internal_error) | 500 | `internal` | An unhandled bug | Retry once, then report the `request_id` |
| [`http_400`](#http_400) | 400 | `invalid_request` | The framework rejected the request before routing | Send well-formed JSON |
| [`http_401`](#http_401) | 401 | `authentication` | Auth failed below the route layer | Same fix as `invalid_api_key` |
| [`http_403`](#http_403) | 403 | `permission` | Refused below the route layer | Same fix as `missing_scope` |
| [`http_404`](#http_404) | 404 | `not_found` | The **path** does not exist | Fix the URL — not the id |
| [`http_405`](#http_405) | 405 | `invalid_request` | Wrong method on a real path | Read the `Allow` response header |
| [`http_406`](#http_406) | 406 | `invalid_request` | Your `Accept` header excludes JSON | Send `Accept: application/json` |
| [`http_409`](#http_409) | 409 | `conflict` | Conflict below the route layer | Re-read, then retry once |
| [`http_415`](#http_415) | 415 | `invalid_request` | Wrong `Content-Type` on a body | Send `Content-Type: application/json` |
| [`http_429`](#http_429) | 429 | `rate_limit` | Too many requests | Back off and retry |

The `http_*` family is raised by the framework **before any route runs** — an unknown path, a method
that does not exist, a body it could not parse. They carry a generic `message` because at that point
nothing has been interpreted yet. Everything else comes from a route or the store, and says
precisely what went wrong.

---

## `key_revoked`

`401` · `authentication` · raised by every route

**Means** — this key is well-formed, but its hash is listed in the revocation registry.

**Usually** — the key leaked and was revoked, or it belonged to a rotation that has completed.

**Fix** — mint a new secret and use it (`scripts/brain_keys.py mint --project <project> --env test`,
which prints the key once). **Revocation is per-key, not per-format**: before the registry existed,
a leaked key could only be handled by changing the key format for everyone. Now that one key's
entry is stamped, that one key stops working — on the next request, with no restart — and nobody
else is affected.

**Revocation has no inverse.** A key is revoked because it is believed to be in someone else's
hands, so restoring it would restore their access too. The way back to a working integration is
always a new key.

**The registry never holds a key** — only the SHA-256 of one, plus the `sk_<env>_<project>` prefix
and the last four characters. It is therefore safe to commit, log, and pass around; it cannot be
used to authenticate as anybody, and a key that is lost can be replaced but never recovered.

## `key_registry_unreadable`

`500` · `internal` · raised by every route

**Means** — a key registry was configured (`BRAIN_KEYS_FILE`) but could not be read or parsed, so
**every key is refused**. That file holds both the issued keys (hash, prefix, last four, project,
status) and any hashes revoked by hand; either section being unreadable produces this.

**Usually** — a bad path, a permissions problem, or malformed JSON after a hand edit.

**Fix** — repair the file, or unset `BRAIN_KEYS_FILE` to run with no registry.
`scripts/brain_keys.py list` reads the same file and reports the same problem out of band, and the
minting tool refuses to *write* a registry it could not read — a fresh ledger written over a
corrupt one would silently un-revoke every key it held.

**Why this fails CLOSED when an ABSENT registry fails open** — the two are different states, and
conflating them is the bug. No registry configured means *"nobody has revoked anything"*, which is
a legitimate way to run. A registry that is present but unreadable means *somebody wrote down
which keys must stop working and we cannot tell which* — and continuing to accept keys in that
state is exactly the failure the registry was added to prevent.

## `invalid_api_key`

`401` · `authentication` · raised on every request, before routing

**Means** — the `Authorization` header was absent, was not a `Bearer` credential, or the key did not
parse as `sk_<test|live>_<project>_<secret>` (four `_`-separated segments, none empty).

**Usually** — the header was sent as the bare key without `Bearer `; an unset environment variable
went out as the literal `$BRAIN_API_KEY`; or the key was copied with a trailing newline.

**Fix** — send `Authorization: Bearer sk_test_acme_<secret>`. Note what is *not* here: there is no
project, tenant, or user field to send. **The credential is the only thing that selects a Brain**, so
if you were looking for where to put the project id, it is already inside the key. `sk_test_…` and
`sk_live_…` address different namespaces — a Brain created with a test key is invisible to a live
key, and that is deliberate, not a bug.

---

## `missing_scope`

`403` · `permission` · raised when a route requires a scope the credential lacks

**Means** — the key is genuine and resolved, but does not carry the scope this route needs. The
vocabulary is `records:write · records:read · memories:read · objects:write · objects:read ·
context:read`, and `message` names the missing one.

**Usually** — a deliberately narrowed key (say, read-only) used on a write path. Keys are currently
issued with all six scopes, so seeing this means someone narrowed it on purpose.

**Fix** — use a key that carries the scope. A scope cannot be requested per call and cannot be
escalated by any header, body field, or parameter — that is the point of it. Retrying is pointless.

---

## `mcp_brain_unbound`

`403` · `permission` · hosted MCP only

**Means** — the key authenticates, but its issuance record does not bind it to one Brain. Hosted
tools accept no caller-controlled Brain ID, so falling back would risk the wrong customer.

**Fix** — mint a replacement with `scripts/brain_keys.py mint --brain <brain-id>` and the minimum
read scopes. An older unbound key may still use HTTP paths, where the Brain is explicit in the URL.

---

## `mcp_identity_unconfigured`

`401` · `authentication` · local stdio only

**Means** — the local bridge was started without both `BRAIN_MCP_PROJECT` and `BRAIN_MCP_BRAIN`.

**Fix** — set both values for local development, or connect to hosted `/mcp` with a Brain-bound
bearer credential. There is no implicit customer identity.

---

## `invalid_request_body`

`400` · `invalid_request` · raised by request validation on every route with a body or typed query

**Means** — Pydantic rejected the request. `message` is `"<field>: <reason>"`, and `<field>` is the
exact path into your JSON.

**Usually** — one of four things:

- a required field is absent — `id: Field required`
- `"if_version": true` — `if_version: Input should be a valid integer`. **`bool` subclasses `int` in
  Python**, so `true` once passed as version 1; strict typing now refuses it
- a timestamp that is not RFC 3339 — `as_of: Input should be a valid datetime`. `occurred_at` and
  `as_of` are date-times, and a string that merely looks like one is rejected rather than ignored
- no body at all, or a body that is not a JSON object — `body: Input should be a valid dictionary`

**Fix** — read the field name in `message` and fix that field only. If the field name surprises you,
compare the request against `openapi.yaml`, which is the contract — not the framework's generated
schema, and not any SDK's docstring.

---

## `unknown_query_parameter`

`400` · `invalid_request` · raised by `listMemories`

**Means** — you sent a query parameter the contract does not declare, and `message` names it:
`unknown query parameter(s): limitt`.

**Usually** — a typo (`limitt=1000`), or a read knob put in the wrong place. `listMemories` declares
exactly `subject · owner · as_of · cursor · limit`.

**Fix** — remove or correct the parameter. If it was a knob — `purpose · budget · include · schema ·
filter · mode` — it belongs in the **body of `POST /v1/brains/{id}/context`**, not in a query string.

We refuse rather than ignore on purpose: a silently dropped `limitt=1000` reads to you as *accepted*,
and you would go on believing a bound was applied that never was.

---

## `unknown_kind`

`400` · `invalid_request` · raised by `rememberRecord` on the spine engine

**Means** — `content.kind` is not one of `message`, `document`, `event`, `fact`, `correction`.

**Usually** — a kind invented for one application's domain.

**Fix** — pick one of the five, or send a **plain string**, which is stored as a `document` (you said
nothing about structure, so none is invented). Do not add a kind for your vertical: the five are the
entire vocabulary, and one contract serving every application with no per-customer branch is the
property that makes that possible.

---

## `invalid_watermark`

`400` · `invalid_request` · raised by `listMemories` when `since_watermark` cannot be read

**Means** — the engine serving this Brain could not read the string you sent as `since_watermark`
as a boundary. The contract can only promise the `wm_` prefix; what follows is the issuing engine's
business, so "well-formed" is not the same as "readable here".

**Usually** — a watermark that was constructed rather than received (`wm_0`, a timestamp, an
ordinal); one truncated or re-formatted on the way through a client; or a watermark from a
**different Brain**. Watermarks are opaque and are only comparable within the Brain that issued
them — a Brain in your test project and one in live do not share a clock.

**Fix** — send back, byte for byte, a `watermark` you received: from a `MemoryPage`, from the
`Brain-Watermark` header on a write, or from a Context. Store it as an opaque string; do not parse
it, sort it against another Brain's, or round-trip it through a number.

Why this is an error and not an ignored parameter: a boundary we could not read, quietly treated as
"from the beginning", returns your entire corpus while looking exactly like a delta — and a sync
client would apply it as one. Same reasoning as `unknown_query_parameter`.

---

## `conflicting_parameters`

`400` · `invalid_request` · raised by `listMemories`

**Means** — you sent two parameters that cannot both apply, and `message` names them. Today that is
`since_watermark` together with `cursor`.

**Usually** — a paging helper that always attaches its `cursor` being pointed at a delta read.

**Fix** — send one. In a delta read the **watermark IS the cursor**: page with `since_watermark`
alone, following each page's returned `watermark` while `has_more` is true (a delta page returns
`next_cursor: null` for exactly this reason). Use `cursor` for a full read, which is ordered by id
instead of by when things changed. Honouring both would leave the page order ambiguous, and a
resumable page needs one order.

---

## `streaming_not_applicable`

`406` · `invalid_request` · raised by `createContext`

**Means** — you sent `Accept: text/event-stream` on a request that does not stream. Only
`mode: thorough` streams.

**Usually** — a client that sets the SSE `Accept` header globally, or a default `mode` (which is
`fast`) left in place while streaming was switched on.

**Fix** — either send `"mode": "thorough"` in the body, or drop the `Accept` header and read the
single JSON response.

`fast` does not stream on purpose. It composes nothing, so there is no generative call to wait on
and a stream would deliver one frame after the same wait — all of the event-loop
complexity for none of the latency it exists to hide. We refuse rather than quietly returning JSON
to a client that asked for a stream, because content negotiation you cannot detect is the same
defect as a silently ignored parameter.

---

## `brain_not_found`

`404` · `not_found` · raised by every route that names a Brain

**Means** — no Brain with that id exists **in the project this key addresses**.

**Usually** — the Brain was never created; the id is misspelled; or it was created with a `sk_test_`
key and read with a `sk_live_` one. Test and live are different projects internally, not a flag on
one, so they cannot see each other's Brains.

**Fix** — `POST /v1/brains` with the id you want (ids are yours to choose), then retry. If you are
sure it exists, check the environment segment of the key you used.

---

## `record_not_found`

`404` · `not_found` · raised by `getRecord` and `forgetRecord`

**Means** — the Brain exists; this Record id does not belong to it.

**Usually** — an id from a different Brain or environment; a mangled `rec_` prefix; or the Record was
forgotten, which is permanent.

**Fix** — use the `id` returned by `remember` verbatim, and store it against the Brain it came from.
If you are retrying a `remember` that timed out, do **not** guess the id — resend the write with the
same `Idempotency-Key` and you will get the original Record back, never a second one.

---

## `memory_not_found`

`404` · `not_found` · raised by `getMemory`

**Means** — the Brain exists; this Memory id does not belong to it.

**Usually** — the id was constructed rather than read, or the Memory does not exist **yet**. Memories
are *derived* from Records, never written directly, so a Record that reports `status: "processing"`
has no Memories at all yet. It is also possible the Record it cited was forgotten: a derived row that
loses all of its supporting Records is deleted with them.

**Fix** — take Memory ids from `record.memory_ids` or from a Context's `citations`. If the Record is
still `processing`, poll `GET /records/{id}` until it is `ready` — this is the one 404 where waiting
is the correct response, and the `status` field is how the contract tells you so instead of lying.

---

## `record_not_erasable`

`403` · `policy` · raised by `forgetRecord`

**Means** — this Record came from an externally reconciled source such as Messages, Notes,
Contacts, or Calendar. Deleting only Brain's copy would not be durable: the next source sweep would
read the original again and restore it after a receipt said it was gone.

**Fix** — delete the item at its source and let the next sweep reconcile that deletion. To remove
the entire customer namespace immediately, call `forgetBrain`; its terminal cascade removes every
source and derivative under that Brain. API-origin Records are self-attesting—the API is their
origin—so `forgetRecord` can erase those directly.

---

## `memory_pending_rederivation`

`404` · `not_found` · raised by `getMemory`

**Means** — this Memory exists and **cannot be served**. It was derived from several Records, you
deleted one of them, and it is excluded from every read path until it is re-derived from the ones
that survived.

**Usually** — you called `DELETE /records/{id}` and are now following a `memory_ids` pointer or a
citation you captured before the deletion. The receipt from that delete already counted this:
`rederiving.memories` is exactly the number of Memories put into this state.

**Why not just serve it** — its statement was written from evidence that included the Record you
deleted, so serving it would serve the deleted content back to you inside a summary, under a
citation list that no longer says where it came from. Half the evidence is not most of the answer;
it is an assertion nobody can check. The other half of the same rule is that a Memory which lost
**all** of its supporting Records is deleted outright, and returns
[`memory_not_found`](#memory_not_found) — that one is not a policy choice, because a Memory with no
citations is not a shape the contract can represent.

**Fix** — nothing on your side. Re-read `GET /brains/{id}/memories` after re-derivation; what comes
back is a Memory derived only from Records that still exist, and it may say something different
from the one you were holding, which is the point. If you deleted the Record by mistake, there is
no undo — `remember` it again and the Memory is derived fresh.

**Not a synonym for `memory_not_found`.** They are separated so this state cannot be a silent
filter: a Memory the system still holds but refuses to serve must say so, or the only way to
discover it is to notice that answers quietly got worse.

---

## `trace_not_found`

`404` · `not_found` · raised by `getTrace`

**Means** — no recall trace with that id is still held for this Brain.

**Usually** — the trace has aged out. Traces live in a bounded in-process ring buffer, not in the
database, so they are **recent-only by design**: a busy Brain evicts old ones, and a restart clears
all of them. A trace id from yesterday is expected to be gone.

**Fix** — re-run the Context request and read the trace it returns, or list `GET
/v1/brains/{id}/traces` to see what is currently held. If you need a trace to outlive the buffer,
capture it at the time of the request; the API does not promise durability for it.

**Why it is not durable** — a trace records the shape of a retrieval, not its content, and keeping
every one forever would be a second store of things the contract already forbids duplicating. The
buffer exists to answer "what did that request actually do", which is a question you ask within
minutes, not months.

## `corrected_memory_not_found`

`404` · `not_found` · raised by `rememberRecord` on a `correction` Record

**Means** — the `corrects` field names a Memory this Brain does not hold, so there is nothing to
supersede. Checked before anything is written: a failed correction leaves no Record behind.

**Usually** — a constructed or mangled id; a `mem_…` id from a different Brain or environment; or a
Memory whose supporting Records were forgotten, which deletes it — there is nothing left to correct
because the claim is already gone.

**Fix** — send a Memory id you *read from this Brain*: from `record.memory_ids`, a Context's
`memories`, or `GET /v1/brains/{id}/memories`. Do not retry with the same id — unlike
[`memory_not_found`](#memory_not_found) while a Record is `processing`, a correction targets a
Memory that must already exist, so polling cannot make this succeed.

---

## `object_not_found`

`404` · `not_found` · raised by `getObject`

**Means** — nothing has been accepted at that `type`/`key` in this Brain.

**Usually** — it was never accepted, or the `type`/`key` differ by case or spacing. Both are exact
strings; `refund_window` and `Refund_Window` are two different Objects.

**Fix** — `POST /v1/brains/{id}/objects` to accept the value first. An Object is something **your
application** took authority for; the system never creates one on your behalf, so a missing Object is
always a write you have not made yet.

**Engine note** — the spine-backed engine implements Objects against a real, versioned table. Until
the operator has applied `brain/ingest/schema.sql` (one idempotent, additive DDL the ingest pipeline
self-applies), that table does not exist and *every* Object call returns
[`not_implemented`](#not_implemented) naming that fix — **not** this code. That distinction is
deliberate: `object_not_found` would have sent you looking for an acceptance you never made.

---

## `brain_exists`

`409` · `conflict` · raised by `createBrain`

**Means** — a Brain with that id already exists in this project.

**Usually** — a retried create, or two processes bootstrapping the same Brain concurrently.

**Fix** — `GET /v1/brains/{id}` and use it. For "create if absent", `GET` first and treat a
`brain_not_found` as the signal to create; when two callers race, the loser gets this code and should
`GET`, not retry the create. `createBrain` is deliberately **not** idempotent — a Brain is a memory
boundary, and silently returning someone else's Brain because the id matched is exactly the failure
that must never happen.

---

## `stale_version`

`409` · `conflict` · raised by `acceptObject` · **carries `current_version`**

**Means** — you sent `if_version: N` and the Object is not at version N. The body tells you what it
actually is.

**Usually** — someone else accepted a new version between your read and your write.

**Fix** — re-read the Object, re-apply your change on top of **that** value, and retry with
`if_version` set to the `current_version` you were just handed. Retry once; if it conflicts again,
something else is writing continuously and you should serialize the writers.

**Do not** drop `if_version` to force the write through. That converts a reported conflict into a
silent lost update, which is the exact failure `if_version` exists to make visible. Objects are
immutable and versioned: a successful accept appends a version, it never mutates the old one.

---

## `correction_conflict`

`409` · `conflict` · raised by `rememberRecord` on a `correction` Record, spine engine

**Means** — the Memory named by `corrects` exists but was **already superseded**, and its slot now
has a newer active value. Superseding it again would put two live claims on one slot, which the
database refuses (one active row per slot is enforced by a unique index, not by trust).

**Usually** — a stale id: you read the Memory, something revised it (a newer Record, an earlier
correction), and your correction arrived after. The same race as `stale_version`, on Memory.

**Fix** — re-read the current value (`GET /v1/brains/{id}/memories` filtered to the subject, or the
Context you are working from) and send the correction against the **current** Memory's id. The
failed attempt rolled back whole — no Record, no supersede, nothing to clean up.

---

## `idempotency_conflict`

`409` · `conflict` · raised by `rememberRecord`, both engines

**Means** — this `Idempotency-Key` was already used, for a Record whose content was **different**.
A key identifies a *request*, not a slot: reusing it is how you say "this is the same write, I am
retrying." Two different bodies under one key are two writes wearing one name, and there is no
answer to that which is not a lie. Overwriting would mutate a Record the contract calls immutable;
returning the earlier one would hand you a Record you never wrote and call it yours.

**Usually** — a client that derives its key from something too coarse (a user id, a day, a job
name) rather than from the write itself, so two genuinely different writes collide. Occasionally a
retry that edited the body before resending.

**Fix** — retry with the **byte-identical** body to get the original Record back (that is a replay
and returns `202` with the same `rec_…`), or send a **new key** for the new content. If what you
actually want is to change what this Brain believes, that is a `correction` Record: it supersedes
the old claim and keeps the history, which is what `Idempotency-Key` can never do.

**Note** — this used to be silent. The spine ran `ON CONFLICT … DO UPDATE`, so a replay carrying
different content rewrote the stored Record and answered `202`. History changed and nobody was
told (CLOSURE_PLAN #12).

---

## `local_only_record`

`403` · `policy` · raised by `rememberRecord` (on write) and `getRecord` (on read), hosted deployments

**Means** — `visibility: local_only` says this Record never leaves the device. A hosted deployment
cannot keep that promise, so it will not participate in one. **Two moments, one rule:**

  · **on write** — the Record is refused rather than stored. A promise never made beats a promise
    made and broken, and storing it in a datacentre while answering `202` would be the second.
  · **on read** — a Record that reached the host another way (the sync pipeline is custody
    *transport*, not serving) is refused rather than returned.

`403`, not `404`: the Record exists and you may well own it. What you cannot have is it leaving the
device. Saying "not found" would be a lie that also teaches you to retry.

**Also true of Context, and this is the part that was broken.** `POST /context` filters `local_only`
evidence out of the served window on a hosted deployment — until 2026-07-28 it did not, so the same
content the direct read refused came back as body text inside a `200`. There is no error for that
case by design: the evidence is simply absent, exactly as if the Brain did not hold it, because a
`403` naming what was withheld would itself leak that something is there.

**Fix** — read it from Brain Local on the device that holds it, or write it as `redacted_summary`
or `shareable` if it may live on a host.

**Scope, stated honestly** — this closes the *response* leak. A hosted deployment still ranks rows
it will not serve, because the lane predicates live in `brain/retrieve/` and pushing visibility into
that SQL is a retrieval change that has to clear the eval gate.

## `too_many_requests`

`429` · `rate_limit` · raised by every route

**Means** — this API key made more requests in the last minute than the limit allows.

**Usually** — a retry loop with no backoff, or a batch job that should be pacing itself.

**Fix** — wait `retry_after_seconds`, which is on the error body, then retry. The window is fixed,
not sliding, so the counter resets rather than decays.

**Honest bound** — the limiter is **per process**. Two instances mean two counters, so the
effective limit is per-instance. Real rate limiting belongs at the edge and is not shipped yet; this
exists so the declared response is reachable and a single instance has a floor.

## `unsupported_media_type`

`415` · `invalid_request` · raised by every route that takes a body

**Means** — the request body is not JSON.

**Usually** — a missing or wrong `Content-Type`. An HTTP client that defaults to
`application/x-www-form-urlencoded` produces this.

**Fix** — send `Content-Type: application/json` (or any `+json` type).

**Why it is checked first** — it used to surface as `invalid_request_body`, which sends you hunting
through the payload for a field problem that is really a header problem.

## `not_implemented`

`501` · `internal` · raised by the spine engine on paths that are not built yet

**Means** — the route is real and the contract is honest about it; **this** engine does not serve it
yet. `message` names what unblocks it: creating and forgetting Brains and the deletion cascade (T9)
— or a database the operator has not applied `brain/ingest/schema.sql` to yet (one idempotent DDL),
which is two separate gates with the same fix: on `rememberRecord`, an `events` type domain not yet
widened for `api.*` Records; on `acceptObject`/`getObject`, a missing `objects` table. In both of
those the path itself is BUILT, nothing was half-written (the failed statement rolls back whole),
and the same call succeeds the moment the DDL has run.

**Usually** — you are calling a write path against the spine-backed deployment. The in-memory
conformance engine implements all of them, which is why the same call can succeed there.

**Fix** — none caller-side, and **do not retry** — it will fail identically until the tier ships or
the operator applies the DDL. This is deliberate: a half-built write path that appeared to work
would be worse than one that says it is not there, because you would build on a guarantee that does
not exist.

**Known wart, stated rather than hidden:** the `type` is `invalid_request`, which reads as though the
caller erred; it is a server-side gap and would be better typed. The status is contract-legal (every
operation declares `default: Error`), and changing the type is a contract change that has to go
through the conformance gate.

---

## `memory_without_lineage`

`500` · `internal` · raised while serializing a Memory

**Means** — a stored Memory has no supporting Records, so it cannot cite its evidence. We refuse to
serialize it at all rather than return an assertion you have no way to verify.

**Usually** — a derivation defect upstream: a row written without its `event_ids`, or a partial
cascade that removed Records and left the derived row behind.

**Fix** — nothing caller-side, and retrying is pointless: it is deterministic, not transient. Report
the `request_id`. The server-side fix is to re-derive or quarantine the offending row — never to relax
the check, because **cited-or-abstain is the guarantee**, and a citation-free Memory is the shape of
every confidently-wrong answer this API exists to not produce.

---

## `internal_error`

`500` · `internal` · the catch-all for an unhandled exception

**Means** — a bug. The handler exists so that even a bug returns the one typed error shape: no stack
trace, no framework HTML, nothing untyped ever reaches you.

**Usually** — genuinely unexpected. A dependency being unavailable is the most common cause.

**Fix** — retry **once**. Reads are safe. `remember` is safe to retry if you resend the same
`Idempotency-Key`: the key becomes the record's natural key under a `UNIQUE` constraint, so a replay
with identical content returns the **original** Record and cannot mint a second. If it recurs, report
the `request_id` — it identifies the exact call in the server logs.

---

## `http_400`

`400` · `invalid_request` · framework-level, before routing

**Means** — the request was rejected before any route saw it — malformed HTTP or a body the framework
could not parse at all.

**Fix** — send well-formed JSON. Note that most malformed bodies surface as
[`invalid_request_body`](#invalid_request_body) instead, which names the field; if you got this
generic one, the problem is with the request as a whole, not one field in it.

---

## `http_401`

`401` · `authentication` · framework-level

**Means** — authentication failed below the route layer.

**Fix** — same as [`invalid_api_key`](#invalid_api_key), which is what an authentication failure
actually returns today. This code is mapped so that if any future middleware raises a bare 401, it
still arrives as the one typed shape rather than `{"detail": ...}`.

---

## `http_403`

`403` · `permission` · framework-level

**Means** — the request was refused below the route layer.

**Fix** — same as [`missing_scope`](#missing_scope), which is what a permission failure returns
today. Mapped for the same reason as `http_401`: no failure path may escape the Error shape.

---

## `http_404`

`404` · `not_found` · framework-level

**Means** — **the path does not exist.** This is not "your id was not found" — no route matched the
URL at all, and `message` is the generic `Not Found`.

**Usually** — a missing `/v1` prefix, a mistyped segment (`/v1/brain/…` for `/v1/brains/…`), or a
resource path that is not in the contract.

**Fix** — compare the URL against `openapi.yaml`. Every path is
`/v1/brains`, `/v1/brains/{id}`, or `/v1/brains/{id}/{records|memories|objects|context}`. If your id
is the problem instead, you would have received `brain_not_found` or `record_not_found` — a specific
code means routing worked.

---

## `http_405`

`405` · `invalid_request` · framework-level · **carries the `Allow` response header**

**Means** — the path is real, the method is not: `PUT` on a Record, `POST` on a Memory.

**Fix** — read the `Allow` header on the response; it lists exactly what that path accepts. That
header is passed through deliberately, because a refusal a client cannot learn from is just a number.

Worth knowing which method to expect: nothing in this API is updated in place. You `POST` to remember
a Record and to accept an Object (which appends a version), you `GET` to read, and you `DELETE` to
forget. There is no `PUT` or `PATCH` anywhere in the contract.

---

## `http_406`

`406` · `invalid_request` · framework-level

**Means** — your `Accept` header excludes `application/json`, and JSON is all this API speaks.

**Fix** — send `Accept: application/json`, or omit the header entirely. (Today the server does not
enforce `Accept`, so this is mapped ahead of a stricter negotiation rather than commonly seen.)

---

## `http_409`

`409` · `conflict` · framework-level

**Means** — a conflict raised below the route layer.

**Fix** — re-read the resource and retry once, exactly as for
[`stale_version`](#stale_version). Route-level conflicts arrive as `brain_exists` or `stale_version`,
which name the resource and, where one exists, hand you the current version.

---

## `http_415`

`415` · `invalid_request` · framework-level

**Means** — a body was sent with a `Content-Type` the API does not accept.

**Usually** — `curl -d '{...}'` with no `-H 'Content-Type: application/json'`, which defaults to
`application/x-www-form-urlencoded`.

**Fix** — send `Content-Type: application/json`. (Today a wrong content type is more likely to
surface as [`invalid_request_body`](#invalid_request_body) — the parser fails before negotiation
does — so treat both as the same fix.)

---

## `http_429`

`429` · `rate_limit` · framework-level

**Means** — too many requests.

**Fix** — back off exponentially and retry; this is the one code where retrying is the correct
response rather than a hopeful one. Make writes safe to repeat by sending an `Idempotency-Key` on
every `remember`, so a retried write after a rate limit returns the original Record instead of a
duplicate.

**Not enforced yet** in this deployment: no limiter is wired, so you will not see this today. It is
documented and mapped ahead of that, because clients need retry behaviour built *before* the limit
exists, not after.

---

## Adding a code

1. Raise `StoreError(type_, code, message, status)` — the taxonomy lives in one place.
2. Add a table row **and** a section here. Both are asserted.
3. `.venv/bin/python scripts/check_error_docs.py` — it fails on an undocumented code, a documented
   code nothing raises, and a status or type in this file that disagrees with the raise site.

---

# 00 — Measurements: the non-generative Context, and where the latency actually is

**Date:** 2026-07-26 · **Status:** measured, reproducible · **Dataset tag:** real founder corpus,
Cloud SQL `brain_rebuild`, user `founder`, live spine (events 42,081 · memories 2,365 ·
search_chunks 11,216 · embeddings 10,643 · kg_entities 408 · people 1,460 · fact_assertions 7,042).

Gates every number in the API design. Two of the three planned experiments ran; the third
(contextual retrieval) is scoped at the end and has not run.

Reproduce:

```bash
.venv/bin/python scripts/measure_fast_context.py --out artifacts/FAST_CONTEXT_2026_07_26.json
```

Artifact: `artifacts/FAST_CONTEXT_2026_07_26.json` (per-case grades, windows, latencies).

---

## 1. The headline: generation is currently SUBTRACTIVE

The question was whether a `fast` Context — retrieved, cited evidence that composes nothing — still
carries the answer.

> **CORRECTED 2026-07-29.** This section said "zero generative calls" and the table below recorded
> `0`. Both were false when written: `_envelope` returned a hardcoded `0` while
> `brain/retrieve/rewrite.py:expand()` called Gemini on every request. The real cost was **2** — one
> embedding, one generative rewrite. The rewrite has since been removed from the `fast` path
> (`artifacts/_ablation_fast_rewrite/`, branch DROP), so `fast` now spends **1** model call: the
> query embedding, which the ledger charges. The carry figures below
> are unaffected — they were graded on retrieved windows, and the rewrite did not move them (clean
> arm 1-1, p=0.75). Only the COST column was wrong.
>
> **AND THEN THE RECEIPT SPLIT, 2026-07-29 (later the same day).** For a few hours this note said
> that one call was what `cost.generative_calls` reports. It is not, and that was the same category
> error one layer up: **an embedding is not a generative call.** `CostReport` now carries three
> fields, and a ready `fast` Context reports `model_calls_total: 1 · embedding_calls: 1 ·
> generative_calls: 0`. Read the column below as *model calls*, which is what it counts and what
> the header now says.
>
> Graded on the same 43 held-out founder-confirmed cases, with the ruler's
own deterministic crit-fact grader (`brain.eval.heldout.grader`; writer ≠ checker holds, B4).

| Path | Model calls AS RUN (see note) | Carries / serves the answer | Confidently wrong |
|---|---:|---:|---:|
| **Non-generative Context, window 12** | **2** | **32/43 (74%)** | **0 — structural** |
| Non-generative Context, window 8 | 2 | 30/43 (70%) | 0 — structural |
| Full serve path `ask()` (v2-40 ruler, 2026-07-24) | up to 6 | 27/43 (63%) | 0 |

**The evidence already contains five answers the generative path fails to serve.** Route →
synthesize → judge is not adding comprehension on this corpus; it is losing cases retrieval found.

**And the fast tier cannot be confidently wrong, because it asserts nothing.** It hands over cited
evidence for the caller's model to read. It can be incomplete; it cannot fabricate. That is a
structural property, not a score — which is exactly the kind of guarantee a memory API can sell.

Trap behaviour: of 8 traps, the bait text appeared in the evidence window in **1** (V2-04). That is
a retrieval observation, not a B14 safety failure — conflating them would launder a retrieval fact
into a safety fact, which the grader doctrine forbids.

Context size is comfortable: median **2,585 estimated tokens** at window 12 (max 8,633), well
inside a 4k budget.

### The evidence-window budget costs 2 cases

Window 8 → 30/43; window 12 → 32/43. Two cases have their answer retrieved but crowded out of the
synthesizer's evidence window. This quantifies the "serve-window crowding class" already named in
`status/BRAIN_STATE_OF_THE_UNION_2026_07_24.md` — it is worth 2 cases, and `budget` is the knob
that exposes the trade to the caller.

---

## 2. The latency finding: generation was never the bottleneck

The design assumed removing generation was the route to the 200 ms SLO. **It is not.** Retrieval
alone measured **p50 2,437 ms · p95 3,495 ms** over the 43 cases — and note those numbers INCLUDE
the generative query rewrite nobody had noticed was on the path. With it removed (2026-07-29):
**p50 1,433.5 ms · p95 2,163.5 ms · p99 11,073.0 ms**, n=258 → see the run note. Still not 200 ms,
which was the finding then and remains it now.

> **NAME THE RUN, 2026-07-29.** Two ablations ran that day and this line quoted the first one
> without saying so, which reads as a contradiction of `docs/status/CURRENT_TRUTH.md` §1b:
>
> | artifact | n (variant B, counted, error-free) | p50 | p95 | p99 |
> |---|---:|---:|---:|---:|
> | `_ablation_fast_rewrite/20260729T024204Z_all.json` (quoted above) | 257 | 1,433.5 | 2,163.5 | 11,073.0 |
> | `_ablation_fast_rewrite/20260729T031018Z_all.json` (**the quotable one**, `CURRENT_TRUTH.md` §1b) | 258 | 1,433.4 | 2,085.0 | 11,116.2 |
>
> Both recomputed here from the raw artifacts with the same nearest-rank rule the harness uses
> (`index = ceil(q·n) − 1`, no interpolation, `scripts/_ablation_fast_rewrite.py:448`). The two
> agree on p50 to 0.1 ms and disagree on p95 by 79 ms — sampling, not a change in the system, and
> the conclusion (*not 200 ms; p95 above 2 s either way*) is identical. **Quote §1b of
> `CURRENT_TRUTH.md` when a number leaves this repo**, and always carry the run id: an unlabelled
> p95 is the reason this note exists.

Breakdown (12 queries, pooled connection and transport, embedding precomputed where noted):

| Component | p50 | Note |
|---|---:|---|
| `embed_query` (Vertex round trip) | **170 ms** | p95 181 ms. Not the problem, and cacheable — agents repeat queries. |
| **Lanes + fusion** | **1,586 ms** | **The bottleneck.** 90% of a pooled request. |
| Per-call setup (fresh DB conn + GeminiClient) | 464 ms | Pure waste; a pooled server pays zero. The measurement harness paid it. |

### Per-lane, and why this is good news

| Lane | p50 | max |
|---|---:|---:|
| `state_lane` | **425 ms** | 804 ms |
| `graph_lane` | **352 ms** | 539 ms |
| `fts_lane` | **347 ms** | 1,043 ms |
| `person_lane` | 97 ms | 286 ms |
| `vector_lane` | **90 ms** | 191 ms |
| `temporal_lane` | 65 ms | 418 ms |
| `life_event_lane` | 40 ms | 159 ms |
| `episode_lane` | 34 ms | 37 ms |
| `calendar_lane` | 33 ms | 65 ms |
| `kinship_lane` | 0 ms | 0 ms |
| **sum of medians** | **1,483 ms** | ≈ the 1,586 ms measured — **the lanes run sequentially** |

**The slowest single lane is 425 ms against a sequential sum of 1,483 ms.** So the lanes were made
to run concurrently, each on its own pooled read connection (`brain/spine/pool.py`), with the embed
submitted into the same fan-out so its 170 ms overlaps the eight lanes that do not need meaning.

### Fan-out: built and measured, 2026-07-26

| | p50 | p95 | max |
|---|---:|---:|---:|
| Sequential (pooled conn+transport) | 1,756 ms | 2,129 ms | 2,255 ms |
| **Concurrent, warm** | **1,020 ms** | **1,078 ms** | 1,176 ms |
| End-to-end over the 43 cases (harness pays fresh setup) | 2,437 → **1,465 ms** | 3,495 → **1,872 ms** | |

**Measured speedup 1.72× — not the ~3.5× predicted above.** The prediction assumed the lanes were
independent; they are independent in *code* but they contend for one Postgres instance. Wall clock
did not fall to the slowest lane (425 ms) because under concurrency every lane gets slower. **The
database is the shared resource, and that is the honest ceiling on this technique.**

**Correctness: proven identical.** Fused candidate order compared sequential vs concurrent across
all 43 cases — **43/43 byte-identical**. The non-generative board is unchanged (30/43 at window 8,
32/43 at window 12, same single trap-bait case). It is a scheduling change and nothing else.

**Consequence for the roadmap:** the remaining path to 200 ms is **not** more concurrency. It is
making the three slow lanes cheaper — `state` (425 ms), `graph` (352 ms), `fts` (347 ms) — a
targeted indexing problem, plus connection reuse (`pg_connect` measures **320 ms**, so a server must
never open one per request).

### A correction to the design brief

**The vector lane costs 90 ms.** ANN over 10,643 halfvec(3072) vectors is not a latency problem, so
the embedding-dimension decision is **purely a storage and index-memory question at scale** — it
must not be justified on serving latency. The design brief implied an ANN-latency motive; that is
wrong and is corrected there.

---

## 3. What this means for the product

1. **`fast` is the product.** It carries more (74% vs 63%) for **one model call against up to six**,
   and cannot fabricate. `thorough` is the upsell, and it
   must be shown to *add* something before it is worth its price — on this corpus it currently
   subtracts.

   > **CORRECTED 2026-07-29 — the cost RATIO is withdrawn; the call counts replace it.** This read
   > *"costs ~1000× less (one embedding call versus six generative calls at ~$0.0075)"*. Two things
   > were wrong with it. **The numerator was not one call** — when this was written `fast` was
   > spending two, an embedding plus an undetected generative query rewrite, so the comparison was
   > 2-vs-6 dressed as 1-vs-6. **And the denominator of the ratio was a zero that is not zero**: the
   > embedding is a real Vertex round trip (p50 170 ms, §2), never priced here, so "1000× less" was
   > a generation-token ratio presented as a request-cost ratio. The `$0.0075` is a real figure but
   > it is the *`ask()` path's* per-answer cost from `docs/status/STRESS_REPORT_2026_07.md`, not a
   > per-Context one. **1 model call vs up to 6 is countable, verifiable and enough** — a priced
   > ratio needs the embedding's price, and nobody has looked it up.
2. **The 200 ms SLO is credible but not free**, and the work is lane concurrency plus three SQL
   indexes — not model choice, not a rewrite, not a second store.
3. **Publish `p50/p95` next to `confidently_wrong_rate`.** Both are now measured.

---

## 4. Two live defects found while connecting (not part of the experiment)

Both concern `BRAIN_DATABASE_URL` and both are real today:

1. **`db.ping()` and `db.table_exists()` return False against a perfectly healthy database.** The
   `com.brain.sync` launchd agent supplies a bare `postgresql://…` URL. SQLAlchemy resolves that to
   psycopg2, which is not installed (`ModuleNotFoundError: No module named 'psycopg2'`), so every
   SQLAlchemy-path helper fails closed. `/health` reports the DB down while the pipeline runs fine,
   and every liveness detector built on `table_exists()` would conclude no table exists.
2. **No single value of the variable satisfies both paths.** `brain/spine/db.py:70` *strips*
   `+psycopg` for the raw psycopg3 path, and `founder_confirmed_v2._data_now()` calls
   `psycopg.connect(os.environ["BRAIN_DATABASE_URL"])` directly — which requires the bare form —
   while `db.ping()` requires the `+psycopg` form. The two are mutually exclusive.

**Fix:** normalize in one place (`config.load_settings`), so the SQLAlchemy URL always carries
`+psycopg` and the raw path always strips it, and route `_data_now()` through `db.pg_connect()`.
One-line-class changes, but they need the gate, so they are filed here rather than made.

---

## 5. Not run

**Experiment 3 — contextual retrieval.** Prepending a short statement of where a chunk sits before
embedding it, published at ~35% fewer top-20 retrieval failures (~49% with a lexical lane, which we
have). Requires re-embedding a bounded sample and re-running this same harness — the harness now
exists, so this is a scoped, cheap follow-up. Given §1's result (evidence carries 74%, the misses
are retrieval misses, not synthesis misses), **this is now the highest-value remaining experiment**:
every one of the 11 misses at window 12 is a case where the answer was not retrieved at all.

---

# Two measurements nobody had run: `thorough` on the trap axis, and the first load test

**Date:** 2026-07-27 · closes the measurement holes named in [`GAPS.md`](./GAPS.md) **B4** and
**B5**. Everything below is reproduced by two commands against the live spine
(`SpineStore(user_id='founder')` through `POST /v1/brains/{id}/context` — the same `/v1` a
customer would call):

```bash
.venv/bin/python scripts/measure_thorough_traps.py   # → artifacts/THOROUGH_TRAPS_2026_07_27.json
.venv/bin/python scripts/measure_load.py             # → artifacts/LOAD_TEST_2026_07_27.json
```

Numbers are dated snapshots of a LIVE corpus (the sync tick that finished 19:27:28 EDT landed
2,818 embed changes minutes before these runs; 12,085 active embeddings). Neither number belongs
in `CURRENT_TRUTH.md` without its dataset tag.

---

## B4 — `confidently_wrong` on `mode: thorough`, measured for the first time

### What `thorough` actually is on the real engine

Read from `memory_api/spine.py` and then **proven empirically** (not just by code-reading):

- `SpineStore.create_context` **never reads `mode`**. The string `"mode"` appears exactly once in
  the file — `spine.py:532`, where `stream_context` echoes it into the SSE `status` frame. Both
  paths assemble evidence through the same `_assemble()` → `_envelope()`.
- The only behavior `mode` changes is in `app.py:429-446`: `Accept: text/event-stream` is honoured
  for `thorough` and refused for `fast` (406 `streaming_not_applicable` — confirmed live).
- Measured across all 20 cases: served `text` **identical 20/20** between fast JSON and thorough
  JSON, and **identical 20/20** between thorough JSON and the SSE terminal `context` frame.
  Latency is indistinguishable (fast p50 1,286 ms · thorough JSON 1,274 ms · thorough SSE 1,312 ms).

**So `thorough` is currently `fast` with streaming.** Nothing generative, no multi-hop, no second
retrieval pass. The contract survives on the word "may" (`openapi.yaml:875` — "`thorough` may
multi-hop and return 202"), but the mode knob buys a customer nothing except SSE framing today.
That is a knob whose two positions serve one behavior — GAPS-A-adjacent, and now it is written
down instead of implied.

> **RETRACTED 2026-07-29 — the paragraph below is false and is kept only as the record of how.**
>
> It concluded the spine's cost receipt was honest. It was not. The reasoning was circular: it read
> `generative_calls: 0` out of 60 responses and treated the agreement between a **hardcoded constant
> and itself** as evidence. `_envelope` returned the literal `0` on every path; nothing counted
> anything. Meanwhile `brain/retrieve/rewrite.py:expand()` was calling Gemini on **every single
> request**, so the true number was 2 — one embedding, one generative rewrite.
>
> The observation "observed `[0]` across all 60 responses" is therefore not a finding about the
> engine; it is 60 readings of the same constant. **A receipt that cannot be wrong is not a
> receipt** — and the check that would have caught it is the one now in
> `memory_api/tests/test_fast_spends_one_embedding.py`: break generation and see whether the engine
> notices.
>
> Ironically, the paragraph's *other* half was right for the right reason — the fixture's `1` was
> called a lie, when the fixture was the only thing in the building reporting a non-constant.
>
> Superseded by: `artifacts/_ablation_fast_rewrite/` (two 516-request runs, 2026-07-29) and the
> corrected `CostReport` in `openapi.yaml`. `fast` now spends **one model call — the query
> embedding — and generates nothing**, reported as three separate fields
> (`model_calls_total: 1 · embedding_calls: 1 · generative_calls: 0`) read from the ledger. The
> receipt is three numbers because one integer could not distinguish "it called a model" from "it
> composed text", and shipping it as one is what let a hardcoded `0` pass for a measurement.

~~**Does `cost.generative_calls` tell the truth?** On the spine engine, **yes**: `_envelope`
hardcodes `generative_calls: 0` (`spine.py:575`) and zero generative calls actually happen, on
every path, both modes — observed `[0]` across all 60 responses. On the **conformance fixture**
it lies: `inmemory.py:403` reports `generative_calls: 1` for `thorough` while making zero
generative calls. The fixture misrepresents a cost field the engine reports honestly — small, but
exactly the class of self-misrepresentation GAPS section A exists for.~~

### The first measured trap board for `thorough`

8 trap cases (`abstain_ok`) + 12 non-trap cases (2 per class, deterministic pick) from
`brain.eval.heldout.founder_confirmed_v2.CASES`, graded on the SSE terminal frame's `text` with
the full `brain.eval.heldout.grader` — `abstain_ok` and `answer_must_not` **included**, which the
prior fast-context measurement deliberately did not do.

| trap | grade | why |
|---|---|---|
| V2-03 (Pinnie $4M plan) | CORRECT · correction | window carries the Pinnie/Aashay attribution |
| **V2-04 (June ad spend)** | **confident_wrong** | **bait `$44k` present in the served text** |
| **V2-26 (my wedding)** | **confident_wrong** | window returns evidence, neither abstains nor corrects |
| **V2-27 (engagement party)** | **confident_wrong** | window returns evidence, neither abstains nor corrects |
| V2-28 (+30 customers) | CORRECT · correction | bait absent, correcting facts carried |
| V2-29 (Hertz confirmation) | CORRECT · correction | |
| V2-30 (Chance concert) | CORRECT · correction | |
| V2-31 (419 3rd Ave rent) | CORRECT · correction | |

**Headline: `confidently_wrong` = 3/8 traps (3/20 cases) on `thorough` under the full grader.**
Non-trap carries: 8/12 (misses V2-01 · V2-32 · V2-33 · V2-39, all `incomplete` — consistent with
the known board misses). Transport errors: 0.

### How to read a non-zero number on a non-asserting tier — the decomposition

The grader was built for **asserted answers**; a Context asserts nothing — it hands cited evidence
to the caller's model. The 3 decompose into two different defects, and only one of them contains
bait:

- **1/8 — bait physically in the served window (V2-04).** The PINNIE PLAN note (`Ad Spend: $44K`)
  is retrieved for "how much did **we** spend on ads", so the one number the founder ruled must
  never be served as ours ships to the caller's model as top evidence, ownership unmarked. This is
  the same single window-bait the fast measurement found
  (`artifacts/FAST_CONTEXT_2026_07_26.json` → `traps.bait_in_window: ["V2-04"]`), and on the date
  of this run `thorough` **was** `fast` — the two tiers ran identical code, so the result carried
  over necessarily. **That premise expired on 2026-07-29** (see the note below): `thorough` now
  runs a generative query rewrite that `fast` does not, so a future run has to re-measure this
  rather than inherit it.
- **2/8 — the window neither abstains nor corrects (V2-26, V2-27).** For "when is my wedding"
  (founder is single) the tier returns ~11.7k chars of wedding-adjacent evidence about *other
  people's* events, with nothing that corrects the false premise. No bait token is served — the
  defect is that a downstream model reading that window has every invitation to answer the
  question as asked.

**So both statements from GAPS B4 are now measured, not argued:** the tier still *asserts*
nothing — `confidently_wrong = 0` in the strict fabrication sense remains structural — but on the
date of this run `thorough` had no generative path of its own to measure, and when the trap ruler
is applied to what the tier actually serves, the number is **3/8, not 0**. The structural 0 is a
property of who does the asserting, not of what gets served.

> **"THE DAY `thorough` GROWS A REAL GENERATIVE PATH" ARRIVED ON 2026-07-29, AND THIS BOARD IS NOW
> THE PRE-REGISTERED BASELINE IT MUST BEAT — UNRUN.** `brain/retrieve/rewrite.py:expand()` was
> always on this path; nobody had noticed, which is why the sentence above says `thorough` had no
> generative path. It had one, and so did `fast`. What changed is that the rewrite is now spent
> **only** on `thorough` (`spine.py:_assemble`, `expand_query=(mode == "thorough")`), so the two
> tiers genuinely differ for the first time. Consequences for this section, stated rather than
> quietly absorbed:
>
> · **The 3/8 and the 1/8 stand as measurements** of what was served on 2026-07-27, by both tiers.
> · **They no longer transfer between tiers.** Any claim that a `fast` trap result predicts a
>   `thorough` one, or the reverse, now needs its own run.
> · **Nothing has re-measured `thorough` since the split.** The honest status of the B4 axis on
>   today's `thorough` is *unmeasured*, not *3/8*. Re-run
>   `scripts/measure_thorough_traps.py`, whose own premise block carries the matching retraction.

---

## B5 — the first load test: every prior latency figure was single-threaded

In-process threaded, one `TestClient` per thread (own portal/event loop), barrier-started levels
of 1 / 4 / 8 / 16 threads × 8 sequential fast-context requests each — **232 requests, every one a
distinct query** (seeded shuffle over the 43 held-out + 36 lifelike questions × 4 phrasings; a
derived-but-distinct pool, stated plainly). 2 uncounted warmup requests absorb first-touch costs
(the first was 12.6 s cold, the second 1.8 s).

| concurrency | n | p50 | p95 | max | errors | wall | throughput |
|---|---|---|---|---|---|---|---|
| 1 (baseline) | 8 | **1,552 ms** | 2,702 ms | 2,702 ms | 0 | 14.1 s | 0.57 req/s |
| 4 | 32 | **2,680 ms** (×1.73) | 11,320 ms (×4.19) | 11,595 ms | 0 | 38.0 s | 0.84 req/s |
| 8 | 64 | **4,997 ms** (×3.22) | 11,476 ms (×4.25) | 13,299 ms | 0 | 49.9 s | 1.28 req/s |
| 16 | 128 | **10,987 ms** (×7.08) | 15,883 ms (×5.88) | 19,309 ms | 0 | 94.0 s | 1.36 req/s |

Prior single-threaded p50 for reference: ~1,291 ms (2026-07-26, same HTTP path); this run's fresh
c=1 baseline is 1,552 ms on today's larger corpus and longer phrasings.

**The shape:** zero errors at every level — it degrades, it does not break — but **throughput
saturates at ~1.3–1.4 req/s** (c=8 → c=16 adds 0.08 req/s while p50 doubles). Past c≈8, added
concurrency converts almost entirely to queueing. The mechanism is in the code, not conjecture:

1. **`brain/spine/pool.py` `DEFAULT_SIZE = 10`** — sized to *one request's* lane fan-out,
   explicitly "not server concurrency". At c=16, ~10 lanes × 16 in-flight requests contend for 10
   pooled connections; the pool becomes the server-wide ceiling it was never meant to be.
2. **Every request opens its own `pg_connect()`** for the main path — measured 320 ms p50 through
   the cloud-sql-proxy (design prompt §3, "a server must never open one per request" — it does).
3. **One Gemini `embed_query` HTTPS call per request**, N-wide against shared quota (no 429s
   observed at these levels).

**Wall-clock / sync-tick honesty:** the launchd sync agent (`StartInterval` 1200 s) is the known
retrieval-degrader (12.6 s / 2.9 s / 1.8 s under `brain.flow.refresh`). This run landed cleanly
**between ticks**: the previous tick finished 19:27:28 EDT (`runs/sync/sync.out.log`, 22/22
stages ok), levels ran 19:29:42–19:33:07 EDT, and the log's (mtime, size) — sampled before and
after every level — never moved. **No latency jump is attributable to the tick; the curve above
is pure concurrent-read contention.** The 8 slowest requests (15.7–19.3 s) all sit inside the
c=16 window, timestamps in the artifact.

**What this does NOT measure,** so the number cannot be over-read: a single Python process (GIL
shared by client threads and server handlers — an external multi-process driver would be
stricter), reads only (concurrent-write degradation remains measured separately and worse),
~90 s per level (no sustained-load soak), localhost (no network between caller and API).

---

## What moved because of this page

- **GAPS B4** is no longer "never measured": `thorough` = `fast` + SSE (proven 20/20 byte-equal),
  first trap board 3/8 · bait-in-window 1/8 · fixture's `generative_calls: 1` is a fiction worth
  a one-line fix.
- **GAPS B5** is no longer "no load test": the engine holds 0 errors to c=16 but saturates at
  ~1.4 req/s with p50 ×7. The first latency work under load is already named in the code: a real
  server-sized connection strategy (pool sized for concurrency + no per-request `pg_connect`),
  ahead of any algorithmic change.

---

# SDKs — generated from the contract, never written by hand

**Date:** 2026-07-29 · Backlog item **S3** · Script: [`scripts/generate_sdks.sh`](../../../scripts/generate_sdks.sh)

Two SDKs — TypeScript and Python — are **functions of** [`openapi.yaml`](./openapi.yaml). Nobody
writes one. Nothing generated is committed. One command produces both, and the command fails if the
contract is invalid or either SDK does not build.

---

## Run it

```bash
cd your checkout/brain-platform
scripts/generate_sdks.sh
```

Real output, re-run 2026-07-29 from a clean generated tree:

```
── toolchain
── validating docs/initiatives/memory-api/openapi.yaml
docs/initiatives/memory-api/openapi.yaml: OK
   every $ref resolves
   contract version 0.1.0
── python
Generating dist/sdk/python/brain-memory-api-client
   python sdk imports: the two calls that are the whole product are present
── typescript
@hey-api/openapi-ts v0.99.0
[Job 1] ✓ dist/sdk/typescript/src · 4 files · 92ms
   typescript sdk typechecks under --strict
── postcondition
   all 15 operations present in both SDKs

generated v0.1.0 from docs/initiatives/memory-api/openapi.yaml
  dist/sdk/python/brain-memory-api-client   88 files
  dist/sdk/typescript                       16 files
```

The dated 2026-07-27 timing snapshot was `10.5 s` cold with warm pip/pnpm caches and `4.1 s` warm;
the 2026-07-29 run above re-verifies contract shape, imports, typechecking, operation presence, and
file counts rather than claiming a new benchmark. The TypeScript side needs `pnpm` **or** `npx` on
`PATH` — this machine has only `pnpm`, so the `npx` branch is written but unproven and CI is where
it gets its first real run. Python needs nothing but an interpreter ≥3.11: `/usr/bin/python3` on
macOS is 3.9 and silently resolves the generator to a years-old release instead of failing, so the
script picks the interpreter rather than inheriting it.

---

## What you get

```
dist/sdk/                     gitignored, disposable, NEVER hand-edited
  .toolchain/py/              the pinned generators, in their own venv
  python/brain-memory-api-client/   88 Python files · 8,277 lines · installable package
  typescript/                 16 TypeScript files · 3,291 lines · package.json + tsconfig + src/
```

**1,202 lines of contract → 11,568 lines of client, in two languages.** That ratio is
the whole argument: it is the volume of code nobody has to review, keep in sync, or be wrong in.

All **15 operations** are present in both, named by `operationId` — `createBrain`, `getBrain`,
`forgetBrain`, `getProfile`, `getCensus`, `listTraces`, `getTrace`, `rememberRecord`, `getRecord`,
`forgetRecord`, `listMemories`, `getMemory`, `acceptObject`, `getObject`, and `createContext`.

The TypeScript package has **zero runtime dependencies** — the fetch client is emitted inline, so
there is no third-party package between a caller and the contract.

The Python package **builds, installs and imports** as part of the run. A generator that emits a
tree of files that does not import is a generator that failed quietly; this one is checked.

### The knob budget lands in the type system

This is the property that makes generation more than convenience. `ContextRequest` in the generated
Python, unedited:

```python
query: str
purpose: str | Unset = UNSET
as_of: datetime.datetime | Unset = UNSET
budget: ContextRequestBudget | Unset = UNSET
include: list[ContextRequestIncludeItem] | Unset = UNSET
schema: ContextRequestSchema | Unset = UNSET
filter_: ContextRequestFilter | Unset = UNSET
mode: ContextRequestMode | Unset = ContextRequestMode.FAST
```

Seven read knobs, and `mode` defaults to `fast` because the contract says so. An eighth knob appears
here, and in `types.gen.ts`, in the same commit that adds it — visible to every reviewer, in two
languages, before `check_api_simplicity.py` even runs.

---

## The gotcha this built: **all three tools fail silently**

Found while checking that the validator was not a no-op, 2026-07-27. Point `createBrain`'s
`requestBody` at `#/components/schemas/NoSuchSchema` and:

| | Result on a dangling `$ref` | Exit code |
|---|---|---|
| `openapi-spec-validator` | `openapi.yaml: OK` | **0** |
| `openapi-python-client` | `WARNING … Endpoint will not be generated` — the endpoint is *dropped* | **0** |
| `@hey-api/openapi-ts` | `Skipping unresolvable $ref` — the operation is emitted **with a hole** | **0** |

Three tools, three green lights, one SDK missing an operation the contract promises. A naive
"validate then generate" script reports success. `openapi-spec-validator` checks the document
against the OpenAPI JSON Schema; it does **not** resolve local pointers, and neither generator
treats an unresolvable one as fatal.

So the script does not trust any of them:

1. **Every `$ref` is resolved** before anything is generated, and an unresolvable one is fatal with
   the exact JSON path. External (`http://…`) refs are refused outright — a contract that is only
   complete over the network is not reproducible.
2. **`--fail-on-warning`** on `openapi-python-client`, which is off by default. A partial SDK is not
   a green build.
3. **A postcondition**: every `operationId` in the contract must exist as a module in the Python
   tree *and* as an `export const` in `sdk.gen.ts`. This is the one check that catches the
   TypeScript case, because there the file count does not change.

All three were verified to fire, not assumed to (see Verification).

The general form is one the repo already knows: **a check that cannot express failure is not a
check.** "It validated" and "it generated" are both satisfiable by a tool that quietly did less
than you asked.

---

## Why generated and not hand-written

**A hand-written SDK is a second implementation of the contract, and its failure mode is silent.**
It keeps compiling after the server changes, because it encodes its author's belief about the API
rather than the API. The divergence surfaces at a customer, months later, in the one path nobody
wrote a test for. This is the same class of error the repo already spends gates on: a generated
FastAPI schema can only ever agree with itself, which is why
[`check_api_conformance.sh`](../../../scripts/check_api_conformance.sh) fuzzes against the
**checked-in** `openapi.yaml`. An SDK written by hand reintroduces exactly the problem that check
exists to prevent, one layer further out — and further out is worse, because it is the layer the
customer touches.

Four consequences that are worth more than the typing they save:

1. **The contract cannot lie about the client.** The only way for the SDK to be wrong is for the
   contract to be wrong — and the contract has two gates on it already.
2. **An ungeneratable spec is a spec bug, caught at build time.** Validation and `$ref` resolution
   run before generation and the script exits non-zero. A contract that no generator can read is a
   contract no customer can implement either; better to learn that here.
3. **Surface is visible.** See above. Quiet API growth is the thing the simplicity law exists to
   stop, and generation makes the growth show up as a diff in a typed file.
4. **Marginal cost per language is ~zero.** Go, Rust, Kotlin are one more line in the script, not
   one more artifact to keep honest.

The rule that follows: **hand-written code never lives inside `dist/sdk/`.** If an ergonomic wrapper
is ever wanted (`brain.remember(...)` over `rememberRecord(client, ...)`), it is a separate,
committed package that *depends on* the generated one. The generated tree stays a pure function of
the contract, or it is not one.

---

## The generators, and one we declined

| | Tool | Pin | Why |
|---|---|---|---|
| Python | [`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client) | `0.29.0` | Reads OpenAPI 3.1 natively. `httpx` + `attrs`, typed, `py.typed`, sync and async. |
| TypeScript | [`@hey-api/openapi-ts`](https://github.com/hey-api/openapi-ts) | `0.99.0` | Reads 3.1 natively, emits a self-contained fetch client, no runtime deps. |
| — | `ruff` | `0.15.0` | `openapi-python-client` shells out to it to format its output. Pinned into the toolchain so the format step cannot silently skip. |
| — | `openapi-spec-validator` | `0.7.2` | The fail-fast. Dev-only, toolchain-only. |
| — | `typescript` | `5.9.3` | Peer of the TS generator, and the `--strict` typecheck of its output. |

**`openapi-generator` was evaluated and declined**, for two reasons — the first checked here, the
second on the record and *not* verifiable here:

- **It is a JVM tool and this machine has no JVM** — checked: `java -version` fails,
  `/usr/libexec/java_home` finds nothing, no JDK under `/Library/Java/JavaVirtualMachines`. It could
  not be *run*, so a build step using it could not be *verified* — and an unverified build step is
  how a "generated" SDK quietly becomes a hand-written one, the first time somebody cannot run the
  script.
- **Its OpenAPI 3.1 support is partial and works by down-conversion to 3.0.** This one is the
  project's own documented position, not something measured here — with no JVM there was no way to
  test it. If it is out of date, the check is cheap: install a JDK and diff the generated types
  against the shapes this contract leans on — `type: [string, 'null']` (`valid_to`, `text`,
  `abstained`, `next_cursor`, `if_version`), `const` (the `object` discriminator on every resource),
  and 3.1 `examples`. A generator that has to lower the contract to read it produces a client for a
  different contract.

Both chosen tools are open source and read 3.1 as written. If a JVM ever lands on the build host and
a language is wanted that only `openapi-generator` covers, add it as a **third** generator; do not
replace these.

The pins are load-bearing: **a generator upgrade is an SDK change.** Bumping any version above is
its own commit, reviewed against the regenerated diff.

---

## Release policy

1. **The SDK version *is* the contract version.** `package.json` and the Python `pyproject.toml`
   both take `info.version` from `openapi.yaml`. There is no field anywhere to type a different
   number into. An SDK cannot claim a version the contract does not have.
2. **Nothing generated is committed.** `dist/sdk/` is gitignored twice over
   ([`.gitignore`](../../../.gitignore): `dist/` and `dist/sdk/`). Thousands of generated lines in a
   pull request is how a real ten-line diff gets missed, and a generated file in the tree is a file
   somebody eventually edits.
3. **A release is a contract release.** Bump `info.version` → `check_api_simplicity.py` GREEN →
   `check_api_conformance.sh` GREEN → `generate_sdks.sh` GREEN → publish → tag `sdk-v<version>`.
   The gates come first; an SDK is downstream of a contract that passed.
4. **A change that fails the simplicity law never ships**, in any language. The generator will
   happily emit an eighth knob. The gate is what refuses it.
5. **Regenerate on every contract change, not on demand.** The correct time to discover the spec no
   longer generates is the commit that broke it.

**Not yet automated, stated plainly:** the publish step itself. The npm scope `@brain` is not
claimed and the PyPI name is not registered, so `generate_sdks.sh` stops at a built, typechecked,
importable package in `dist/sdk/` and does not push anywhere. Wiring `npm publish` / `twine upload`
into CI is a follow-up to S3, not part of it.

---

## Known warts, not patched

Two name collisions the contract's own nouns cause. Both are real; neither was silently worked
around, because working around them means editing the contract to suit a generator:

- **TypeScript** exports `Record`, `Object` and `Error` as types — all three shadow globals. A
  consumer writing `import type { Record } from '@brain/memory-api'` loses TypeScript's `Record<K,V>`
  utility type *in that file*. Namespace import (`import type * as Brain from …`) avoids it.
- **Python** renames the `filter` knob to `filter_` (`ContextRequest.filter_`), the generator's
  standard escape for a builtin.

Neither is worth renaming a resource over. Both belong in the quickstart (S10) so a stranger meets
them in the docs rather than in a compiler error.

---

## Verification, 2026-07-27

The contract validates as OpenAPI 3.1 — **no errors, nothing patched to make it pass.**
`generate_sdks.sh` runs this before it generates anything; to run it alone (after one
`generate_sdks.sh` has built the toolchain):

```
$ dist/sdk/.toolchain/py/bin/python -m openapi_spec_validator docs/initiatives/memory-api/openapi.yaml
docs/initiatives/memory-api/openapi.yaml: OK
```

**That "OK" was itself checked.** Against a copy with `operationId` set to an integer and
`responses` to a string, the same validator exits 1 with
`Validation Error: 12345 is not of type 'string'` and the failing instance path — so the green above
is a result, not a default.

**And each of the three added guards was made to fire**, by running the shipped code against a
deliberately broken copy of the contract (`#/components/schemas/BrainCreate` →
`#/components/schemas/NoSuchSchema`) and against an amputated pair of SDKs:

```
$ <shipped $ref check> broken.yaml
   UNRESOLVABLE $ref  #/components/schemas/NoSuchSchema  at  $.paths./brains.post.requestBody.content.application/json.schema
exit 1

$ openapi-python-client generate --path broken.yaml --fail-on-warning …
WARNING parsing POST /brains within brains. Endpoint will not be generated.
exit 1                                              # exit 0 without the flag

$ <shipped postcondition> openapi.yaml <sdk tree with create_brain.py deleted and createContext renamed>
   MISSING OPERATION  python: createBrain (create_brain.py)
   MISSING OPERATION  typescript: createContext
exit 1
```

The simplicity law is unaffected by this work — no resource, verb, or knob was added, because an
SDK is not surface:

```
$ .venv/bin/python scripts/check_api_simplicity.py
  resources        5/5   Brain, Context, Memory, Object, Record
  verbs            6/6   accept, create, forget, get, list, remember
  read knobs       7/7
  write knobs      3/3
GREEN — the simplicity law holds.
```

The generators live in `dist/sdk/.toolchain/py`, **not** in the project `.venv`.
`openapi-spec-validator` pins `referencing<0.37.0` and installing it into `.venv` downgrades a
package the conformance gate resolves through. An SDK build must not be able to perturb a gate, so
the toolchain is isolated and the project venv is left exactly as found (`pip check`: no broken
requirements).

---

# MCP

Brain exposes four bounded tools at the authenticated Streamable HTTP endpoint `/mcp`. The MCP
adapter calls the same Store, models, scopes, tenancy rules, errors, watermarks, and receipts as
the HTTP API. It does not contain retrieval or ingestion logic.

## Connect

Keep the credential in `BRAIN_API_KEY`. A hosted MCP credential is bound by the operator to one
Brain, so no tool accepts a project or Brain override.

### Codex

```bash
export BRAIN_API_KEY="..."
codex mcp add brain \
  --url https://your-brain.example/mcp \
  --bearer-token-env-var BRAIN_API_KEY
```

### Claude Code

```bash
export BRAIN_API_KEY="..."
claude mcp add --transport http brain https://your-brain.example/mcp \
  --header 'Authorization: Bearer ${BRAIN_API_KEY}'
```

### Cursor

Put this in `~/.cursor/mcp.json`. Launch Cursor from an environment that provides
`BRAIN_API_KEY`; do not replace the placeholder with the secret.

```json
{
  "mcpServers": {
    "brain": {
      "url": "https://your-brain.example/mcp",
      "headers": {"Authorization": "Bearer ${env:BRAIN_API_KEY}"}
    }
  }
}
```

### Any Streamable HTTP client

```json
{
  "url": "https://your-brain.example/mcp",
  "headers": {"Authorization": "Bearer ${BRAIN_API_KEY}"}
}
```

The current design-partner path uses a minted, Brain-bound, scoped, revocable bearer key. Brain
does **not** claim a partial OAuth implementation. Broader self-serve discovery waits for a full
authorization server with discovery, PKCE, audience validation, expiry, refresh, and revocation.

## The four tools

| Tool | Default authority | Use |
|---|---|---|
| `brain_status` | read-only | Verify identity, environment, scopes, and capabilities without returning memory content. |
| `context` | read-only | Retrieve task-bounded Context with citations, unknowns, conflicts, processing, abstention, budget, cost, and watermark. |
| `get_record` | read-only | Resolve a citation to the underlying custody Record. |
| `remember` | additive | Add one Record with an explicit stable idempotency key; never deletes or accepts application authority. |

The default customer credential should carry only `records:read` and `context:read`. Add
`records:write` separately when the product needs `remember`.

## Integration sequence

1. Call `brain_status` and verify the expected Brain and scopes.
2. Call `context` at one real product decision point.
3. Preserve every returned state; do not turn an unknown or conflict into a fluent assertion.
4. Resolve at least one returned `record_id` with `get_record`.
5. Run the application tests and `brain doctor --json`.
6. Only then enable `remember`, using stable source identity and an idempotency key. Pass the
   returned watermark to `context` as `min_watermark` for read-your-write.

Every successful tool response has a `request_id`. Tool failures serialize the HTTP taxonomy:
`type`, `code`, `message`, `request_id`, and `docs_url`.

## Install the Integrator

The CLI is distributed directly from the service while the package remains unpublished:

```bash
curl -fsSL https://your-brain.example/guide/downloads/install.sh | sh
export BRAIN_API_KEY="..."
brain connect --url https://your-brain.example/mcp --company "Acme" --client auto
brain doctor --json
```

The installer verifies the executable checksum. `brain connect` writes only environment
references to client configuration and a non-secret `.brain/integration.json` receipt to the
customer repository.

## Local stdio

Stdio is a development bridge, not the customer path. It requires both `BRAIN_MCP_PROJECT` and
`BRAIN_MCP_BRAIN`; there is no founder default.

```bash
export BRAIN_MCP_PROJECT="test:local"
export BRAIN_MCP_BRAIN="demo"
.venv/bin/python -m memory_api.mcp_server
```

## Proof boundary

The public sandbox uses `InMemoryStore`. It proves protocol and contract behavior, not customer
durability. Customer proof must run against the authenticated `SpineStore` service with a
dedicated test tenant.

---

# Connect Brain

Connect Brain. Your coding agent integrates it, tests it, and shows you the proof.

## Five-minute setup

Ask the Brain operator for three values:

- the hosted MCP URL;
- a Brain-bound key with `records:read` and `context:read`;
- the company or personal Brain name you expect `brain_status` to return.

Install the unpublished, checksum-verified CLI directly from your service:

```bash
curl -fsSL https://docs.brain.new/downloads/install.sh | sh
export BRAIN_API_KEY="..."
brain connect --url "$BRAIN_MCP_URL" --company "Acme" --client auto
brain doctor --json
```

`brain connect` detects Codex, Claude Code, and Cursor. Use `--client codex,claude,cursor` to name
them explicitly. The installer verifies the CLI, canonical Skill, reference, and generated client
wrappers; `brain connect` installs the matching wrapper without overwriting a customer-owned file.
It never accepts a key as an argument and never writes one into the repository.

## Connect your agent

Give the agent the generated `brain-integrate` Skill, then ask it to resume from
`.brain/integration.json`. The receipt is the state machine; chat history is not.

The agent must begin read-only, run `brain_status`, integrate Context at one product seam, resolve
one citation with `get_record`, preserve the full response state, run the repository tests, and
run the doctor. It may add `remember` only when writes are explicitly requested.

## Codex setup

```bash
codex mcp add brain --url https://your-brain.example/mcp \
  --bearer-token-env-var BRAIN_API_KEY
```

Copy the generated Skill to your Codex skills directory, or keep it in the project instructions.
The CLI emits the exact path it configured.

## Claude Code setup

```bash
claude mcp add --transport http brain https://your-brain.example/mcp \
  --header 'Authorization: Bearer ${BRAIN_API_KEY}'
```

The generated Claude wrapper and canonical Skill have the same workflow; neither contains a
credential.

## Cursor setup

`brain connect --client cursor` merges only the `brain` entry into `~/.cursor/mcp.json` and
preserves unrelated servers. The header keeps `${env:BRAIN_API_KEY}` as an environment reference.
If Cursor was opened from the Dock, ensure its process receives that environment variable.

## Generic MCP

Use Streamable HTTP at `/mcp` with `Authorization: Bearer $BRAIN_API_KEY`. Tool discovery returns
exactly `brain_status`, `context`, `get_record`, and `remember`.

## Read-only integration

Call `context` where the application must decide using prior facts—not at every screen or model
call. Preserve:

- `status`, including `processing`;
- `citations` and their `record_id` values;
- `unknowns`, `conflicts`, and `abstained.reason`;
- `budget`, `cost`, `watermark`, and request identity.

Resolve at least one citation with `get_record` before the application treats Context as evidence.
The application—not Brain and not the coding agent—keeps authority over accepted business state.

## Idempotent writes

Add `records:write` only when the product genuinely needs new Records. One logical event gets one
stable source identity and one stable idempotency key. A retry reuses both. Pass `remember`'s
watermark to the next `context` call as `min_watermark`; `processing` is an honest result, not a
failure to hide.

On a dedicated test Brain with an explicitly writable key, `brain doctor --json --write-probe`
adds two labelled synthetic Records with `visibility: shareable`: a stable Record whose replay must
return the same Record and watermark, plus a per-run freshness Record whose watermark must produce
a cited read-your-write result that resolves. Do not run it on customer production data by accident.

## Citations and authority

Records are immutable inputs. Memories are derived claims. Objects are application-accepted state.
Context is a bounded view for one task. A model can propose an Object; it cannot silently accept
one. A citation proves what evidence Brain used, not that the application must agree with it.

## Processing and watermarks

A successful write can still return a processing Record. The watermark names the evidence
boundary. Retry Context with that boundary until it is ready, using bounded backoff. Never serve
older Context as if it included the new write.

## Errors and repair

`brain doctor --json` checks authentication, identity, tool discovery, annotations, scopes,
failure shapes, Context, and citation resolution. Each failure records a stable code, surface,
whether an agent can repair it, the next action, retry command, request ID when available, and a
documentation URL.

Run `brain receipt` to inspect progress and `brain support-bundle --json` for a redacted handoff.
Neither output includes a key, memory content, or Record identifier.

## Status and doctor

```bash
brain status --json
brain doctor --json --query "a known customer fact"
brain receipt
```

Use a query whose answer the customer is authorized to inspect. The doctor stores only counts and
check states; it never puts returned content into the receipt.

## Revocation and deletion

`brain disconnect` removes only Brain-owned client configuration. It does not pretend to revoke
the server credential. Ask the Brain operator to revoke the key by its safe last four characters;
revocation takes effect on the next request. Rotate with a new key rather than restoring a revoked
one.

Record or Brain deletion is a separate custody operation in the HTTP API. It is intentionally not
an MCP tool and should never be part of an unattended agent installation.

## Support bundles

```bash
brain support-bundle --output brain-support.json --json
```

Review the JSON before sharing it. It contains CLI version, safe endpoint metadata, receipt state,
checks, and failure codes—not credentials, customer content, citation IDs, or query text.

## Limitations

- The design-partner path is scoped bearer authentication, not OAuth discovery.
- Key issuance and server revocation are operator actions today.
- The CLI is downloaded from the service; it is not published to npm.
- The public sandbox is ephemeral `InMemoryStore`; customer durability is proven only on the
  authenticated `SpineStore` service.
- Context conflicts and Object arbitration remain named product gaps; see
  [Known limitations](limits.html).

---

# Status checks

A green process is not a green Brain. Check the customer path in four small layers.

## Customer view

Open the [Project Console](dashboard.html). Enter the API base, Brain ID, and project key.

The console is read-only. It first proves the key can read that Brain, then settles identity,
Records and sources, derived Memories, and recent recall independently. A slow corpus census cannot
hold the other reads hostage. Nothing is stored in a cookie, URL, `localStorage`, or
`sessionStorage`.

**Green means:** the Brain exists under that project credential and each existing read operation
returns its own state.

**It does not mean:** a new write becomes retrievable, idempotency holds, citations resolve, or
data survives a restart.

## Integration path

Run the doctor against the same URL, key, and Brain your application uses:

```bash
.venv/bin/python scripts/brain_doctor.py \
  --base "$BRAIN_API_BASE" \
  --key "$BRAIN_API_KEY" \
  --brain "$BRAIN_ID"
```

The doctor walks the customer path rather than pinging a health route. It writes labelled,
idempotent probes, checks read-your-writes, asks for cited Context, follows evidence, and verifies
the stable error shape.

**Green means:** the integration path it just exercised worked end to end.

**Important:** the doctor writes small probe Records. Use the read-only Project Console when writes
are not appropriate.

## Deployment identity

Confirm the serving revision, traffic, image, and store mode:

```bash
gcloud run services describe "$SERVICE" \
  --project "$GCP_PROJECT" \
  --region "$GCP_REGION" \
  --format='yaml(status.url,status.latestReadyRevisionName,status.traffic,spec.template.spec.containers[0].image,spec.template.spec.containers[0].env)'
```

For the public sandbox, `BRAIN_STORE` must be `inmemory`. A real customer spine must be private,
allowlisted, durable, restart-tested, and isolated before onboarding. Never infer the store from a
successful HTTP response.

## Release gates

Run the smallest deterministic release board:

```bash
.venv/bin/python scripts/check_api_simplicity.py
.venv/bin/python scripts/check_error_docs.py
.venv/bin/python scripts/check_quickstart.py
.venv/bin/python scripts/build_docs_site.py
git diff --exit-code -- docs/site
```

For retrieval or answer changes, also run the binding evaluation gate described in
`docs/EVAL_GATING_POLICY.md`. For a customer deployment, finish with `brain_doctor.py` against the
actual customer URL and credential.

**Green means:** the contract stayed small, raised errors stayed documented, every documented curl
returned its claimed status, and committed site bytes match the deterministic builder.

**Yellow still matters:** contract-fuzzer warnings, database-dependent skips, public
`InMemoryStore` behavior, and any current limitation remain yellow even when these commands exit
zero.

---

# Known limitations

These are current product boundaries, not footnotes.

## Context conflicts are incomplete

`include: ["memories"]` materializes cited, temporally eligible Memories whose complete lineage is
contained in the selected evidence window. `include: ["conflicts"]` still returns an empty array. The current Memory
atom allows one active value per single-valued slot and has no unresolved-dispute state, so Brain
will not mislabel ordinary historical changes as conflicts. Use `listMemories` for browsing and do
not present Context conflicts as complete.

## Objects do not arbitrate Context

Accepted Objects are stored and readable, but they do not yet resolve competing Memory claims
inside `createContext`.

## Ownership arbitration is partial

Ownership claims are returned, but ownership is not enforced inside every retrieval window.
The private spine does enforce operation scopes and forced row-level tenant isolation; those
controls do not make an unresolved ownership claim safe to promote to self.

## Trace durability depends on the engine

The public `InMemoryStore` sandbox keeps bounded process-local traces. Private `SpineStore`
deployments persist bounded trace receipts in Postgres and tombstone deleted Record pointers.

## Record deletion follows source custody

`forgetRecord` permanently erases API-origin Records and quarantines partially supported derived
rows. Externally reconciled Records return `403 record_not_erasable`: delete at their origin and
let Brain reconcile, or forget the whole Brain. `forgetBrain` performs the terminal tenant
cascade and leaves one content-free durable receipt.

## Sandbox and spine are different

The public Cloud Run sandbox uses `InMemoryStore`. It proves request/response behavior and the
developer experience, never durable or real-corpus memory.

## Latency is a live trade

Warm and cold latency depend on the selected retrieval/rewrite path and the backing corpus. Treat
all published numbers as dated, dataset-tagged measurements rather than an SLA.

## Brain Mac is a bounded customer slice

Brain Mac's read-only four-operation migration is ready. The visible Ask/Search experience and
write paths have not moved onto the Memory API yet.

---

# Changelog

## 2026-07-31

- Added authenticated Streamable HTTP MCP at `/mcp` with four tools: `brain_status`, `context`,
  `get_record`, and opt-in `remember`.
- Added the checksum-verified Brain Integrator CLI, one canonical integration Skill, generated
  Codex/Claude Code/Cursor wrappers, resumable receipts, deterministic doctor, and redacted
  support bundles. The CLI is served directly and is not published to npm.
- Added exact client configuration and setup-brief generation to the project console without
  storing a credential or pretending browser choices are server policy.
- Added an optional Brain Mac cited-Context canary behind the existing rollback flag and resolved
  its first citation without changing visible Ask or Search behavior.
- Replaced category claims with direct product language: keep the history behind a product, return
  the records that apply, and show their sources and gaps.
- Materialized requested Context Memories from exact selected-evidence lineage on the private
  spine. Conflict population and accepted-Object arbitration remain open.
- Replaced format labels and implementation-status copy with direct product language.
- Updated the Brain Mac page to the shipped `getBrain`, `getProfile`, `listMemories`, and
  `getMemory` read slice.
- Updated the product pages to reflect the live customer SpineStore and current remaining gaps.
- Added one-pass explanatory motion to the source map and temporal handoff, plus a read-only Brain
  Mac boundary diagram. All three remain static when reduced motion is requested.
- Collapsed the five product-page Markdown actions into one native disclosure without removing any
  copy, view, or download path.

## 2026-07-30

- Added Copy Markdown, View Markdown, Download `.md`, Copy full guide, and Download full guide to
  every public page.
- Made `llms-full.txt` and `full-guide.md` the same complete 18-page guide, including the product,
  tools, status, limits, errors, and changelog.
- Added a copy-paste agent context install, explicit safe write sequence, and direct links to the
  public OpenAPI contract.
- Reframed Brain Mac as one reference integration and pressure-tested the unchanged five-resource
  contract across operations, health, finance, GTM, publishing, and conversational agents.
- Corrected the public current-limit story and fixed the Quickstart proof panel so long code cannot
  force horizontal overflow on mobile.
- Content-versioned the CSS and JavaScript URLs so an already-open browser cannot reuse a previous
  release's cached interface.

## 2026-07-29

- Published the Memory API contract, generated SDKs, same-origin guide, playground, and dashboard.
- Corrected the `fast` cost receipt to expose model, embedding, and generative calls separately.
- Added `not_yet_derived` to the published contract and normalized timestamps and identifiers.
- Added conventional agent discovery: `llms.txt`, `llms-full.txt`, `agent.md`, public OpenAPI, page
  Markdown, and a bounded integration skill.
- Rebuilt the public experience around the five-resource ontology and current limitations.
- Added OpenAPI-derived deep links for all 15 operations, exact heading/field search, one accessible
  copy action on every code block, and an all-operation same-origin console that never stores keys.
