openapi: 3.1.0
info:
  title: ContextDB Cloud data-plane API
  version: 1.0.0-alpha.1
  description: |
    The ContextDB Cloud data plane (hosted alpha). Managed, decision-safe
    agent memory: remember with provenance, recall for grounding, and
    recall_for_action before consequential actions.

    Authentication: `Authorization: Bearer cdb_…` is a project-wide server
    credential. The key determines org and project; requests supply only
    `user_id`, which is a partition key, not end-user authentication.

    This surface is the complete alpha allowlist. Shipped `/v1/forget`
    supports scoped deletion of one memory, one stable entity-and-attribute
    slot, or one verified user partition. Partition erasure requires
    `erase_partition=true`, typed `confirmation` equal to `user_id`, and an
    `Idempotency-Key`. Scoped ID checks make a foreign memory ID
    indistinguishable from a missing one.

    `/mcp` is a Hosted Alpha stateless JSON-RPC 2.0 endpoint over HTTP.
    It exposes tools only, with no sessions, SSE, resumability, or
    server-initiated messages.

    Not production-ready. No SLA.
  contact:
    name: Atoms AI
    email: gaurav@saaslabs.co
  license:
    name: Proprietary, hosted alpha
    identifier: LicenseRef-Proprietary
servers:
  - url: https://api.contextdb.ai
    description: Hosted alpha
tags:
  - name: meta
    description: Public process health and dependency readiness
  - name: memory
    description: Store, extract, and recall customer memories
  - name: decisions
    description: Check trusted memories and resolve explicit confirmations
  - name: mcp
    description: Model Context Protocol discovery and tool calls

security:
  - ProjectKey: []

paths:
  /health:
    get:
      tags: [meta]
      summary: Liveness with no auth or dependency checks
      operationId: health
      security: []
      responses:
        "200":
          description: Process is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "429": { $ref: "#/components/responses/RateLimited" }

  /.well-known/contextdb-audit-keys.json:
    get:
      tags: [meta]
      summary: Public keys for offline audit-export verification
      operationId: auditVerificationKeys
      security: []
      responses:
        "200":
          description: Active audit signing keys
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditPublicKeys"
        "429": { $ref: "#/components/responses/RateLimited" }

  /ready:
    get:
      tags: [meta]
      summary: Readiness for metadata, memory, connector, and formation stores plus PII key
      operationId: ready
      security: []
      responses:
        "200":
          description: All checks pass
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadyResponse"
        "503":
          description: At least one check fails
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadyResponse"
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/remember:
    post:
      tags: [memory]
      summary: Store one memory
      operationId: remember
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RememberRequest"
      responses:
        "200":
          description: The stored memory
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RememberResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/remember_many:
    post:
      tags: [memory]
      summary: LLM-free batch store
      operationId: rememberMany
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RememberManyRequest"
      responses:
        "200":
          description: The stored memories
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RememberManyResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/evolve:
    post:
      tags: [memory]
      summary: Apply one explicit factual-memory evolution operation
      operationId: evolve
      parameters:
        - $ref: "#/components/parameters/RequiredIdempotencyKey"
      description: |
        Applies ADD, UPDATE, DELETE, or NOOP through the atomic public SDK
        state machine. The project key fixes org/project scope and `user_id`
        fixes the end-user partition. Service and evaluation keys are denied.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EvolveRequest"
      responses:
        "200":
          description: Applied operation and resulting consistency floor
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvolveResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/extract_memories:
    post:
      tags: [memory]
      summary: Extract sourced memories from a bounded conversation
      operationId: extractMemories
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      description: |
        Runs a bounded, extractive formation loop over PII-processed turns.
        Every candidate cites an exact quote and turn index. `propose` returns
        candidates without writing; `commit` writes validated candidates
        through the same scoped trust and slot-deduplication path as remember.

        Provider failures return a well-formed terminal run result with a
        named status. No raw transcript is stored in the formation-run ledger.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtractMemoriesRequest"
      responses:
        "200":
          description: Formation completed or found no durable memories
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FormationResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502":
          description: Provider failed or returned invalid output after bounded retry
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FormationResponse" }
        "503":
          description: Provider unavailable, queue exhausted, or storage failed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FormationResponse" }
        "504":
          description: Provider timed out within the formation budget
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FormationResponse" }

  /v1/formation/jobs:
    post:
      tags: [memory]
      summary: Submit a durable asynchronous Formation job
      operationId: submitFormationJob
      parameters:
        - $ref: "#/components/parameters/RequiredIdempotencyKey"
      description: |
        Durably snapshots and enqueues bounded, PII-processed structured text
        turns. The worker returns either encrypted candidates for review or an
        idempotent commit result. Audio is not accepted. Poll the Location
        resource until the closed status is `succeeded` or `failed`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FormationJobRequest"
      responses:
        "202":
          description: Job durably accepted, or an identical enqueue replayed
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
            Location:
              description: Project-scoped polling resource for this job
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormationJobSubmission"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/formation/jobs/{job_id}:
    get:
      tags: [memory]
      summary: Inspect one project-scoped Formation job
      operationId: getFormationJob
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
            pattern: "^frm_[a-f0-9]{32}$"
      responses:
        "200":
          description: Current state, attempts, and encrypted-at-rest terminal result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormationJob"
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/recall:
    post:
      tags: [memory]
      summary: Recall memories for grounding
      operationId: recall
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecallRequest"
      responses:
        "200":
          description: Rendered context plus the memories behind it
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecallResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/recall_for_action:
    post:
      tags: [decisions]
      summary: Trusted, action-relevant memory only
      description: |
        An advisory pre-action evaluation. Records and returns a durable
        `decision_id`, explicit `act` / `ask` / `abstain` outcome, trusted
        memories, and pending confirmation IDs. The host uses the decision ID
        to report its terminal outcome through `/v1/receipts`, and must enforce
        the result itself. Confirmation is one policy signal, not the only path
        to `act`, and it does not prove objective truth.
      operationId: recallForAction
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecallForActionRequest"
      responses:
        "200":
          description: Durable action decision and its receipt target
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecallForActionResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/confirm:
    post:
      tags: [decisions]
      summary: Graduate a pending memory via explicit confirmation
      description: |
        Records the authenticated caller's project-credential context against
        one scoped pending memory and updates its confirmation state. API and
        MCP authentication identify the project credential, not the end user
        named by `user_id`; the customer host authenticates that person and
        retains any end-user attestation. Call `recall_for_action` again after
        confirmation because policy still determines `act`, `ask`, or
        `abstain`. Confirmation does not prove objective truth. Partition
        ownership is enforced at the store layer: a memory ID outside the
        caller's org/project/user scope returns 404, indistinguishable from a
        missing one.
      operationId: confirm
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfirmRequest"
      responses:
        "200":
          description: The confirmed memory
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConfirmResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/forget:
    post:
      tags: [memory]
      summary: Delete one memory, one slot, or a verified user partition
      description: |
        Scope is derived from the project key plus the required `user_id`.
        Choose exactly one mode: `memory_id`, `entity` + `attribute`, or
        `erase_partition=true`. Whole-partition erasure also requires
        `confirmation` equal to `user_id` and an `Idempotency-Key`.
      operationId: forget
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForgetRequest"
      responses:
        "200":
          description: Erasure result
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForgetResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /v1/receipts:
    post:
      tags: [decisions]
      summary: Report what the host did after an action decision
      description: |
        Attaches one terminal, host-reported execution receipt to the durable
        `decision_id` returned by `recall_for_action`. ContextDB records the
        report but does not inspect the downstream system. Structured fields
        only: free-text payloads are not accepted. Acting after an `ask` or
        `abstain` decision is retained with `policy_alignment=violated`.
      operationId: recordExecutionReceipt
      parameters:
        - $ref: "#/components/parameters/RequiredIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecutionReceiptRequest"
      responses:
        "200":
          description: The terminal execution receipt
          headers:
            Idempotency-Replayed:
              $ref: "#/components/headers/IdempotencyReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecutionReceiptResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

  /mcp:
    post:
      tags: [mcp]
      summary: Model Context Protocol endpoint (JSON-RPC 2.0, stateless)
      description: |
        Single JSON-RPC 2.0 objects over POST; responses are JSON. Tools:
        `remember`, `recall`, `recall_for_action`, `pending_confirmations`,
        `confirm`, and scoped `forget`, with the same contract and scoping as the
        plain HTTP routes. Whole-partition erasure remains on the idempotent
        HTTP and console flows. Authentication is the same `cdb_` bearer key.
        Notifications return 202 with no body.
      operationId: mcp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JsonRpcRequest"
      responses:
        "200":
          description: JSON-RPC result or error object
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonRpcResponse"
        "202":
          description: Notification accepted (no body)
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/pending_confirmations:
    get:
      tags: [decisions]
      summary: Facts waiting on a yes (query string)
      operationId: pendingConfirmationsGet
      parameters:
        - name: user_id
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/UserId"
      responses:
        "200":
          description: Memories pending confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingConfirmationsResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }
    post:
      tags: [decisions]
      summary: Facts waiting on a yes (JSON body)
      operationId: pendingConfirmationsPost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PendingConfirmationsRequest"
      responses:
        "200":
          description: Memories pending confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingConfirmationsResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/StoreUnavailable" }

components:
  securitySchemes:
    ProjectKey:
      type: http
      scheme: bearer
      description: Project-wide server credential (`cdb_…`). Never in browsers.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Project-scoped retry key (8-128 URL-safe characters). The same key and
        canonical payload replay the encrypted stored response. Reusing a key
        with a different route or payload returns 409. On extract_memories this
        applies only when mode is commit.
      schema:
        type: string
        pattern: "^[A-Za-z0-9._:\\-]{8,128}$"
    RequiredIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Stable project-scoped key for this logical mutation or job. Retrying
        with the same key and payload resolves to the original resource or
        response; reuse with a different payload returns 409.
      schema:
        type: string
        pattern: "^[A-Za-z0-9._:\\-]{8,128}$"

  headers:
    IdempotencyReplayed:
      description: "true when this response was replayed; false for the owner request"
      schema:
        type: string
        enum: ["true", "false"]

  responses:
    BadRequest:
      description: Malformed request or failed validation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthenticated:
      description: Missing, malformed, unknown, or revoked API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotEntitled:
      description: Authenticated but not allowed (entitlement or billing state)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Target not found in the caller's partition (or missing)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    IdempotencyConflict:
      description: |
        The key was reused with a different request, an identical request is
        still running, or a previous process ended with an ambiguous outcome.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: |
        The decision already has a terminal receipt, or the Idempotency-Key
        conflicts with an earlier request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PayloadTooLarge:
      description: Body or batch exceeds the configured cap
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: |
        The client IP/project exceeded its request rate, or the project's
        idempotency retention capacity is full until older keys expire.
      headers:
        Retry-After:
          description: Seconds until a request is likely to be accepted
          schema: { type: integer, minimum: 1 }
        X-RateLimit-Limit:
          description: Sustained requests allowed per minute
          schema: { type: integer, minimum: 1 }
        X-RateLimit-Remaining:
          description: Requests remaining in the current burst bucket
          schema: { type: integer, minimum: 0 }
        X-RateLimit-Scope:
          description: Limiting identity (`ip` or `project`)
          schema: { type: string, enum: [ip, project] }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    StoreUnavailable:
      description: Memory or metadata store unreachable
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    UserId:
      type: string
      pattern: "^[A-Za-z0-9._:@+\\-]{1,128}$"
      description: End-user memory partition key. Not an identity assertion.

    MemoryVersion:
      type: integer
      minimum: 0
      description: Monotonic consistency version within one organization/project.

    PrimaryWalLsn:
      type: [string, "null"]
      pattern: "^[0-9A-F]+/[0-9A-F]+$"
      description: Primary PostgreSQL WAL position observed after commit.

    EpistemicSource:
      type: string
      enum: [user_stated, agent_inferred, third_party]
      description: Who vouches for the claim. Required on writes.

    EvolutionOperation:
      type: string
      enum: [add, update, delete, noop]

    EvolutionOutcome:
      type: string
      enum: [added, updated, deleted, noop, contested]

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code
        message:
          type: string
          description: Human-readable message; never a stack trace
        request_id:
          type: [string, "null"]
      additionalProperties: false

    HealthResponse:
      type: object
      required: [ok, service]
      properties:
        ok: { type: boolean }
        service: { type: string }
        sdk_pin: { type: string }
      additionalProperties: true

    AuditPublicKeys:
      type: object
      required: [schema_version, keys]
      properties:
        schema_version: { type: integer, const: 1 }
        keys:
          type: array
          items:
            type: object
            required: [kid, alg, public_key, status]
            properties:
              kid: { type: string, minLength: 16, maxLength: 16 }
              alg: { type: string, const: Ed25519 }
              public_key: { type: string }
              status: { type: string, const: active }
            additionalProperties: false
      additionalProperties: false

    ReadyResponse:
      type: object
      required: [ready, checks]
      properties:
        ready: { type: boolean }
        checks:
          type: object
          required:
            - metadata_store
            - memory_store
            - connector_store
            - formation_store
            - pii_key
          properties:
            metadata_store: { type: boolean }
            memory_store: { type: boolean }
            connector_store: { type: boolean }
            formation_store: { type: boolean }
            pii_key: { type: boolean }
          additionalProperties: false
      additionalProperties: false

    Memory:
      type: object
      required:
        - id
        - content
        - user_id
        - epistemic_source
        - confidence
        - corroboration_count
        - action_relevant
        - requires_confirmation
        - confirmed
        - independent_corroboration
        - injection_suspect
        - entity_key
        - attribute_key
        - valid_until
        - superseded_by
      properties:
        id: { type: string }
        content:
          type: string
          description: Stored content with PII redacted as `[TYPE]` placeholders
        user_id:
          type: [string, "null"]
        epistemic_source:
          $ref: "#/components/schemas/EpistemicSource"
        confidence: { type: number, minimum: 0, maximum: 1 }
        corroboration_count: { type: integer, minimum: 0 }
        action_relevant: { type: boolean }
        requires_confirmation: { type: boolean }
        confirmed: { type: boolean }
        independent_corroboration: { type: integer, minimum: 0 }
        injection_suspect: { type: boolean }
        entity_key: { type: [string, "null"] }
        attribute_key: { type: [string, "null"] }
        valid_until: { type: [string, "null"], format: date-time }
        superseded_by: { type: [string, "null"] }
      additionalProperties: false

    RememberRequest:
      type: object
      required: [user_id, content, source]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        content: { type: string, minLength: 1, maxLength: 4000 }
        source:
          $ref: "#/components/schemas/EpistemicSource"
        confidence: { type: number, minimum: 0, maximum: 1 }
        action_relevant: { type: boolean }
        entity: { type: string, maxLength: 200 }
        attribute: { type: string, maxLength: 200 }
      additionalProperties: false
      description: entity and attribute must be set together (400 otherwise).

    RememberItem:
      type: object
      required: [content]
      properties:
        content: { type: string, minLength: 1, maxLength: 4000 }
        source:
          $ref: "#/components/schemas/EpistemicSource"
        confidence: { type: number, minimum: 0, maximum: 1 }
        action_relevant: { type: boolean }
        entity: { type: string, maxLength: 200 }
        attribute: { type: string, maxLength: 200 }
      additionalProperties: false

    RememberManyRequest:
      type: object
      required: [user_id, items]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        items:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/RememberItem"
      additionalProperties: false

    RememberResponse:
      type: object
      required: [memory, memory_version, primary_wal_lsn]
      properties:
        memory:
          $ref: "#/components/schemas/Memory"
        memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        primary_wal_lsn:
          $ref: "#/components/schemas/PrimaryWalLsn"
      additionalProperties: false

    RememberManyResponse:
      type: object
      required: [memories, memory_version, primary_wal_lsn]
      properties:
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
        memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        primary_wal_lsn:
          $ref: "#/components/schemas/PrimaryWalLsn"
      additionalProperties: false

    EvolveRequest:
      type: object
      required: [user_id, operation]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        operation:
          $ref: "#/components/schemas/EvolutionOperation"
        content: { type: string, minLength: 1, maxLength: 4000 }
        source:
          $ref: "#/components/schemas/EpistemicSource"
        confidence: { type: number, minimum: 0, maximum: 1 }
        action_relevant: { type: boolean }
        entity: { type: string, minLength: 1, maxLength: 200 }
        attribute: { type: string, minLength: 1, maxLength: 200 }
        target_memory_id: { type: string, minLength: 1, maxLength: 256 }
        metadata:
          type: object
          maxProperties: 50
          propertyNames:
            pattern: "^[A-Za-z0-9._:\\-]{1,128}$"
          additionalProperties: true
        noop_reason: { type: string, minLength: 1, maxLength: 256 }
      additionalProperties: false
      description: |
        ADD requires content+source and rejects target_memory_id. UPDATE
        requires content+source plus target_memory_id or entity+attribute.
        DELETE accepts no content/write attributes and requires a target or
        slot. NOOP accepts no content/write attributes and requires a reason.

    EvolveResponse:
      type: object
      required:
        - requested_operation
        - applied_operation
        - outcome
        - memory
        - previous_memory_ids
        - deleted_memory_ids
        - noop_reason
        - memory_version
        - primary_wal_lsn
        - request_id
      properties:
        requested_operation:
          $ref: "#/components/schemas/EvolutionOperation"
        applied_operation:
          $ref: "#/components/schemas/EvolutionOperation"
        outcome:
          $ref: "#/components/schemas/EvolutionOutcome"
        memory:
          oneOf:
            - $ref: "#/components/schemas/Memory"
            - type: "null"
        previous_memory_ids:
          type: array
          maxItems: 100
          items: { type: string, minLength: 1, maxLength: 256 }
        deleted_memory_ids:
          type: array
          maxItems: 100
          items: { type: string, minLength: 1, maxLength: 256 }
        noop_reason: { type: [string, "null"], minLength: 1, maxLength: 256 }
        memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        primary_wal_lsn:
          $ref: "#/components/schemas/PrimaryWalLsn"
        request_id: { type: string }
      additionalProperties: false

    FormationTurn:
      type: object
      required: [speaker, content]
      properties:
        speaker:
          type: string
          enum: [user, agent, third_party]
        content:
          type: string
          minLength: 1
          maxLength: 2000
      additionalProperties: false

    ExtractMemoriesRequest:
      type: object
      required: [user_id, turns]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        turns:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: "#/components/schemas/FormationTurn" }
        mode:
          type: string
          enum: [propose, commit]
          default: propose
        source_id:
          type: [string, "null"]
          pattern: "^[A-Za-z0-9._:@+\\-]{1,128}$"
        max_memories:
          type: integer
          minimum: 1
          maximum: 20
          default: 10
      additionalProperties: false

    FormationCandidate:
      type: object
      required:
        - content
        - quote
        - turn_indexes
        - source
        - confidence
        - action_relevant
        - entity
        - attribute
        - operation
        - target_memory_id
        - noop_reason
        - candidate_key
      properties:
        content: { type: [string, "null"] }
        quote:
          type: [string, "null"]
          description: Exact PII-processed quote supporting the candidate
        turn_indexes:
          type: array
          items: { type: integer, minimum: 0 }
        source:
          $ref: "#/components/schemas/EpistemicSource"
        confidence: { type: number, minimum: 0, maximum: 1 }
        action_relevant: { type: boolean }
        entity: { type: [string, "null"] }
        attribute: { type: [string, "null"] }
        operation:
          $ref: "#/components/schemas/EvolutionOperation"
        target_memory_id: { type: [string, "null"], maxLength: 256 }
        noop_reason: { type: [string, "null"], maxLength: 64 }
        candidate_key:
          type: string
          pattern: "^fmc_[a-f0-9]{40}$"
      additionalProperties: false

    FormationOperationResult:
      type: object
      required:
        - candidate_key
        - requested_operation
        - applied_operation
        - outcome
        - memory_id
        - previous_memory_ids
        - deleted_memory_ids
        - target_memory_id
        - noop_reason
      properties:
        candidate_key:
          type: string
          pattern: "^(?:fmc_[a-f0-9]{40}|legacy_v1_[0-9]+)$"
        requested_operation:
          $ref: "#/components/schemas/EvolutionOperation"
        applied_operation:
          $ref: "#/components/schemas/EvolutionOperation"
        outcome:
          $ref: "#/components/schemas/EvolutionOutcome"
        memory_id: { type: [string, "null"] }
        previous_memory_ids:
          type: array
          maxItems: 100
          items: { type: string }
        deleted_memory_ids:
          type: array
          maxItems: 100
          items: { type: string }
        target_memory_id: { type: [string, "null"] }
        noop_reason: { type: [string, "null"], maxLength: 256 }
      additionalProperties: false

    FormationOperationCounts:
      type: object
      required: [added, updated, deleted, noop]
      properties:
        added: { type: integer, minimum: 0, maximum: 20 }
        updated: { type: integer, minimum: 0, maximum: 20 }
        deleted: { type: integer, minimum: 0, maximum: 20 }
        noop: { type: integer, minimum: 0, maximum: 20 }
      additionalProperties: false

    FormationResponse:
      type: object
      required:
        - run_id
        - status
        - mode
        - attempts
        - candidates
        - memories
        - memory_ids
        - operation_results
        - operation_counts
        - memory_version
        - primary_wal_lsn
        - error_code
        - request_id
      properties:
        run_id: { type: string }
        status:
          type: string
          enum:
            - completed
            - no_memories
            - provider_unavailable
            - provider_error
            - invalid_output
            - timed_out
            - budget_exhausted
            - storage_error
            - internal_error
            - interrupted
        mode:
          type: string
          enum: [propose, commit]
        attempts: { type: integer, minimum: 0, maximum: 3 }
        candidates:
          type: array
          items: { $ref: "#/components/schemas/FormationCandidate" }
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
        memory_ids:
          type: array
          maxItems: 20
          items: { type: string }
        operation_results:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/FormationOperationResult" }
        operation_counts:
          $ref: "#/components/schemas/FormationOperationCounts"
        memory_version:
          type: [integer, "null"]
          minimum: 0
        primary_wal_lsn:
          type: [string, "null"]
          pattern: "^[0-9A-Fa-f]+/[0-9A-Fa-f]+$"
        error_code: { type: [string, "null"] }
        request_id: { type: string }
      additionalProperties: false

    FormationJobRequest:
      type: object
      required: [user_id, turns]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        turns:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: "#/components/schemas/FormationTurn" }
        mode:
          type: string
          enum: [propose, commit]
          default: propose
        source_id:
          type: [string, "null"]
          pattern: "^[A-Za-z0-9._:@+\\-]{1,128}$"
        max_memories:
          type: integer
          minimum: 1
          maximum: 20
          default: 10
        deadline_seconds:
          type: number
          exclusiveMinimum: 0
          maximum: 300
          default: 25
      additionalProperties: false

    FormationJobStatus:
      type: string
      enum: [queued, running, retry_wait, succeeded, failed]

    FormationTerminalReason:
      type: string
      enum:
        - completed
        - no_memories
        - input_rejected
        - pii_rejected
        - scope_rejected
        - result_rejected
        - provider_unavailable
        - provider_error
        - deadline_exceeded
        - max_attempts
        - worker_lost
        - commit_rejected
        - storage_error
        - internal_error
        - configuration_changed

    FormationJobSubmission:
      type: object
      required: [job_id, status, request_id]
      properties:
        job_id:
          type: string
          pattern: "^frm_[a-f0-9]{32}$"
        status:
          $ref: "#/components/schemas/FormationJobStatus"
        request_id: { type: string }
      additionalProperties: false

    FormationCandidateReview:
      type: object
      required:
        - content
        - quote
        - turn_indexes
        - source
        - confidence
        - action_relevant
        - entity
        - attribute
        - operation
        - target_memory_id
        - noop_reason
        - candidate_key
        - accepted
        - rejection_reason
      properties:
        content: { type: [string, "null"] }
        quote:
          type: [string, "null"]
          description: Exact PII-processed quote supporting an accepted candidate
        turn_indexes:
          type: array
          items: { type: integer, minimum: 0 }
        source:
          oneOf:
            - $ref: "#/components/schemas/EpistemicSource"
            - type: "null"
        confidence: { type: [number, "null"], minimum: 0, maximum: 1 }
        action_relevant: { type: [boolean, "null"] }
        entity: { type: [string, "null"] }
        attribute: { type: [string, "null"] }
        operation:
          oneOf:
            - $ref: "#/components/schemas/EvolutionOperation"
            - type: "null"
        target_memory_id: { type: [string, "null"] }
        noop_reason: { type: [string, "null"], maxLength: 64 }
        candidate_key:
          type: [string, "null"]
          pattern: "^fmc_[a-f0-9]{40}$"
        accepted: { type: boolean }
        candidate_index: { type: integer, minimum: -1 }
        rejection_reason: { type: [string, "null"] }
      additionalProperties: false

    FormationAttempt:
      type: object
      required:
        - attempt
        - status
        - provider_called
        - provider
        - model
        - latency_ms
        - accepted_count
        - rejected_count
        - provider_cost_usd
        - error_code
        - failure_reason
        - started_at
        - finished_at
      properties:
        attempt: { type: integer, minimum: 1, maximum: 4 }
        status:
          type: string
          enum:
            - running
            - succeeded
            - retry
            - failed
            - worker_lost
            - deadline_exceeded
        provider_called: { type: boolean }
        provider: { type: string }
        model: { type: string }
        latency_ms: { type: integer, minimum: 0 }
        accepted_count: { type: integer, minimum: 0, maximum: 20 }
        rejected_count: { type: integer, minimum: 0, maximum: 40 }
        provider_cost_usd: { type: [number, "null"], minimum: 0 }
        error_code: { type: [string, "null"] }
        failure_reason: { type: [string, "null"] }
        started_at: { type: string, format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
      additionalProperties: false

    FormationJobResult:
      type: object
      required:
        - mode
        - terminal_reason
        - candidates
        - memory_ids
        - operation_results
        - operation_counts
        - memory_version
        - primary_wal_lsn
        - accepted_count
        - rejected_count
        - provider
        - model
        - turns
        - provider_attempts
        - provider_latency_ms
        - provider_cost_usd
        - error_code
      properties:
        mode:
          type: string
          enum: [propose, commit]
        terminal_reason:
          $ref: "#/components/schemas/FormationTerminalReason"
        candidates:
          type: array
          maxItems: 60
          items:
            $ref: "#/components/schemas/FormationCandidateReview"
        memory_ids:
          type: array
          maxItems: 20
          items: { type: string }
        operation_results:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/FormationOperationResult" }
        operation_counts:
          $ref: "#/components/schemas/FormationOperationCounts"
        memory_version:
          type: [integer, "null"]
          minimum: 0
        primary_wal_lsn:
          type: [string, "null"]
          pattern: "^[0-9A-Fa-f]+/[0-9A-Fa-f]+$"
        accepted_count: { type: integer, minimum: 0, maximum: 20 }
        rejected_count: { type: integer, minimum: 0, maximum: 40 }
        provider: { type: string }
        model: { type: string }
        turns: { type: integer, minimum: 1, maximum: 100 }
        provider_attempts: { type: integer, minimum: 0, maximum: 3 }
        provider_latency_ms: { type: integer, minimum: 0 }
        provider_cost_usd: { type: [number, "null"], minimum: 0 }
        error_code: { type: [string, "null"] }
      additionalProperties: false

    FormationJob:
      type: object
      required:
        - job_id
        - status
        - mode
        - attempt_count
        - provider_attempts
        - max_attempts
        - max_provider_attempts
        - terminal_reason
        - error_code
        - accepted_count
        - rejected_count
        - provider
        - model
        - provider_latency_ms
        - provider_cost_usd
        - elapsed_ms
        - created_at
        - started_at
        - completed_at
        - attempts
        - result
        - request_id
      properties:
        job_id:
          type: string
          pattern: "^frm_[a-f0-9]{32}$"
        status:
          $ref: "#/components/schemas/FormationJobStatus"
        mode:
          type: string
          enum: [propose, commit]
        attempt_count: { type: integer, minimum: 0, maximum: 4 }
        provider_attempts: { type: integer, minimum: 0, maximum: 3 }
        max_attempts: { type: integer, minimum: 1, maximum: 4 }
        max_provider_attempts: { type: integer, minimum: 1, maximum: 3 }
        terminal_reason:
          oneOf:
            - $ref: "#/components/schemas/FormationTerminalReason"
            - type: "null"
        error_code: { type: [string, "null"] }
        accepted_count: { type: integer, minimum: 0, maximum: 20 }
        rejected_count: { type: integer, minimum: 0, maximum: 40 }
        provider: { type: string }
        model: { type: string }
        provider_latency_ms: { type: integer, minimum: 0 }
        provider_cost_usd: { type: [number, "null"], minimum: 0 }
        elapsed_ms: { type: integer, minimum: 0 }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
        attempts:
          type: array
          maxItems: 4
          items:
            $ref: "#/components/schemas/FormationAttempt"
        result:
          oneOf:
            - $ref: "#/components/schemas/FormationJobResult"
            - type: "null"
        request_id: { type: string }
      additionalProperties: false

    RecallRequest:
      type: object
      required: [user_id, query]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        query: { type: string, minLength: 1, maxLength: 2000 }
        top_k: { type: integer, minimum: 1, maximum: 50, default: 5 }
        entity: { type: string, maxLength: 200 }
        min_confidence: { type: number, minimum: 0, maximum: 1 }
        min_memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        min_primary_wal_lsn:
          type: string
          pattern: "^[0-9A-Fa-f]+/[0-9A-Fa-f]+$"
          description: Minimum primary WAL position the read must include.
        read_consistency:
          type: string
          enum: [default, primary, replica_fallback]
          default: default
          description: |
            `default` routes token-bearing reads to primary and tokenless reads
            to the read pool. `primary` always uses primary.
            `replica_fallback` waits briefly on the read pool, then retries
            against primary.
      additionalProperties: false

    RecallResponse:
      type: object
      required: [context, memories]
      properties:
        context:
          type: string
          description: Injection-guarded rendered context for prompting
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
      additionalProperties: false

    RecallForActionRequest:
      type: object
      required: [user_id, query]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        query: { type: string, minLength: 1, maxLength: 2000 }
        top_k: { type: integer, minimum: 1, maximum: 50, default: 5 }
      additionalProperties: false

    RecallForActionResponse:
      type: object
      required: [decision_id, outcome, memories, pending_confirmation_ids]
      properties:
        decision_id:
          type: string
          format: uuid
          description: Durable receipt target for this action evaluation
        outcome:
          type: string
          enum: [act, ask, abstain]
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
          description: Trusted memories; populated only for an act outcome
        pending_confirmation_ids:
          type: array
          items: { type: string }
          description: Memories that caused an ask outcome
      additionalProperties: false

    ConfirmRequest:
      type: object
      required: [user_id, memory_id]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        memory_id: { type: string, minLength: 1, maxLength: 128 }
      additionalProperties: false

    ConfirmResponse:
      type: object
      required: [memory, memory_version, primary_wal_lsn]
      properties:
        memory:
          $ref: "#/components/schemas/Memory"
        memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        primary_wal_lsn:
          $ref: "#/components/schemas/PrimaryWalLsn"
      additionalProperties: false

    ForgetRequest:
      type: object
      required: [user_id]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        memory_id:
          type: string
          minLength: 1
          maxLength: 128
        entity:
          type: string
          minLength: 1
          maxLength: 200
        attribute:
          type: string
          minLength: 1
          maxLength: 200
        erase_partition:
          type: boolean
          default: false
        confirmation:
          type: string
          maxLength: 128
      additionalProperties: false

    ForgetResponse:
      type: object
      required:
        [mode, deleted, verified, request_id, memory_version, primary_wal_lsn]
      properties:
        mode:
          type: string
          enum: [memory, slot, partition]
        deleted:
          type: integer
          minimum: 0
        verified:
          type: [boolean, "null"]
          description: Non-null only for whole-partition erasure
        request_id:
          type: string
        memory_version:
          $ref: "#/components/schemas/MemoryVersion"
        primary_wal_lsn:
          $ref: "#/components/schemas/PrimaryWalLsn"
      additionalProperties: false

    ExecutionReceiptRequest:
      type: object
      required: [user_id, decision_id, action_name, status]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
        decision_id:
          type: string
          format: uuid
        action_name:
          type: string
          pattern: "^[A-Za-z][A-Za-z0-9._:\\-]{0,127}$"
          description: Stable action category, such as appointment.book
        status:
          type: string
          enum: [succeeded, failed, skipped]
        external_ref:
          type: string
          pattern: "^[A-Za-z0-9._:\\-]{1,128}$"
          description: Optional opaque non-PII identifier in the downstream system
        error_code:
          type: string
          pattern: "^[A-Za-z0-9._:\\-]{1,128}$"
          description: Structured failure code; allowed only when status is failed
      additionalProperties: false

    ExecutionReceipt:
      type: object
      required:
        - id
        - decision_id
        - action_name
        - status
        - policy_alignment
        - external_ref
        - error_code
        - request_id
        - created_at
      properties:
        id: { type: string, format: uuid }
        decision_id: { type: string, format: uuid }
        action_name: { type: string }
        status:
          type: string
          enum: [succeeded, failed, skipped]
        policy_alignment:
          type: string
          enum: [aligned, violated]
          description: Violated when the host attempted execution after ask or abstain
        external_ref:
          type: [string, "null"]
        error_code:
          type: [string, "null"]
        request_id: { type: string }
        created_at: { type: string, format: date-time }
      additionalProperties: false

    ExecutionReceiptResponse:
      type: object
      required: [decision_id, outcome, receipt]
      properties:
        decision_id: { type: string, format: uuid }
        outcome:
          type: string
          enum: [act, ask, abstain]
        receipt:
          $ref: "#/components/schemas/ExecutionReceipt"
      additionalProperties: false

    JsonRpcRequest:
      type: object
      required: [jsonrpc, method]
      properties:
        jsonrpc: { type: string, const: "2.0" }
        id: {}
        method: { type: string }
        params: { type: object }
      additionalProperties: false

    JsonRpcResponse:
      type: object
      required: [jsonrpc]
      properties:
        jsonrpc: { type: string, const: "2.0" }
        id: {}
        result: { type: object }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: integer }
            message: { type: string }
          additionalProperties: false
      additionalProperties: false

    PendingConfirmationsRequest:
      type: object
      required: [user_id]
      properties:
        user_id:
          $ref: "#/components/schemas/UserId"
      additionalProperties: false

    PendingConfirmationsResponse:
      type: object
      required: [memories]
      properties:
        memories:
          type: array
          items: { $ref: "#/components/schemas/Memory" }
      additionalProperties: false
