openapi: 3.1.0
info:
  title: MemorylAIer API
  version: 1.2.0
  description: |
    Persistent memory infrastructure for AI agents, accessible via x402 micropayments and API keys.

    **BYOI architecture** — agents bring intelligence, we provide storage, retrieval, dedup, decay,
    and self-improvement infrastructure.

    ## Authentication

    Every endpoint (except `/health` and `/ready`) requires one of:

    1. **API key** — `Authorization: Bearer <key>` header. The key is SHA-256 hashed and matched
       against stored hashes to resolve a namespace.
    2. **x402 micropayment** — USDC on Base (EIP-155:8453). The server returns `402 Payment Required`
       with a price envelope; the client pays and resends the request with an `X-Payment` header.
       API-key holders bypass x402 entirely.

    ### x402 Pricing (USDC on Base)

    | Operation | Price |
    |-----------|-------|
    | Write (store facts, create session, flush, update memory, add lesson, create trigger, update config, PUT /relationships) | $0.001 |
    | Read (get item, get lessons, get memory, categories, stats, entities, triggers, deletes, GET /relationships/:name, GET /graph/:name, GET /items/:id/history, GET /config) | $0.0005 |
    | Search (semantic query, POST /graph/traverse, POST /extract) | $0.001 |

    ## Rate Limiting

    All endpoints are rate-limited. Limits are per client (API key hash or IP address).
    Responses include `RateLimit` headers per draft-7 of the IETF rate-limit header spec.

    | Tier | Default | Used By |
    |------|---------|---------|
    | Strict | 5 req/min | Session creation |
    | Moderate | 30 req/min | Writes, semantic search |
    | Generous | 120 req/min | Reads, deletes, all other endpoints |

    ## Namespace Resolution

    The `{ns}` path parameter represents the namespace. It is resolved automatically from the
    authenticated identity (API key or x402 wallet address). The client must still include it
    in the URL path, but cannot access other namespaces.
  contact:
    name: Matrix
  license:
    name: Proprietary

servers:
  - url: https://memorylaier.com
    description: Production

security:
  - BearerApiKey: []
  - x402Payment: []

tags:
  - name: Health
    description: Liveness and readiness probes (no auth required)
  - name: Items
    description: Store, retrieve, and soft-delete structured facts (memory items)
  - name: Query
    description: Semantic memory search with score decay
  - name: Sessions
    description: Ephemeral working-memory sessions with flush lifecycle
  - name: Lessons
    description: Behavioral lessons learned by agents (self-improvement)
  - name: Categories
    description: Auto-managed category summaries with top facts
  - name: Stats
    description: Namespace usage statistics and memory health
  - name: Entities
    description: Auto-extracted entity graph (people, projects, concepts)
  - name: Triggers
    description: Temporal triggers for scheduled memory surfacing
  - name: Config
    description: Per-namespace configuration (decay rate, etc.)
  - name: Relationships
    description: Knowledge graph relationships between entities
  - name: Graph
    description: Knowledge graph traversal
  - name: Extract
    description: Conversation extraction

paths:
  # ─── Health ───────────────────────────────────────────────────────
  /health:
    get:
      operationId: getHealth
      summary: Liveness check
      description: Returns `ok` if the database connection is healthy. No auth required.
      tags: [Health]
      security: []
      responses:
        "200":
          description: Service is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "503":
          description: Database unreachable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthErrorResponse"

  /ready:
    get:
      operationId: getReady
      summary: Readiness check
      description: |
        Returns `ready` when both the database and the embedding model (all-MiniLM-L6-v2)
        are loaded and functional. No auth required.
      tags: [Health]
      security: []
      responses:
        "200":
          description: Service is ready to accept traffic
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadyResponse"
        "503":
          description: One or more subsystems not ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadyResponse"

  # ─── Items ────────────────────────────────────────────────────────
  /v1/{ns}/items:
    put:
      operationId: putItems
      summary: Store structured facts
      description: |
        Store one or more structured facts as memory items. Each fact is embedded locally
        using all-MiniLM-L6-v2 (384 dimensions) and stored with pgvector HNSW indexing.

        A write-gate scores each item for deduplication and confidence adjustment.
        Auto-supersedes duplicate facts (>0.9 similarity) — old versions are archived automatically.
        Supports optional structured data via the `data` field on each fact.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Items]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutItemsBody"
      responses:
        "201":
          description: Facts stored successfully
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PutItemsResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/items/{id}:
    get:
      operationId: getItem
      summary: Get a specific item
      description: |
        Retrieve a single memory item by ID. Also increments the item's access count
        and updates `last_accessed_at` (used for decay scoring).

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Items]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/ItemId"
      responses:
        "200":
          description: Item found
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Item"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: deleteItem
      summary: Soft-delete an item
      description: |
        Soft-deletes a memory item by setting its status to `archived`. The item is no
        longer returned in queries but remains in the database.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Items]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/ItemId"
      responses:
        "200":
          description: Item soft-deleted
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [deleted]
                properties:
                  deleted:
                    type: boolean
                    const: true
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/items/{id}/history:
    get:
      operationId: getItemHistory
      summary: Get supersession chain
      description: |
        Follows the full supersession chain for an item using a recursive CTE,
        returning both predecessors and successors. Useful for tracking how a fact
        evolved over time.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Items]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/ItemId"
      responses:
        "200":
          description: Supersession history
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [history, count]
                properties:
                  history:
                    type: array
                    items:
                      $ref: "#/components/schemas/HistoryEntry"
                  count:
                    type: integer
                    description: Number of entries in the supersession chain
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Query ────────────────────────────────────────────────────────
  /v1/{ns}/query:
    post:
      operationId: queryMemories
      summary: Semantic memory search
      description: |
        Search memories using semantic similarity. Provide either `query_text` (which is
        embedded server-side) or a pre-computed `query_embedding` (384 dimensions).

        **Three-tier retrieval:**
        1. **Category tier** — If the query is highly similar (>=0.7 cosine) to a category
           summary embedding, returns top facts from that category ranked by confidence.
        2. **Hybrid search** — When `hybrid: true` and `query_text` is provided, combines
           vector similarity with PostgreSQL full-text search using Reciprocal Rank Fusion (RRF, k=50).
           Best for queries containing entity names, identifiers, or exact terms.
        3. **Full vector search** — HNSW vector search across all active items,
           applies time-decay scoring with access-count boosting, and re-ranks results.

        Score formula: `similarity × e^(-decay × age_days) × (1 + ln(1 + access_count))`

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (search)
      tags: [Query]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QueryBody"
      responses:
        "200":
          description: Search results
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueryResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Sessions ─────────────────────────────────────────────────────
  /v1/{ns}/sessions:
    post:
      operationId: createSession
      summary: Create a new session
      description: |
        Create a new ephemeral working-memory session. Sessions hold arbitrary JSON working
        memory that persists across turns until flushed or discarded.

        **Rate limit:** Strict (5 req/min) — to prevent session-creation abuse.
        **x402 price:** $0.001 (write)
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "201":
          description: Session created
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Session"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/sessions/{id}/memory:
    get:
      operationId: getWorkingMemory
      summary: Read working memory
      description: |
        Retrieve the current working memory JSON object for an active session.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Working memory retrieved
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [memory]
                properties:
                  memory:
                    $ref: "#/components/schemas/WorkingMemory"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

    put:
      operationId: updateWorkingMemory
      summary: Overwrite working memory
      description: |
        Replace the entire working memory object for an active session. The body must be
        a JSON object (arbitrary keys and values).

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkingMemory"
      responses:
        "200":
          description: Working memory updated
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [updated]
                properties:
                  updated:
                    type: boolean
                    const: true
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/sessions/{id}/flush:
    post:
      operationId: flushSession
      summary: Flush and close session
      description: |
        Flush the session: marks it as `flushed`, returns the final working memory snapshot,
        and closes the session. After flushing, the session cannot be modified.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session flushed
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FlushSessionResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/sessions/{id}:
    delete:
      operationId: discardSession
      summary: Discard a session
      description: |
        Discard an active session without flushing. The session is marked as `discarded`
        and its working memory is lost.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session discarded
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [discarded]
                properties:
                  discarded:
                    type: boolean
                    const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Lessons ──────────────────────────────────────────────────────
  /v1/{ns}/lessons:
    put:
      operationId: putLesson
      summary: Add a behavioral lesson
      description: |
        Store a new behavioral lesson (pattern) that the agent has learned. Lessons are
        used for self-improvement — agents can retrieve them to avoid repeating mistakes.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Lessons]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutLessonBody"
      responses:
        "201":
          description: Lesson added
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Lesson"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

    get:
      operationId: getLessons
      summary: Get all active lessons
      description: |
        Retrieve all active behavioral lessons for the namespace. Also increments
        the `access_count` for each lesson returned.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Lessons]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Active lessons
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LessonsListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/lessons/{id}:
    delete:
      operationId: deleteLesson
      summary: Remove a lesson
      description: |
        Deactivate a lesson by setting `active = false`. The lesson remains in the database
        but is no longer returned by `GET /v1/{ns}/lessons`.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Lessons]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - $ref: "#/components/parameters/LessonId"
      responses:
        "200":
          description: Lesson removed
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [removed]
                properties:
                  removed:
                    type: boolean
                    const: true
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Categories ───────────────────────────────────────────────────
  /v1/{ns}/categories:
    get:
      operationId: getCategories
      summary: List categories with top facts
      description: |
        List all categories for the namespace, each with its top 10 facts ranked by confidence.
        Categories are auto-created when items are stored with a `category` field.

        Category summary embeddings are rebuilt automatically after every 5 new items.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Categories]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Categories with top facts
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoriesListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Entities ─────────────────────────────────────────────────────
  /v1/{ns}/entities:
    get:
      operationId: listEntities
      summary: List all known entities
      description: |
        List all entities extracted from stored facts, ranked by how many facts reference them.
        Entities are auto-extracted from fact text (capitalized names, @-mentions, quoted strings).

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Entities]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Maximum entities to return
      responses:
        "200":
          description: Entity list
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/entities/{name}:
    get:
      operationId: getEntity
      summary: Get facts about an entity
      description: |
        Find all active facts mentioning a specific entity (person, project, concept).
        Returns the entity with all related memory items.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Entities]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: name
          in: path
          required: true
          description: Entity name (e.g., "John Smith", "@alice", "React")
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: Entity with related facts
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityWithFacts"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Triggers ────────────────────────────────────────────────────
  /v1/{ns}/triggers:
    put:
      operationId: createTrigger
      summary: Create a temporal trigger
      description: |
        Set a future date when a memory should be surfaced. Use for reminders, follow-ups,
        and scheduled memory injection.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Triggers]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTriggerBody"
      responses:
        "201":
          description: Trigger created
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Trigger"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

    get:
      operationId: listTriggers
      summary: List upcoming triggers
      description: |
        List all unfired triggers ordered by trigger time.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Triggers]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Upcoming triggers
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TriggerListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/triggers/pending:
    get:
      operationId: getPendingTriggers
      summary: Get fired (pending) triggers
      description: |
        Get all triggers whose time has passed and have been fired. Check this at the
        start of each session to see what needs attention.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Triggers]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Pending triggers
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TriggerListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/triggers/{id}:
    delete:
      operationId: deleteTrigger
      summary: Cancel a trigger
      description: |
        Delete an unfired trigger. Cannot delete triggers that have already fired.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Triggers]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: id
          in: path
          required: true
          description: Trigger ID
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Trigger cancelled
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [deleted]
                properties:
                  deleted:
                    type: boolean
                    const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Config ──────────────────────────────────────────────────────
  /v1/{ns}/config:
    get:
      operationId: getConfig
      summary: Get namespace configuration
      description: |
        Read the current namespace configuration (decay rate, etc.).

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Config]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Current config
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NamespaceConfig"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

    patch:
      operationId: updateConfig
      summary: Update namespace configuration
      description: |
        Update namespace configuration. Currently supports `decay_rate` (0-1) which controls
        how quickly older memories lose relevance in search results.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Config]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NamespaceConfig"
      responses:
        "200":
          description: Config updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NamespaceConfig"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Relationships ───────────────────────────────────────────────
  /v1/{ns}/relationships:
    put:
      operationId: createRelationship
      summary: Create a relationship
      description: |
        Create a knowledge graph relationship between two entities (source → predicate → target).
        Relationships are used to build a connected entity graph for multi-hop traversal.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (write)
      tags: [Relationships]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRelationshipBody"
      responses:
        "201":
          description: Relationship created
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Relationship"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/relationships/{name}:
    get:
      operationId: getRelationships
      summary: Get relationships for an entity
      description: |
        Get all relationships where the given entity appears as either source or target.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Relationships]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: name
          in: path
          required: true
          description: Entity name to look up relationships for
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: Relationships for the entity
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [relationships, count]
                properties:
                  relationships:
                    type: array
                    items:
                      $ref: "#/components/schemas/Relationship"
                  count:
                    type: integer
                    description: Number of relationships returned
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/relationships/{id}:
    delete:
      operationId: deleteRelationship
      summary: Delete a relationship
      description: |
        Delete a knowledge graph relationship by ID.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Relationships]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: id
          in: path
          required: true
          description: Relationship ID
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Relationship deleted
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [deleted]
                properties:
                  deleted:
                    type: boolean
                    const: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Graph ─────────────────────────────────────────────────────────
  /v1/{ns}/graph/{name}:
    get:
      operationId: getGraphNode
      summary: Get graph node with connections
      description: |
        Get a knowledge graph node for the given entity, including its type, fact count,
        and all direct relationships (both incoming and outgoing).

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Graph]
      parameters:
        - $ref: "#/components/parameters/Namespace"
        - name: name
          in: path
          required: true
          description: Entity name to look up in the graph
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: Graph node with connections
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GraphNode"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/{ns}/graph/traverse:
    post:
      operationId: traverseGraph
      summary: Multi-hop graph traversal
      description: |
        Perform a breadth-first multi-hop traversal of the knowledge graph starting from
        a given entity. Uses a recursive CTE to follow relationships up to `max_depth` hops.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (search)
      tags: [Graph]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TraverseBody"
      responses:
        "200":
          description: Graph traversal results
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [nodes, edges]
                properties:
                  nodes:
                    type: array
                    items:
                      $ref: "#/components/schemas/GraphNode"
                  edges:
                    type: array
                    items:
                      $ref: "#/components/schemas/GraphEdge"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Extract ───────────────────────────────────────────────────────
  /v1/{ns}/extract:
    post:
      operationId: extractConversation
      summary: Extract facts from conversation
      description: |
        Takes raw conversation messages, auto-extracts factual sentences using heuristic
        filters (skips questions, greetings, filler), and stores them as inferred facts
        at 0.6 confidence.

        **Rate limit:** Moderate (30 req/min)
        **x402 price:** $0.001 (search)
      tags: [Extract]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtractBody"
      responses:
        "201":
          description: Facts extracted and stored
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [items, count, extracted_facts]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Item"
                  count:
                    type: integer
                    description: Number of items created
                  extracted_facts:
                    type: integer
                    description: Number of factual sentences extracted
        "200":
          description: No facts found in the conversation
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                type: object
                required: [items, count, extracted_facts]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Item"
                    maxItems: 0
                  count:
                    type: integer
                    const: 0
                  extracted_facts:
                    type: integer
                    const: 0
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─── Stats ────────────────────────────────────────────────────────
  /v1/{ns}/stats:
    get:
      operationId: getStats
      summary: Namespace usage statistics
      description: |
        Returns aggregate usage statistics for the namespace: item counts, session counts,
        lesson counts, category count, and retrieval log metrics.

        **Rate limit:** Generous (120 req/min)
        **x402 price:** $0.0005 (read)
      tags: [Stats]
      parameters:
        - $ref: "#/components/parameters/Namespace"
      responses:
        "200":
          description: Usage statistics
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

components:
  securitySchemes:
    BearerApiKey:
      type: http
      scheme: bearer
      description: |
        API key authentication. The key is SHA-256 hashed server-side and matched against
        stored hashes to resolve a namespace. Obtain keys via Stripe billing integration.
    x402Payment:
      type: apiKey
      in: header
      name: X-Payment
      description: |
        x402 micropayment header. When a request is made without payment or API key, the
        server responds with `402 Payment Required` and a JSON body containing the price
        envelope. The client pays USDC on Base via the x402 facilitator, then resends the
        request with the `X-Payment` header containing the payment proof.

  parameters:
    Namespace:
      name: ns
      in: path
      required: true
      description: |
        Namespace identifier. Auto-resolved from the authenticated identity (API key or
        x402 wallet). The client must include it in the URL but cannot access other namespaces.
      schema:
        type: string

    ItemId:
      name: id
      in: path
      required: true
      description: Numeric item ID (positive integer)
      schema:
        type: integer
        minimum: 1

    SessionId:
      name: id
      in: path
      required: true
      description: Session UUID
      schema:
        type: string
        format: uuid

    LessonId:
      name: id
      in: path
      required: true
      description: Numeric lesson ID (positive integer)
      schema:
        type: integer
        minimum: 1

  headers:
    RateLimit:
      description: |
        Rate limit information per IETF draft-7 (`RateLimit` header).
        Contains `limit`, `remaining`, and `reset` directives.
      schema:
        type: string
      example: "limit=30, remaining=27, reset=45"

  schemas:
    # ─── Health ───────────────────────────────────────────────────
    HealthResponse:
      type: object
      required: [status, timestamp]
      properties:
        status:
          type: string
          const: ok
        timestamp:
          type: string
          format: date-time

    HealthErrorResponse:
      type: object
      required: [status, db]
      properties:
        status:
          type: string
          const: error
        db:
          type: boolean
          const: false

    ReadyResponse:
      type: object
      required: [status, db, model]
      properties:
        status:
          type: string
          enum: [ready, not_ready]
        db:
          type: boolean
          description: Whether the database connection is healthy
        model:
          type: boolean
          description: Whether the embedding model (all-MiniLM-L6-v2) is loaded

    # ─── Items ────────────────────────────────────────────────────
    Fact:
      type: object
      required: [text]
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 5000
          description: The fact text to store
        category:
          type: string
          maxLength: 100
          pattern: "^[a-zA-Z0-9_]+$"
          description: Optional category tag (alphanumeric + underscores only)
        strength:
          type: string
          enum: [explicit, implied, inferred]
          description: How strongly the fact was stated. Affects default confidence.
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Agent-supplied confidence score (0-1). Overrides strength-based default.
        sentiment:
          type: string
          enum: [positive, negative, neutral, mixed]
          description: Optional sentiment label
        data:
          type: object
          additionalProperties: true
          description: Optional structured data (JSON object) — stored as metadata, flattened for embedding

    PutItemsBody:
      type: object
      required: [facts]
      properties:
        facts:
          type: array
          items:
            $ref: "#/components/schemas/Fact"
          minItems: 1
          maxItems: 20
          description: Array of facts to store (1-20)
        session_id:
          type: string
          format: uuid
          description: Optional session ID to associate these facts with

    Item:
      type: object
      required: [id, namespace_id, fact_text, confidence, strength, status, created_at, updated_at]
      properties:
        id:
          type: integer
          description: Unique item ID
        namespace_id:
          type: string
          format: uuid
          description: Owning namespace
        fact_text:
          type: string
          description: The stored fact text
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Write-gate-adjusted confidence score
        strength:
          type: string
          enum: [explicit, implied, inferred]
        category:
          type: ["string", "null"]
          description: Category tag or null
        sentiment:
          type: ["string", "null"]
          description: Sentiment label or null
        status:
          type: string
          enum: [active, archived, superseded, stale, merged]
        metadata:
          type: ["object", "null"]
          additionalProperties: true
          description: Structured data stored with the fact (from `data` field on input)
        invalidated_by:
          type: ["integer", "null"]
          description: ID of the item that superseded this one (null if active)
        gate:
          $ref: "#/components/schemas/WriteGate"
          description: Write-gate scoring results (only present on PUT /items response)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PutItemsResponse:
      type: object
      required: [items, count]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Item"
        count:
          type: integer
          description: Number of items created

    # ─── Query ────────────────────────────────────────────────────
    QueryBody:
      type: object
      properties:
        query_text:
          type: string
          minLength: 1
          maxLength: 5000
          description: Natural language query text (embedded server-side). Required if `query_embedding` is not provided.
        query_embedding:
          type: array
          items:
            type: number
          minItems: 384
          maxItems: 384
          description: Pre-computed 384-dimension embedding vector. Required if `query_text` is not provided.
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
          description: Maximum number of results to return
        categories:
          type: array
          items:
            type: string
            maxLength: 100
          description: Filter results to these categories only. If provided, skips category-tier search.
        min_confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Minimum confidence threshold for returned results
        include_invalidated:
          type: boolean
          default: false
          description: Include superseded/invalidated facts for history tracking
        as_of:
          type: string
          format: date-time
          description: Return facts as they existed at this point in time (created before as_of, not yet archived). ISO 8601 datetime.
        hybrid:
          type: boolean
          default: false
          description: Enable hybrid search combining vector similarity with full-text search via Reciprocal Rank Fusion (RRF). Requires `query_text`. Best for queries with entity names or exact identifiers.
      description: |
        Either `query_text` or `query_embedding` must be provided (at least one is required).

    MemoryResult:
      type: object
      required: [id, fact, confidence, strength, category, age_days, score]
      properties:
        id:
          type: integer
        fact:
          type: string
          description: The fact text
        confidence:
          type: number
        strength:
          type: string
          enum: [explicit, implied, inferred]
        category:
          type: ["string", "null"]
        age_days:
          type: integer
          description: Days since the fact was created
        score:
          type: number
          description: Final relevance score (similarity with time-decay and access-count boosting applied)
        access_count:
          type: integer
          description: Number of times this fact has been accessed

    QueryResponse:
      type: object
      required: [memories, tier_used, total_searched, latency_ms]
      properties:
        memories:
          type: array
          items:
            $ref: "#/components/schemas/MemoryResult"
        tier_used:
          type: string
          enum: [category, full, hybrid]
          description: |
            Which retrieval tier was used. `category` means a category summary matched with
            high similarity; `full` means full HNSW vector search was performed.
        total_searched:
          type: integer
          description: Total number of items searched/considered
        latency_ms:
          type: integer
          description: Server-side retrieval latency in milliseconds

    # ─── Sessions ─────────────────────────────────────────────────
    Session:
      type: object
      required: [id, namespace_id, working_memory, status, expires_at, created_at]
      properties:
        id:
          type: string
          format: uuid
        namespace_id:
          type: string
          format: uuid
        working_memory:
          $ref: "#/components/schemas/WorkingMemory"
        status:
          type: string
          enum: [active, flushed, discarded, expired]
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    WorkingMemory:
      type: object
      additionalProperties: true
      description: Arbitrary JSON object used as ephemeral working memory for a session.

    FlushSessionResponse:
      type: object
      required: [flushed, working_memory]
      properties:
        flushed:
          type: boolean
          const: true
        working_memory:
          $ref: "#/components/schemas/WorkingMemory"
          description: Final snapshot of the working memory at time of flush

    # ─── Lessons ──────────────────────────────────────────────────
    PutLessonBody:
      type: object
      required: [pattern]
      properties:
        pattern:
          type: string
          minLength: 1
          maxLength: 5000
          description: The behavioral pattern or lesson learned
        source:
          type: string
          enum: [explicit, correction, feedback]
          default: explicit
          description: How the lesson was learned

    Lesson:
      type: object
      required: [id, namespace_id, pattern, source, active, access_count, created_at]
      properties:
        id:
          type: integer
        namespace_id:
          type: string
          format: uuid
        pattern:
          type: string
          description: The lesson pattern text
        source:
          type: string
          enum: [explicit, correction, feedback]
        active:
          type: boolean
        access_count:
          type: integer
          description: Number of times this lesson has been retrieved
        created_at:
          type: string
          format: date-time

    LessonsListResponse:
      type: object
      required: [lessons, count]
      properties:
        lessons:
          type: array
          items:
            $ref: "#/components/schemas/Lesson"
        count:
          type: integer
          description: Number of active lessons returned

    # ─── Categories ───────────────────────────────────────────────
    CategoryTopFact:
      type: object
      required: [id, fact_text, confidence]
      properties:
        id:
          type: integer
        fact_text:
          type: string
        confidence:
          type: number

    CategoryWithFacts:
      type: object
      required: [id, namespace_id, name, item_count, pending_items, last_rebuilt_at, top_facts]
      properties:
        id:
          type: integer
        namespace_id:
          type: string
          format: uuid
        name:
          type: string
          description: Category name
        item_count:
          type: integer
          description: Total active items in this category
        pending_items:
          type: integer
          description: Items added since last summary rebuild
        last_rebuilt_at:
          type: ["string", "null"]
          format: date-time
          description: When the category summary embedding was last rebuilt
        top_facts:
          type: array
          items:
            $ref: "#/components/schemas/CategoryTopFact"
          maxItems: 10
          description: Top 10 facts by confidence in this category

    CategoriesListResponse:
      type: object
      required: [categories, count]
      properties:
        categories:
          type: array
          items:
            $ref: "#/components/schemas/CategoryWithFacts"
        count:
          type: integer
          description: Number of categories returned

    # ─── Stats ────────────────────────────────────────────────────
    StatsResponse:
      type: object
      required: [items, sessions, lessons, categories, retrievals, memory_health]
      properties:
        items:
          type: object
          required: [total, active]
          properties:
            total:
              type: integer
              description: Total items (including archived)
            active:
              type: integer
              description: Active (non-archived) items
        sessions:
          type: object
          required: [total, active, flushed]
          properties:
            total:
              type: integer
            active:
              type: integer
              description: Currently active sessions
            flushed:
              type: integer
              description: Sessions that were flushed
        lessons:
          type: object
          required: [total, active]
          properties:
            total:
              type: integer
            active:
              type: integer
              description: Currently active lessons
        categories:
          type: integer
          description: Total number of categories
        retrievals:
          type: object
          required: [total, avg_latency_ms]
          properties:
            total:
              type: integer
              description: Total number of retrieval queries logged
            avg_latency_ms:
              type: ["integer", "null"]
              description: Average retrieval latency in milliseconds (null if no retrievals)
        memory_health:
          $ref: "#/components/schemas/MemoryHealth"
        referral_code:
          type: ["string", "null"]
          description: Your referral code for sharing with other agents

    MemoryHealth:
      type: object
      required: [duplicate_ratio, superseded_items, merged_items, stale_items, entity_count, pending_triggers]
      properties:
        duplicate_ratio:
          type: number
          description: Ratio of superseded+merged items to total managed items
        superseded_items:
          type: integer
          description: Items auto-superseded by newer versions
        merged_items:
          type: integer
          description: Items merged during nightly dedup
        stale_items:
          type: integer
          description: Items archived due to 90+ days without access
        avg_confidence:
          type: ["number", "null"]
          description: Average confidence of active items
        avg_access_count:
          type: ["number", "null"]
          description: Average access count of active items
        categories_needing_rebuild:
          type: integer
          description: Categories with pending summary re-embedding
        oldest_unaccessed_days:
          type: ["integer", "null"]
          description: Age in days of the oldest never-accessed active item
        entity_count:
          type: integer
          description: Number of distinct entities extracted
        pending_triggers:
          type: integer
          description: Number of unfired triggers
        last_maintenance_run:
          type: ["string", "null"]
          format: date-time
          description: When the nightly maintenance job last ran

    # ─── Write Gate ──────────────────────────────────────────────
    WriteGate:
      type: object
      required: [score, checks, conflict_item_ids]
      properties:
        score:
          type: number
          minimum: 0
          maximum: 1
          description: Composite gate score (average of factual, density, conflict checks)
        checks:
          type: object
          required: [factual, density, conflict]
          properties:
            factual:
              type: number
              description: "1.0 = factual statement, 0.1 = question/greeting, 0.2 = too short"
            density:
              type: number
              description: "Information density (proper nouns, numbers). 0.3-1.0"
            conflict:
              type: number
              description: "1.0 = no conflicts, 0.5 = duplicate detected (>0.9 cosine similarity)"
        conflict_item_ids:
          type: array
          items:
            type: integer
          description: IDs of existing items with >0.9 cosine similarity (potential duplicates)
        superseded_ids:
          type: array
          items:
            type: integer
          description: IDs of items that were auto-archived (superseded) by this new item

    # ─── Entities ────────────────────────────────────────────────
    Entity:
      type: object
      required: [name, type, item_id]
      properties:
        name:
          type: string
          description: Entity name (e.g., "John Smith", "@alice")
        type:
          type: string
          enum: [name, mention, reference]
          description: How the entity was extracted
        item_id:
          type: integer
          description: The memory item this entity was extracted from

    EntitySummary:
      type: object
      required: [name, type, fact_count]
      properties:
        name:
          type: string
        type:
          type: string
          enum: [name, mention, reference]
        fact_count:
          type: integer
          description: Number of facts referencing this entity

    EntityListResponse:
      type: object
      required: [entities, count]
      properties:
        entities:
          type: array
          items:
            $ref: "#/components/schemas/EntitySummary"
        count:
          type: integer

    EntityWithFacts:
      type: object
      required: [entity, facts]
      properties:
        entity:
          type: string
          description: The entity name
        facts:
          type: array
          items:
            type: object
            required: [id, fact_text, confidence, category]
            properties:
              id:
                type: integer
              fact_text:
                type: string
              confidence:
                type: number
              category:
                type: ["string", "null"]

    # ─── Triggers ────────────────────────────────────────────────
    CreateTriggerBody:
      type: object
      required: [trigger_at, label]
      properties:
        trigger_at:
          type: string
          format: date-time
          description: ISO 8601 datetime for when the trigger should fire
        label:
          type: string
          minLength: 1
          maxLength: 500
          description: Description of what to remember/do at trigger time
        item_id:
          type: integer
          minimum: 1
          description: Optional link to a specific memory item

    Trigger:
      type: object
      required: [id, namespace_id, trigger_at, label, fired, created_at]
      properties:
        id:
          type: integer
        namespace_id:
          type: string
          format: uuid
        trigger_at:
          type: string
          format: date-time
        label:
          type: string
        item_id:
          type: ["integer", "null"]
        fired:
          type: boolean
        fired_at:
          type: ["string", "null"]
          format: date-time
        created_at:
          type: string
          format: date-time

    TriggerListResponse:
      type: object
      required: [triggers, count]
      properties:
        triggers:
          type: array
          items:
            $ref: "#/components/schemas/Trigger"
        count:
          type: integer

    # ─── Config ──────────────────────────────────────────────────
    NamespaceConfig:
      type: object
      properties:
        decay_rate:
          type: number
          minimum: 0
          maximum: 1
          default: 0.01
          description: Score decay rate per day (0 = no decay, 1 = max decay)

    # ─── Relationships ──────────────────────────────────────────────
    CreateRelationshipBody:
      type: object
      required: [source, predicate, target]
      properties:
        source:
          type: string
          minLength: 1
          description: Source entity name
        predicate:
          type: string
          minLength: 1
          description: Relationship predicate (e.g., works_at, manages, knows)
        target:
          type: string
          minLength: 1
          description: Target entity name
        item_id:
          type: integer
          minimum: 1
          description: Optional link to a specific memory item
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Confidence score for this relationship (0-1)

    Relationship:
      type: object
      required: [id, namespace_id, source_name, predicate, target_name, confidence, created_at]
      properties:
        id:
          type: integer
          description: Unique relationship ID
        namespace_id:
          type: string
          format: uuid
          description: Owning namespace
        source_name:
          type: string
          description: Source entity name
        predicate:
          type: string
          description: Relationship predicate
        target_name:
          type: string
          description: Target entity name
        item_id:
          type: ["integer", "null"]
          description: Linked memory item ID (null if not linked)
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Relationship confidence score
        created_at:
          type: string
          format: date-time

    # ─── Graph ──────────────────────────────────────────────────────
    GraphNode:
      type: object
      required: [name, entity_type, fact_count, relationships]
      properties:
        name:
          type: string
          description: Entity name
        entity_type:
          type: string
          description: Type of entity (e.g., name, mention, reference)
        fact_count:
          type: integer
          description: Number of facts referencing this entity
        relationships:
          type: array
          items:
            type: object
            required: [predicate, target, direction]
            properties:
              predicate:
                type: string
                description: Relationship predicate
              target:
                type: string
                description: Connected entity name
              direction:
                type: string
                enum: [outgoing, incoming]
                description: Whether the relationship goes from or to this entity

    GraphEdge:
      type: object
      required: [source, predicate, target]
      properties:
        source:
          type: string
          description: Source entity name
        predicate:
          type: string
          description: Relationship predicate
        target:
          type: string
          description: Target entity name

    # ─── Extract ────────────────────────────────────────────────────
    ConversationMessage:
      type: object
      required: [role, content]
      properties:
        role:
          type: string
          enum: [user, assistant, system]
          description: Message role
        content:
          type: string
          description: Message content

    ExtractBody:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          items:
            $ref: "#/components/schemas/ConversationMessage"
          minItems: 1
          description: Conversation messages to extract facts from
        category:
          type: string
          maxLength: 100
          pattern: "^[a-zA-Z0-9_]+$"
          description: Optional category to assign to extracted facts
        session_id:
          type: string
          format: uuid
          description: Optional session ID to associate extracted facts with

    TraverseBody:
      type: object
      required: [start]
      properties:
        start:
          type: string
          minLength: 1
          description: Starting entity name for traversal
        max_depth:
          type: integer
          minimum: 1
          maximum: 4
          default: 2
          description: Maximum number of hops to traverse (1-4)
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
          description: Maximum number of nodes to return (1-100)

    # ─── History ────────────────────────────────────────────────────
    HistoryEntry:
      type: object
      required: [id, fact_text, confidence, status, created_at]
      properties:
        id:
          type: integer
          description: Item ID
        fact_text:
          type: string
          description: The fact text at this point in the chain
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Confidence score
        status:
          type: string
          enum: [active, archived, superseded, stale, merged]
          description: Item status
        invalidated_by:
          type: ["integer", "null"]
          description: ID of the item that superseded this one (null if not superseded)
        created_at:
          type: string
          format: date-time
        archived_at:
          type: ["string", "null"]
          format: date-time
          description: When the item was archived (null if still active)

    # ─── Errors ───────────────────────────────────────────────────
    Error:
      type: object
      required: [error, code]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code (e.g., ITEM_NOT_FOUND, AUTH_REQUIRED, SESSION_CLOSED)
        message:
          type: string
          description: Detailed error message (only in development mode)

  responses:
    ValidationError:
      description: Request validation failed (invalid body, params, or query)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Validation failed"

    Unauthorized:
      description: Authentication required — provide a Bearer API key or x402 payment
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Authentication required: provide x402 payment or API key"

    PaymentRequired:
      description: |
        x402 payment required. The response body contains the price envelope with payment
        instructions. API key holders never receive this response.
      content:
        application/json:
          schema:
            type: object
            description: x402 price envelope with payment instructions
            properties:
              accepts:
                type: object
                properties:
                  scheme:
                    type: string
                    const: exact
                  network:
                    type: string
                    example: "eip155:8453"
                  payTo:
                    type: string
                    description: EVM address to pay
                    example: "0x1234567890abcdef1234567890abcdef12345678"
                  price:
                    type: string
                    description: Price in USD
                    example: "$0.001"
              description:
                type: string

    NotFound:
      description: The requested resource was not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Item not found"

    TooManyRequests:
      description: Rate limit exceeded. Check the `RateLimit` header for reset timing.
      headers:
        RateLimit:
          $ref: "#/components/headers/RateLimit"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Too many requests — please slow down"

    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Internal server error"
