openapi: 3.1.0

# The Brain Memory API — the one contract. Cloud and Local serve this file byte-identically;
# any divergence is a bug, not a tier (GOD PROMPT v4 §3.4).
#
# The simplicity law is MECHANICAL, not aspirational. `scripts/check_api_simplicity.py` parses
# this file and fails the build on: >5 resources, >6 verbs, >7 read knobs, >3 write knobs,
# >4 lifecycle states, or any customer-specific vocabulary in the core data plane.
# The x-brain-* extensions below are what that checker reads. Do not remove them.

info:
  title: Brain Memory API
  version: 0.1.0
  summary: Sourced history for changing facts — cited or honestly absent.
  description: |
    Two calls are the whole product:

        const brain = await client.brains.create({ id, self });
        await brain.remember(anything);
        const ctx = await brain.context(question);

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

        Record  = what entered the system
        Memory  = what the system inferred
        Object  = what the application accepted
        Context = what is eligible for this task

    Every returned assertion carries provenance to a source Record. A miss means no eligible
    result was found — never that the fact is false. Unknowns are explicit. Conflicts are
    preserved. Attribution is enforced by default: nothing is returned as the subject's own
    unless ownership is established.

servers:
  - url: https://api.brain.new/v1
    description: Cloud
  - url: http://localhost:8000/v1
    description: >-
      Brain Local — identical contract, local engine. Port 8000 is `memory_api.app`; 8787 is
      `brain.api`, a DIFFERENT application that 404s on every route here. This said 8787 until
      2026-07-29, so a generated client built from the contract pointed at the wrong service.

security:
  - apiKey: []

tags:
  - name: brains
  - name: records
  - name: memories
  - name: objects
  - name: context

paths:
  /brains:
    post:
      tags: [brains]
      operationId: createBrain
      summary: Create a Brain
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BrainCreate' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Brain' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [brains]
      operationId: getBrain
      summary: Inspect a Brain
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Brain' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [brains]
      operationId: forgetBrain
      summary: Delete a Brain and everything derived from it
      responses:
        '200':
          description: Deleted, with a receipt
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletionReceipt' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/profile:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [brains]
      operationId: getProfile
      summary: The always-fresh self card — who this Brain is, every line cited
      description: |
        A VIEW over Memory, not a sixth resource: the card an assistant wants on every turn, in
        one call. The Brain's `self`, plus its best-evidenced live Memories about that self —
        ordered by how well supported they are, not by recency.

        Every line carries at least one citation to a source Record, so nothing here is an
        assertion you cannot check. Only Memories whose ownership resolved to `self` are eligible:
        a Brain that never declared `self`, or whose evidence never resolved to it, returns an
        EMPTY card rather than a plausible one. That is the same abstain rule Context applies to
        possessive questions, and it is why this card can be pasted into a prompt unread.

        Computed at read time from current Memory. There is no cached document to invalidate, so
        a Record written a second ago is already reflected the moment its Memories exist.
      parameters: [{ $ref: '#/components/parameters/Limit' }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Profile' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/census:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [records]
      operationId: getCensus
      summary: What is noisy in THIS Brain's own data
      description: |
        A VIEW over Record, not a sixth resource: how many Records this Brain holds, broken down
        by source and by kind, how many are duplicates, and which deterministic noise classes are
        present — each class shipping the predicate that found it.

        Why it exists. The defaults that decide what is worth remembering were tuned on one clean
        corpus, measured at 1.0% pollutant Records. A different application's data will be
        dirtier, and a rate measured on somebody else's corpus is not a fact about yours. So the
        instrument is per-tenant: pollutants are CLASSIFIED here, never silently dropped, and
        every Record stays in custody whatever this reports.

        Deterministic and read-only: zero generative calls, no sampling of the counts, re-runnable
        any time. Classes may overlap, so their shares do not sum to the pollutant rate.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Census' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/traces:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [context]
      operationId: listTraces
      summary: Every Context request this Brain served, newest first
      description: |
        A VIEW over Context, not a sixth resource: what each request actually selected, and why.
        A trace's `id` IS the `id` of the Context it records, so the value your application
        already logged beside a model's output is the handle that explains it.

        Why it exists. The failure this category produces in production is an assistant asserting
        a prior agreement, decision, or promise that nobody can find. Answering "did memory hand
        it that, or did the model invent it?" needs the evidence the retrieval SELECTED, and
        without a trace the only surviving artifact is the output under suspicion. With one, the
        question is a lookup: `cited_records` is what the model was given.

        Traces are RECENT and BOUNDED — a debugging surface, not an audit log. Old ones age out;
        what the system durably knows is Record and Memory. Nothing about eligibility, ranking, or
        content changes because a request was traced.

        Content is deliberately absent. A trace holds counts, ids, lane names, scores and
        timings — never evidence text, citation quotes, or Memory statements — so a Record written
        `local_only` cannot surface here. The one exception is the caller's own `query`, truncated.
      parameters: [{ $ref: '#/components/parameters/Limit' }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TracePage' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/traces/{trace_id}:
    parameters:
      - { $ref: '#/components/parameters/BrainId' }
      - name: trace_id
        in: path
        required: true
        description: The `id` of the Context being explained, or the `trc_…` id of a failed request.
        # Two prefixes because the resource genuinely has two: a trace's id IS its Context's id
        # (`ctx_…`) when one was produced, and `trc_…` when the request failed before there was a
        # Context to name. See the note on RecordId for why the body stays permissive.
        schema: { type: string, pattern: '^(ctx|trc)_[A-Za-z0-9-]+$' }
    get:
      tags: [context]
      operationId: getTrace
      summary: Inspect one Context request — its lanes, scores, citations, budget and cost
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Trace' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/records:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    post:
      tags: [records]
      operationId: rememberRecord
      summary: Remember anything
      description: |
        Idempotent on `Idempotency-Key`. Returns immediately with status `processing`;
        the Record becomes `ready` when its Memories are derived and admitted.
        Poll `getRecord` for the transition to `ready`.

        There is no webhook. One is planned, and describing it here before it exists would make
        this description the first thing about the API that is untrue.
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecordCreate' }
      responses:
        '202':
          description: Accepted and durably queued
          headers:
            Brain-Watermark: { $ref: '#/components/headers/BrainWatermark' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Record' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/records/{record_id}:
    parameters:
      - { $ref: '#/components/parameters/BrainId' }
      - { $ref: '#/components/parameters/RecordId' }
    get:
      tags: [records]
      operationId: getRecord
      summary: Inspect a Record and its processing state
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Record' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [records]
      operationId: forgetRecord
      summary: Delete a Record and cascade to everything derived from it
      description: |
        Any derived row losing ALL supporting Records is deleted. Any losing SOME is excluded
        from serving and re-derived from the survivors. The receipt reports both.
      responses:
        '200':
          description: Deleted, with a receipt
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletionReceipt' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/memories:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    get:
      tags: [memories]
      operationId: listMemories
      summary: Inspect what the system inferred
      description: |
        Every page carries the `watermark` it was read at. Send that back as `since_watermark` and
        the next call returns ONLY what changed after it — incremental sync with no extra
        bookkeeping on either side.
      parameters:
        # x-brain-view-param: a public query parameter that SHAPES A VIEW (paging, filtering,
        # delta-reading) without being one of the 7+3 knobs. Every query parameter must carry
        # x-brain-knob or x-brain-view-param — an unmarked one fails check_api_simplicity.py,
        # so read surface can no longer grow where the budget cannot see it.
        - { name: subject, in: query, x-brain-view-param: true, schema: { type: string } }
        - { name: owner, in: query, x-brain-view-param: true, schema: { type: string } }
        - { name: as_of, in: query, x-brain-view-param: true, schema: { type: string, format: date-time } }
        - name: since_watermark
          in: query
          x-brain-view-param: true
          description: |
            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
            valid time. A Memory about 2019 learned this morning IS in this morning's delta;
            `as_of` is the valid-time question and the two are independent.

            Precisely what a delta contains:
              · Memories written after the boundary.
              · A revision arrives as the SUPERSEDING Memory (a new id, `version` incremented).
                Memory is append-only, so nothing is edited in place and the row you already hold
                stays valid history — set it to `superseded` when its successor arrives.
              · A Memory whose supporting Records were deleted does NOT appear. Deletion is not
                yet in the delta; reconcile it from the deletion receipt.

            The watermark is OPAQUE and comparable only against watermarks from the SAME Brain.
            Do not parse it, order it against another Brain's, or construct one. A malformed
            watermark is rejected rather than ignored — a silently dropped boundary would return
            the whole corpus while looking like a delta.

            Mutually exclusive with `cursor`: for a delta read the watermark IS the cursor, and
            accepting both would leave the page order ambiguous.
          # The pattern is the ONLY thing a contract can promise about an opaque token: every
          # watermark starts `wm_`. What comes after is the issuing engine's business and differs
          # between them, so a well-formed token the engine cannot read is still a 400
          # (`invalid_watermark`) rather than a boundary quietly guessed at.
          schema: { type: string, minLength: 4, maxLength: 128, pattern: '^wm_[0-9A-Za-z][0-9A-Za-z._-]*$' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MemoryPage' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/memories/{memory_id}:
    parameters:
      - { $ref: '#/components/parameters/BrainId' }
      - { $ref: '#/components/parameters/MemoryId' }
    get:
      tags: [memories]
      operationId: getMemory
      summary: Inspect one Memory, its revisions, and its evidence
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Memory' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/objects:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    post:
      tags: [objects]
      operationId: acceptObject
      summary: Accept application truth
      description: |
        The ONLY place authority moves. A model may never call this. Immutable and versioned:
        a stale `if_version` fails with the current version. An Object wins over a contradicting
        Memory for its value, and the contradiction surfaces in Context `conflicts` and fires
        `object.evidence_contradicted` — it is never silently reconciled.
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ObjectAccept' }
      responses:
        '201':
          description: Accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Object' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/objects/{object_type}/{object_key}:
    parameters:
      - { $ref: '#/components/parameters/BrainId' }
      - { name: object_type, in: path, required: true, schema: { $ref: '#/components/schemas/Slug' } }
      - { name: object_key, in: path, required: true, schema: { $ref: '#/components/schemas/Slug' } }
    get:
      tags: [objects]
      operationId: getObject
      summary: Inspect the accepted Object
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Object' }
        default: { $ref: '#/components/responses/Error' }

  /brains/{brain_id}/context:
    parameters: [{ $ref: '#/components/parameters/BrainId' }]
    post:
      tags: [context]
      operationId: createContext
      summary: Get task-ready context — cited, budgeted, honest
      description: |
        The product moment. `mode=fast` (the default) makes no generative call — it spends one query
        embedding and composes nothing, which is what makes it structurally unable to fabricate.
        Read `cost.embedding_calls` and `cost.generative_calls`; they answer different questions.
        Read-your-write is default via the `Brain-Watermark` header; if the watermark is not
        yet reachable the response is 202 `processing` — never stale evidence labelled current.

        STREAMING. Send `Accept: text/event-stream` with `mode: thorough` and the response is a
        Server-Sent Events stream, so the caller can render the moment work begins instead of
        waiting for the whole assembly. Four event types, in this order:

            event: status     the request was accepted and work started — {status, query, mode,
                              watermark}. First frame, before any retrieval.
            event: evidence   one assembled piece, as it is selected — {text, citations}. Zero or
                              more. This is the stream's whole point: evidence is usable early.
            event: context    the COMPLETE Context object, byte-identical to what the unstreamed
                              call would have returned. Terminal, and the only frame to trust for
                              budget, unknowns, conflicts and cost.
            event: error      something failed after the stream opened. Carries the same Error
                              body as any other failure. Terminal.

        The `error` frame exists because HTTP status is chosen before the first byte: once a
        stream is open a failure cannot become a 500, so it arrives in band rather than as a
        truncated body that looks like a short answer. `Brain-Watermark` is likewise unavailable
        as a trailer, so the boundary is carried in the `status` frame.

        `mode=fast` does NOT stream, and asking it to is refused rather than silently downgraded:
        there is no generative call to wait on, so 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.
        (This said "a single sub-2s response" until 2026-07-29. Measured over 258 warm requests on
        a ~41k-Record corpus: p50 1433 ms, p95 2085 ms, p99 11116 ms — 92.6 % under two seconds. A
        promise that is wrong for one request in thirteen is not a promise, so the distribution is
        published instead. Cold start is separate and much larger; see `docs/initiatives/memory-api`.)
      parameters: [{ $ref: '#/components/parameters/BrainWatermarkReq' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContextRequest' }
      responses:
        '200':
          description: 'OK — a single Context, or an SSE stream when `Accept: text/event-stream` met `mode: thorough`'
          headers:
            Brain-Watermark: { $ref: '#/components/headers/BrainWatermark' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Context' }
            text/event-stream:
              schema: { $ref: '#/components/schemas/ContextEvent' }
        '202':
          description: Not yet assemblable at the requested watermark, or `thorough` exceeded the synchronous budget
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Context' }
        default: { $ref: '#/components/responses/Error' }

components:

  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: '`sk_live_…` or `sk_test_…`. Hashed at rest; test mode never touches live data.'

  headers:
    BrainWatermark:
      description: Reproducible evidence boundary. Send it back to guarantee read-your-write.
      schema: { type: string, examples: ['wm_01K7Q...'] }

  parameters:
    BrainId:
      name: brain_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/Slug' }
    # THE PREFIX IS THE CONTRACT; THE BODY DELIBERATELY IS NOT. These declared bare `type: string`,
    # which made the spec LOOSER than the engine — the fuzzer generated arbitrary strings, the API
    # rejected them with a typed 400, and conformance correctly reported "schema constraints don't
    # match API validation" on both fetch-by-id operations.
    #
    # The body stays permissive on purpose, because the two engines do not agree on it:
    #     memory_api/ids.py  ->  rec_01K7Q…                       ULID, Crockford base32, no hyphens
    #     the Postgres spine ->  rec_e7a89fe1-c4af-5077-a268-…     UUID, lowercase hex, hyphenated
    # The in-memory fixture behind the public sandbox emits the first; the real engine emits the
    # second. A strict ULID pattern would validate every sandbox id and reject every production
    # one — a client parsing ids would pass its integration tests and fail on the product. So the
    # pattern pins what callers may rely on (an opaque, prefixed, URL-safe token) and pins nothing
    # about ULID-versus-UUID, which is an internal choice that should stay changeable.
    RecordId:
      name: record_id
      in: path
      required: true
      schema: { type: string, pattern: '^rec_[A-Za-z0-9-]+$' }
    MemoryId:
      name: memory_id
      in: path
      required: true
      schema: { type: string, pattern: '^mem_[A-Za-z0-9-]+$' }
    Cursor:
      name: cursor
      in: query
      x-brain-view-param: true
      schema: { type: string }
    Limit:
      name: limit
      in: query
      x-brain-view-param: true
      # `minimum: 1` is not decoration: a page of zero rows is not a request anyone means, and
      # without the floor a negative limit reaches the database as a negative LIMIT.
      schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Plumbing, not a knob. Replays return the original response body.
      schema: { type: string }
    BrainWatermarkReq:
      name: Brain-Watermark
      in: header
      description: Plumbing, not a knob. Guarantees the Context is at or after this boundary.
      schema: { type: string }

  responses:
    Error:
      description: Typed, stable, documented
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    # ─── THE FIVE RESOURCES ────────────────────────────────────────────────────
    # x-brain-resource marks a top-level product noun. There are five. Forever.

    Brain:
      x-brain-resource: true
      type: object
      description: One durable, isolated memory boundary.
      required: [id, object, self, created_at]
      properties:
        id: { type: string }
        object: { type: string, const: brain }
        name: { type: string }
        self:
          $ref: '#/components/schemas/SelfEntity'
        created_at: { type: string, format: date-time }

    Record:
      x-brain-resource: true
      type: object
      description: Immutable raw input. What entered the system.
      required: [id, object, status, created_at]
      properties:
        id: { type: string, examples: ['rec_01K7Q...'] }
        object: { type: string, const: record }
        status: { $ref: '#/components/schemas/Lifecycle' }
        occurred_at:
          type: string
          format: date-time
          description: When the thing HAPPENED, not when it was ingested. Load-bearing for `as_of`.
        source: { $ref: '#/components/schemas/Source' }
        visibility: { $ref: '#/components/schemas/Visibility' }
        memory_ids: { type: array, items: { type: string } }
        failure: { $ref: '#/components/schemas/Error' }
        created_at: { type: string, format: date-time }

    Memory:
      x-brain-resource: true
      type: object
      description: An atomic, temporal assertion derived from Records. What the system inferred.
      required: [id, object, statement, subject, owner, valid_from, citations, version]
      properties:
        id: { type: string, examples: ['mem_01K7Q...'] }
        object: { type: string, const: memory }
        statement: { type: string }
        subject: { type: string, description: Who or what the claim is ABOUT. }
        owner:
          $ref: '#/components/schemas/Ownership'
        valid_from: { type: string, format: date-time }
        valid_to:
          type: [string, 'null']
          format: date-time
          description: Null means current. A superseded Memory keeps its history; nothing mutates.
        version: { type: integer }
        status: { type: string, enum: [active, superseded, expired] }
        citations:
          type: array
          minItems: 1
          description: Never empty. A Memory that cannot name its evidence is not written.
          items: { $ref: '#/components/schemas/Citation' }

    Object:
      x-brain-resource: true
      type: object
      description: Immutable, versioned, developer-governed state. What the application accepted.
      required: [id, object, type, key, value, version, accepted_by, created_at]
      properties:
        id: { type: string, examples: ['obj_01K7Q...'] }
        object: { type: string, const: object }
        type: { type: string }
        key: { type: string }
        value: {}
        version: { type: integer }
        accepted_by: { type: string, description: A credential id. Never a model. }
        evidence_state:
          type: string
          enum: [supported, contradicted, revoked]
          description: |
            `contradicted` when a Memory disagrees; `revoked` when every supporting Record is gone.
            Neither auto-deletes the Object — only the application un-accepts what it accepted.
        created_at: { type: string, format: date-time }

    Context:
      x-brain-resource: true
      type: object
      description: Task-ready, budgeted retrieval with lineage and unknowns. What is eligible now.
      required: [id, object, query, watermark, status, citations, unknowns, budget]
      properties:
        id: { type: string, examples: ['ctx_01K7Q...'] }
        object: { type: string, const: context }
        status: { type: string, enum: [ready, processing] }
        query: { type: string }
        as_of: { type: string, format: date-time }
        watermark: { type: string }
        text: { type: [string, 'null'] }
        value:
          description: Present only when a `schema` was requested AND validated. Never malformed best-effort.
        objects: { type: array, items: { $ref: '#/components/schemas/Object' } }
        memories: { type: array, items: { $ref: '#/components/schemas/Memory' } }
        citations: { type: array, items: { $ref: '#/components/schemas/Citation' } }
        conflicts: { type: array, items: { $ref: '#/components/schemas/Conflict' } }
        unknowns:
          type: array
          items: { type: string }
          description: Explicit. Never coerced to false or zero. A miss is not a denial.
        abstained:
          # WHY THERE IS NO ANSWER, typed. "We found nothing", "we found things that disagree" and
          # "we cannot tell whose this is" are different facts and a caller acts on them
          # differently; a bare empty `unknowns` collapses all three. The vocabulary is the one a
          # shipped client already renders, so a caller can branch on it rather than parse prose.
          # This says what the engine does not know — never what the caller should conclude.
          # A judge verdict is the answer layer's job and does not belong in a memory contract.
          type: [object, 'null']
          properties:
            reason:
              type: string
              enum: [no_evidence, conflicting_evidence, expired_evidence, no_attribution,
                     not_yet_derived, out_of_policy]
        budget: { $ref: '#/components/schemas/BudgetReport' }
        cost: { $ref: '#/components/schemas/CostReport' }
        explain: { type: [object, 'null'], description: Bounded selection reasons. Never model chain of thought. }

    # ─── VIEWS: responses that are NOT resources ───────────────────────────────
    # A view is a projection of a resource that already exists, so it carries no
    # x-brain-resource, adds no knob, and reuses an existing verb. Five nouns is the budget; a
    # convenience endpoint that mints a sixth noun is how every API in this category got big.
    # If a view ever needs its own identity, lifecycle, or write path, it was a resource all
    # along and something has to be deleted to make room for it.

    Profile:
      type: object
      description: |
        The self card — a view over Memory. `memories` are the Brain's best-evidenced live claims
        about its own `self`, each one cited. Empty when ownership never resolved to `self`,
        because a card assembled from unattributed evidence is exactly the defect the ownership
        axis exists to prevent.
      required: [object, brain_id, self, memories, has_more, generated_at]
      properties:
        object: { type: string, const: profile }
        brain_id: { type: string }
        self:
          $ref: '#/components/schemas/SelfEntity'
        memories:
          type: array
          description: Ordered by strength of evidence, not recency. Every entry carries citations.
          items: { $ref: '#/components/schemas/Memory' }
        has_more:
          type: boolean
          description: More eligible Memories exist than `limit` returned. Raise `limit`, or read `/memories`.
        generated_at: { type: string, format: date-time }

    Census:
      type: object
      description: |
        A view over Record: this Brain's own noise, measured deterministically. Counts are exact;
        class membership is measured over `scanned` Records, newest first, when a Brain holds more
        than one pass can classify.
      required: [object, brain_id, records, scanned, duplicate_rate, pollutant_rate,
                 by_source, by_kind, classes, generated_at]
      properties:
        object: { type: string, const: census }
        brain_id: { type: string }
        records: { type: integer, description: Records held. Exact. }
        scanned:
          type: integer
          description: |
            Records classified for the `classes` breakdown, newest first. Equal to `records`
            unless the Brain is larger than one classification pass, and reported rather than
            assumed so a share is never read against the wrong denominator.
        duplicate_rate:
          type: number
          description: Share of Records whose content is byte-identical to an earlier Record from the same source.
        pollutant_rate:
          type: number
          description: Share of `scanned` Records matching at least one class. Classes overlap, so this is not their sum.
        by_source: { type: array, items: { $ref: '#/components/schemas/CensusCount' } }
        by_kind: { type: array, items: { $ref: '#/components/schemas/CensusCount' } }
        classes: { type: array, items: { $ref: '#/components/schemas/CensusClass' } }
        generated_at: { type: string, format: date-time }

    CensusCount:
      type: object
      required: [name, records]
      properties:
        name: { type: string }
        records: { type: integer }
        duplicate_rate: { type: [number, 'null'] }

    CensusClass:
      type: object
      description: One noise class, with the rule that found it. A number without its predicate is not a measurement.
      required: [name, records, share, predicate]
      properties:
        name: { type: string }
        records: { type: integer }
        share: { type: number, description: Of `scanned`, not of `records`. }
        predicate: { type: string, description: The exact deterministic rule. Re-runnable by hand. }

    Trace:
      type: object
      description: |
        What one Context request decided — a view over a Context that already happened, keyed by
        that Context's own `id`. Bounded and non-durable: read it to debug the request you just
        made, not to reconstruct history.
      required: [object, id, brain_id, created_at, query, mode, status, latency_ms,
                 lanes, citations, cited_records, budget]
      properties:
        object: { type: string, const: trace }
        id:
          type: string
          description: |
            The `id` of the Context this explains — the same value the caller received. A request
            that failed before a Context existed gets its own `trc_…` id instead.
          examples: ['ctx_01K7Q...']
        brain_id: { type: string }
        created_at: { type: string, format: date-time }
        query:
          type: string
          description: The caller's own question, truncated. The only content a trace keeps.
        mode: { type: string, enum: [fast, thorough] }
        status: { type: string, enum: [ready, processing, failed] }
        latency_ms:
          type: integer
          description: Wall time for the whole request, measured to the last byte on a stream.
        streamed: { type: boolean }
        lanes:
          type: object
          additionalProperties: { type: integer }
          description: |
            Which retrieval strategies produced the selected evidence, and how many pieces each
            contributed. All lanes run concurrently on every request and are fused; this is an
            observation of what happened, never a setting. Empty when nothing was selected.
        fusion: { type: [string, 'null'], description: How the lanes were combined. }
        selected:
          type: array
          description: The fused ranking, bounded. Lane attribution and score per piece — no text.
          items:
            type: object
            properties:
              rank: { type: integer }
              lanes: { type: array, items: { type: string } }
              score: { type: number }
        citations: { type: integer, description: How many citations the Context carried. }
        cited_records:
          type: array
          description: |
            Distinct source Records the evidence came from, bounded. THE field for the failure
            this view exists to settle: fetch them with `getRecord` and read what the model was
            actually given, rather than arguing about what it said.
          items: { type: string }
        memories: { type: integer }
        conflicts: { type: integer }
        unknowns: { type: integer }
        abstained:
          type: [object, 'null']
          description: Present when the request declined to answer, with the reason it declined.
          properties:
            reason: { type: string }
        budget: { $ref: '#/components/schemas/BudgetReport' }
        cost: { $ref: '#/components/schemas/CostReport' }
        error:
          type: object
          description: |
            Present only on a failed request. The taxonomy, not the message: `type` and `code` are
            stable and documented, a message is free text an engine may interpolate values into.
          properties:
            type: { type: string }
            code: { type: string }

    TracePage:
      type: object
      required: [object, data, has_more]
      properties:
        object: { type: string, const: list }
        data:
          type: array
          description: Newest first — the request being debugged is almost always the last one.
          items: { $ref: '#/components/schemas/Trace' }
        has_more: { type: boolean }

    ContextEvent:
      type: object
      description: |
        ONE frame of the `text/event-stream` body returned when `Accept: text/event-stream` meets
        `mode: thorough`. The body is a sequence of these in SSE framing —
        `event: <name>\ndata: <json>\n\n` — where `<json>` is this object's `data`. Read frames
        until `context` or `error`; both are terminal.
      required: [event, data]
      properties:
        event: { type: string, enum: [status, evidence, context, error] }
        data:
          description: |
            `status` → {status, query, mode, watermark} · `evidence` → {text, citations} ·
            `context` → a complete Context · `error` → a complete Error.

    # ─── REQUESTS: where the knobs live ────────────────────────────────────────

    BrainCreate:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: { $ref: '#/components/schemas/Slug' }
        name: { type: string }
        self:
          $ref: '#/components/schemas/SelfEntity'

    RecordCreate:
      type: object
      additionalProperties: false
      required: [content]
      properties:
        content:
          description: |
            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 recoverable once it has been flattened into a sentence.

            One kind moves memory BACKWARD instead of forward: `correction` says "you told me X,
            it was wrong". It supersedes the Memory it names — bitemporally, so `as_of` before
            the correction still returns what was believed then — and with a `statement` mints
            the replacement, citing this Record. Without one, the claim is retracted.

            This is not a sixth resource. `Record` is still one noun with one write endpoint;
            `kind` selects a SHAPE, never a lifecycle, an authority, or a knob.
          # `oneOf` rather than a discriminator object: one branch is a bare string, and a
          # discriminator may only select between object schemas. The `kind` const on each object
          # branch already makes the choice unambiguous, which is all a generator needs.
          oneOf:
            - type: string
              minLength: 1
              description: The whole content as text. Empty is not something to remember.
            - $ref: '#/components/schemas/MessageContent'
            - $ref: '#/components/schemas/DocumentContent'
            - $ref: '#/components/schemas/EventContent'
            - $ref: '#/components/schemas/FactContent'
            - $ref: '#/components/schemas/CorrectionContent'
        source:
          x-brain-knob: write
          allOf: [{ $ref: '#/components/schemas/Source' }]
        visibility:
          x-brain-knob: write
          allOf: [{ $ref: '#/components/schemas/Visibility' }]
        occurred_at:
          x-brain-knob: write
          type: string
          format: date-time
          description: When it happened. Defaults to now; wrong for anything historical.

    ContextRequest:
      type: object
      additionalProperties: false
      required: [query]
      properties:
        query: { type: string, minLength: 1 }
        purpose:
          x-brain-knob: read
          type: string
          description: One word sets policy, sensitivity, and eligibility for a class of task.
        as_of:
          x-brain-knob: read
          type: string
          format: date-time
          description: >-
            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:
          x-brain-knob: read
          type: object
          additionalProperties: false
          properties:
            # `minimum: 1` for the reason `limit` carries one: a ceiling of zero is not a request
            # anyone means, and without the floor a negative budget returns an empty Context that
            # reports itself `ready` rather than failing.
            tokens: { type: integer, minimum: 1 }
            ms: { type: integer, minimum: 1 }
        include:
          x-brain-knob: read
          type: array
          description: >-
            Select response components. `memories` are cited, temporally eligible Memory atoms
            whose complete Record lineage is contained in the selected evidence window.
          items: { type: string, enum: [objects, memories, citations, conflicts, unknowns, excerpts, explain] }
        schema:
          x-brain-knob: read
          type: object
          description: Request structured context. Invalid means unavailable, never malformed.
        filter:
          # ENFORCED vs RECORDED, stated because a silently ignored knob is the worst kind. A knob
          # may change what is RETURNED, never what is TRUE — and a caller who narrows their window
          # and silently gets the wide one acts on evidence they believe they excluded.
          #
          #   ENFORCED on Context:  since · until · sources — properties of the evidence's own
          #                         Records (`events.occurred_at`, `events.source`), which is the
          #                         layer a candidate traces back to.
          #   RECORDED, not enforced: subjects · owner · sensitivity_lte. The first two are
          #                         properties of a MEMORY; a candidate is a CHUNK, and one built
          #                         from eight Records has no single subject or owner —
          #                         `listMemories` enforces both today because that is where they
          #                         exist. `sensitivity_lte` has no column yet. They land on the
          #                         Context receipt so their absence is visible rather than silent.
          #
          # `as_of` is split one field over: enforced on Context's returned Memory components,
          # where the data is bitemporal; recorded-not-enforced on the selected chunk text, because
          # a chunk's valid_time is a derive stamp and filtering on it would look like a
          # bitemporal read while actually using a build timestamp.
          description: >-
            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.
          x-brain-knob: read
          type: object
          additionalProperties: false
          properties:
            subjects: { type: array, items: { type: string } }
            owner: { type: string }
            sources: { type: array, items: { type: string } }
            since: { type: string, format: date-time }
            until: { type: string, format: date-time }
            sensitivity_lte:
              # Inline rather than `oneOf: [Sensitivity, null]`: semantically identical OpenAPI
              # 3.1, but representable by Swift OpenAPI Generator. The former was silently
              # dropped from ContextRequest.filterPayload.
              type: [string, 'null']
              enum: [public, standard, sensitive, restricted, null]
        mode:
          x-brain-knob: read
          type: string
          enum: [fast, thorough]
          default: fast
          description: >-
            `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_calls: 1`. Not universal: a `202 processing` spends nothing, and the
            in-memory fixture behind the public sandbox calls no model at all. `thorough` may rewrite the
            query, multi-hop, and return 202.
            HISTORY, because this field has now been wrong twice in opposite directions. It said
            `zero` while the code returned a hardcoded 0 and Gemini was called on every request — a
            receipt that cannot be wrong is not a receipt. Counting honestly showed 2: the embedding
            plus a generative query rewrite. Two pre-registered 516-request ablations then asked
            whether the rewrite earned its place and found it did not — paired per-case over 43
            human-confirmed questions in two phrasings, clean arm 1-1 (p=0.75), perturbed arm 3-2
            (p=0.5), with both runs naming the same discordant cases — while costing 4074 ms against
            1439 ms. So the rewrite moved to `thorough` and `fast` is one call.

    ObjectAccept:
      type: object
      additionalProperties: false
      required: [type, key, value]
      properties:
        type: { $ref: '#/components/schemas/Slug' }
        key: { $ref: '#/components/schemas/Slug' }
        value: {}
        if_version:
          type: [integer, 'null']
          description: Optimistic concurrency. Stale writes fail with the current version.

    # ─── SUPPORTING SHAPES (not resources, not knobs) ──────────────────────────

    # The five typed Record contents. Each is CLOSED and tagged, so `RecordCreate.content`
    # resolves to exactly one branch and a caller learns about a misspelled field from a 400
    # rather than from a Memory that quietly lost it.
    MessageContent:
      type: object
      description: |
        Something somebody said. `thread` is what a session IS here — messages that share one
        become an episode and their speakers become people, so a conversation is a thing that
        turns out to exist rather than a resource you had to open in advance.
      additionalProperties: false
      required: [kind, text]
      properties:
        kind: { type: string, const: message }
        text: { type: string, minLength: 1 }
        speaker: { type: string, description: Who said it. Resolved to a person. }
        thread: { type: string, description: The conversation. Groups into an episode. }

    DocumentContent:
      type: object
      description: A note, page, or file's text. Split and embedded; the whole thing stays one Record.
      additionalProperties: false
      required: [kind, text]
      properties:
        kind: { type: string, const: document }
        text: { type: string, minLength: 1 }
        title: { type: string }

    EventContent:
      type: object
      description: |
        Something that happened at a time. An appointment is not a message and not a document, and
        flattening it to prose throws away the `when` that makes it answerable.
      additionalProperties: false
      required: [kind, title]
      properties:
        kind: { type: string, const: event }
        title: { type: string, minLength: 1 }
        starts_at: { type: string, format: date-time }
        ends_at: { type: string, format: date-time }
        participants: { type: array, items: { type: string } }
        place: { type: string }

    FactContent:
      type: object
      description: |
        A claim the application already holds as structure — subject, predicate, value. The escape
        hatch for data that is ALREADY a fact, so nobody has to render a row into a sentence just
        to have it extracted back out again.
      additionalProperties: false
      required: [kind, subject, predicate, value]
      properties:
        kind: { type: string, const: fact }
        subject: { type: string, minLength: 1 }
        predicate: { type: string, minLength: 1 }
        value: { type: string, minLength: 1 }

    CorrectionContent:
      type: object
      description: |
        "You told me X, and it was wrong." The correction loop, as a Record kind — because what
        entered the system is the correction itself, and it belongs in custody like anything
        else. The Memory it names is superseded: its `status` becomes `superseded` and its
        `valid_to` closes at the moment the correction was made (`occurred_at`, defaulting to
        now). Bitemporal honesty is the contract here: `as_of` BEFORE the correction still
        returns the old Memory. History is never rewritten, only ended.

        With `statement`, a successor Memory is minted — owner resolved to `self`, at the
        highest confidence tier, citing this Record as its evidence. Without it, the claim is
        purely retracted: nothing is minted, and Context abstains where it once answered.
        Naming a Memory this Brain does not hold fails with `corrected_memory_not_found`.
      additionalProperties: false
      required: [kind, corrects]
      properties:
        kind: { type: string, const: correction }
        corrects:
          type: string
          minLength: 1
          description: The `mem_…` id of the Memory being corrected.
        statement:
          type: string
          minLength: 1
          description: The replacement claim. Absent means pure retraction — empty is not a retraction, it is a 400.
        because:
          type: string
          minLength: 1
          description: Why the correction was made. Kept on the Record.

    Slug:
      type: string
      minLength: 1
      maxLength: 64
      pattern: '^[A-Za-z0-9._:-]+$'
      description: A non-empty, URL-safe identifier. Empty is not an identifier.

    SelfEntity:
      type: object
      description: |
        The principal this Brain is a memory OF. Resolves "my" and "our". Omit it and every
        possessive question abstains — the safe default is also the honest one.
      required: [type, name]
      properties:
        type: { type: string, enum: [person, org] }
        name: { type: string }

    Ownership:
      type: object
      description: Whose claim this is. Tri-state; `unresolved` abstains rather than guesses.
      required: [state]
      properties:
        state: { type: string, enum: [self, other, unresolved] }
        entity: { type: [string, 'null'] }

    Lifecycle:
      type: string
      x-brain-lifecycle: true
      enum: [processing, ready, failed, quarantined]

    Visibility:
      type: string
      enum: [local_only, redacted_summary, shareable]
      default: shareable
      description: The privacy boundary. `local_only` never leaves the device.

    Sensitivity:
      type: string
      enum: [public, standard, sensitive, restricted]
      description: A model may propose this; a model may never downgrade a developer-set value.

    Source:
      type: object
      properties:
        name: { type: string }
        external_id: { type: string }
        trust: { type: string, enum: [first_party, third_party, untrusted], default: untrusted }

    Citation:
      type: object
      required: [record_id]
      properties:
        record_id: { type: string }
        quote: { type: string }
        locator:
          type: object
          properties:
            start: { type: integer }
            end: { type: integer }

    Conflict:
      type: object
      required: [statement, sides]
      properties:
        statement: { type: string }
        sides: { type: array, items: { $ref: '#/components/schemas/Citation' } }

    BudgetReport:
      type: object
      required: [estimated_tokens, truncated]
      properties:
        requested_tokens: { type: integer }
        estimated_tokens: { type: integer }
        truncated: { type: boolean }

    CostReport:
      type: object
      properties:
        processing_units: { type: number }
        model_calls_total:
          type: integer
          description: >-
            Every model call this request made, of any kind. `embedding_calls + generative_calls`.
        embedding_calls:
          type: integer
          description: >-
            Calls that turned text into a vector. Deterministic, and incapable of composing a
            statement. On a ready `fast` Context this is `1` — retrieval submits one query
            embedding unconditionally and the vector lane cannot exist without it.
        generative_calls:
          type: integer
          description: >-
            Calls that asked a model to PRODUCE text. **On `fast` this is `0`.** That is the claim
            worth checking, because a request that generates nothing cannot fabricate.
            THREE FIELDS BECAUSE ONE COULD NOT TELL THE TRUTH. This was a single integer, and it
            was wrong three ways in three days: hardcoded `0` while Gemini ran twice per request;
            then an honest total of `2`; then `1` after the query rewrite moved to `thorough` — at
            which point `1` under the name *generative* was still a lie, because the remaining call
            is an embedding. A single number had to read either as "it generated something" (false)
            or "it called nothing" (also false). Split, each field means exactly one thing.
            COUNTS ARE LOGICAL OPERATIONS, NOT HTTP REQUESTS. The transport retries underneath
            these, so `model_calls_total: 1` may be several requests on the wire during a provider
            incident. Right unit for cost and fabrication risk; wrong unit for debugging provider
            latency.
            NOT EVERY RESPONSE SPENDS THE SAME. A `202 processing` (watermark not reached) does no
            retrieval, so all three are `0`. The public sandbox runs the in-memory fixture, which
            calls no model at all and also reports `0` — proving the contract, never the engine.
        latency_ms: { type: integer }

    MemoryPage:
      type: object
      required: [data, has_more, watermark]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Memory' } }
        has_more: { type: boolean }
        next_cursor: { type: [string, 'null'] }
        watermark:
          type: string
          description: |
            The boundary this page was read at. Send it back as `since_watermark` for the next
            delta. Always present, so a client can start an incremental loop from any read
            without a second call. Opaque — comparable only to watermarks from this same Brain.

    DeletionReceipt:
      type: object
      required: [deleted, rederiving, hash]
      properties:
        deleted: { type: object, additionalProperties: { type: integer } }
        rederiving: { type: object, additionalProperties: { type: integer } }
        hash: { type: string }
        completed_at: { type: string, format: date-time }

    Error:
      type: object
      required: [type, code, message, request_id]
      properties:
        type: { type: string, enum: [invalid_request, authentication, permission, not_found, conflict, rate_limit, policy, internal] }
        code: { type: string }
        message: { type: string }
        request_id: { type: string, examples: ['req_01K7Q...'] }
        docs_url: { type: string, format: uri }
