openapi: 3.1.0
info:
  title: Angareion Agent API
  version: 1.1.0
  description: >
    The agent-facing API for Angareion — autonomous-agent sensing and memory.

    Agents authenticate by exchanging an API key for a short-lived JWT

    (`POST /auth/token`), then call ingest, delivery, memory, and channel

    endpoints with the JWT in `Authorization: Bearer <jwt>`.


    Errors follow the canonical envelope (see `Error` schema). Every response

    carries `X-Request-Id` for support correlation; rate-limited responses

    include `X-RateLimit-*` and `Retry-After`.


    ## Streaming endpoints


    ### GET /messages/stream


    **Path:** `GET /v1/inbox/stream` (prefix `/v1` is in `servers.url`)


    Server-Sent Events stream delivering real-time messages and read-receipt

    notifications to the authenticated caller.


    **Authentication:** Accepts EITHER an `org_session` httponly cookie (set at

    login by the org auth handler) OR `Authorization: Bearer <jwt>` (agent or

    org admin token). Both yield an equivalent `CallerContext`.


    **Events emitted:**


    | event type | payload | trigger |

    |-----------|---------|---------|

    | `message` | Full delivery payload (JSON) | New message routed to caller |

    | `inbox.read.advanced` | `{"channel_id":"…","last_read_delivery_id":"…"}` |
    Caller advanced read pointer (cross-device sync) |


    **Reconnect:** Send `Last-Event-ID: <delivery_id>` to gap-fill missed events

    since disconnect. The server emits a `retry: <N>ms` hint (jittered
    1500–3500ms)

    at connection open.


    **Slow consumer:** If the server-side buffer (cap 100) fills, the connection

    is closed. The client should reconnect with `Last-Event-ID`.


    **Response headers:**

    - `Content-Type: text/event-stream`

    - `Cache-Control: no-cache`

    - `X-Accel-Buffering: no`
  contact:
    name: Angareion Support
    url: https://angareion.com/support
servers:
  - url: https://api.angareion.com/v1
    description: Production
  - url: http://localhost:8080/v1
    description: Local development
security:
  - BearerAuth: []
tags:
  - name: Auth
    description: API key → JWT exchange.
  - name: Events
    description: Publish events into the sensing pipeline.
  - name: Delivery
    description: Poll and acknowledge deliveries.
  - name: DLQ
    description: Dead-letter queue inspection and replay.
  - name: Agents
    description: Agent registry CRUD and lifecycle.
  - name: Agent Keys
    description: Per-agent API key issuance and revocation.
  - name: Interests
    description: Per-agent interest declarations.
  - name: Channels
    description: Multi-participant channels, members, and feeds.
  - name: Messaging
    description: >
      Human messaging facade. All routes accept EITHER an `org_session` httponly

      cookie (browser / SSE) OR `Authorization: Bearer <jwt>` (agent / SDK /
      CLI).

      Both auth paths yield an equivalent `CallerContext` used by all handlers.
  - name: Memories
    description: Memory CRUD, scope promotion, and search.
  - name: Knowledge
    description: >-
      Document ingestion, listing, chunk inspection, citation lookup, and
      deletion.
  - name: Specialists
    description: Specialist preamble load, list, and session unload.
  - name: Compose
    description: LLM synthesis composition for workflow runs.
  - name: Handoffs
    description: |
      Cross-LLM session handoff — named, agent-scoped snapshots of working state
      saved in one LLM client and resumed in another. Every route here is
      agent-scoped and fails closed: a caller without an agent identity in its
      JWT receives 401, including an org-user token.
paths:
  /auth/token:
    post:
      operationId: exchangeToken
      tags:
        - Auth
      summary: Exchange API key for short-lived JWT
      description: >
        Public endpoint. Accepts an API key in the request body and returns a
        1-hour

        JWT. Per PRD-03 §4.2. The API key in the body IS the credential — do not
        send

        an Authorization header.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenExchangeRequest'
            examples:
              standard:
                summary: Exchange a live API key
                value:
                  api_key: ak_live_AbCdEfGhIjKlMnOpQrStUvWxYz0123456
      responses:
        '200':
          description: JWT issued successfully
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenExchangeResponse'
              examples:
                standard:
                  summary: Successful exchange
                  value:
                    access_token: eyJhbGciOiJIUzI1NiIs.eyJzdWIiOiJhZ18.signature
                    token_type: Bearer
                    expires_at: '2026-05-28T15:30:00Z'
                    agent:
                      id: ag_01H7abc12345
                      name: production-classifier
                      team: platform
                      tenant_id: tn_01H7abc12345
        '400':
          description: Invalid request body
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                bad_body:
                  summary: Malformed JSON
                  value:
                    error:
                      code: invalid_key
                      message: invalid request body
                      request_id: req_01H7abc12345
        '401':
          description: API key invalid, expired, revoked, or agent inactive
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_key:
                  summary: >-
                    Key not found or revoked (anti-enumeration: same response
                    for both)
                  value:
                    error:
                      code: invalid_key
                      message: invalid API key
                      request_id: req_01H7abc12345
                key_expired:
                  summary: Key past expires_at
                  value:
                    error:
                      code: key_expired
                      message: API key has expired
                      request_id: req_01H7abc12345
                agent_inactive:
                  summary: Owning agent is not active
                  value:
                    error:
                      code: agent_inactive
                      message: agent is not active
                      request_id: req_01H7abc12345
  /events:
    post:
      operationId: publishEvent
      tags:
        - Events
      summary: Publish a CloudEvents-formatted event
      description: >
        Validates the CloudEvents envelope, deduplicates by `id`, publishes to
        the

        sensing pipeline, and returns 202 Accepted. Duplicates return 200 with

        `status: "duplicate"` and the original event_id. Per PRD-01.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Event'
            examples:
              standard:
                summary: Publish a custom event
                value:
                  specversion: '1.0'
                  type: com.example.order.created
                  source: //orders.example.com
                  id: evt_01H7abc12345
                  time: '2026-05-28T15:00:00Z'
                  datacontenttype: application/json
                  subject: order/12345
                  data:
                    order_id: '12345'
                    amount_cents: 4900
                    currency: USD
      responses:
        '200':
          description: Duplicate event (idempotent replay returns the original ID)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventAcceptedResponse'
              examples:
                duplicate:
                  value:
                    event_id: evt_01H7abc12345
                    status: duplicate
        '202':
          description: Event accepted for processing
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventAcceptedResponse'
              examples:
                accepted:
                  value:
                    event_id: evt_01H7abc12345
                    status: accepted
        '400':
          description: Invalid CloudEvents envelope or missing required field
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_required:
                  value:
                    error:
                      code: validation_error
                      message: type field is required
                      request_id: req_01H7abc12345
        '401':
          description: Missing or invalid Bearer token
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: Upstream publish failed
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                publish_failed:
                  value:
                    error:
                      code: publish_failed
                      message: failed to publish event
                      request_id: req_01H7abc12345
  /delivery/poll:
    get:
      operationId: pollDeliveries
      tags:
        - Delivery
      summary: Poll pending deliveries for the authenticated agent
      description: |
        Returns up to `limit` pending deliveries scoped to the calling agent's
        agent_id (extracted from the JWT). Per PRD-01.
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
          description: Max deliveries to return. Server clamps to 100.
      responses:
        '200':
          description: Pending deliveries (possibly empty)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeliveryListResponse'
              examples:
                with_items:
                  value:
                    deliveries:
                      - id: dlv_01H7abc12345
                        event_id: evt_01H7abc12345
                        payload:
                          type: com.example.order.created
                          data:
                            order_id: '12345'
                        attempt_count: 1
                        created_at: '2026-05-28T15:00:00Z'
                    count: 1
                empty:
                  value:
                    deliveries: []
                    count: 0
        '401':
          description: Missing or invalid Bearer token
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /delivery/ack:
    post:
      operationId: ackDeliveries
      tags:
        - Delivery
      summary: Acknowledge one or more deliveries
      description: |
        Marks the specified deliveries as acknowledged for the calling agent.
        Up to 100 IDs per call. The server enforces agent-id scoping.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AckRequest'
            examples:
              standard:
                value:
                  delivery_ids:
                    - dlv_01H7abc12345
                    - dlv_01H7def67890
      responses:
        '200':
          description: Acknowledgement count
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AckResponse'
              examples:
                standard:
                  value:
                    acknowledged: 2
        '400':
          description: Invalid request (empty or >100 IDs)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                empty:
                  value:
                    error:
                      code: validation_error
                      message: delivery_ids must contain at least 1 ID
                      request_id: req_01H7abc12345
  /delivery/dlq:
    get:
      operationId: listDLQ
      tags:
        - DLQ
      summary: List dead-letter queue items
      description: >
        Returns DLQ items scoped to the calling agent (agent JWT) or to the
        tenant

        (org-admin JWT). The agent SDK only uses the agent-scoped variant.
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: DLQ page
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DLQListResponse'
              examples:
                with_items:
                  value:
                    dlq_items:
                      - id: dlv_01H7abc12345
                        event_id: evt_01H7abc12345
                        agent_id: ag_01H7abc12345
                        attempt_count: 5
                        dlq_reason: max attempts exceeded
                        dlq_at: '2026-05-28T14:55:00Z'
                        created_at: '2026-05-28T14:00:00Z'
                    total: 1
  /delivery/dlq/{id}/replay:
    post:
      operationId: replayDLQItem
      tags:
        - DLQ
      summary: Replay a DLQ item back to pending
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: dlv_01H7abc12345
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Replay accepted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReplayResponse'
              examples:
                replayed:
                  value:
                    status: replayed
                    delivery_id: dlv_01H7abc12345
        '404':
          description: Delivery not found or not in DLQ
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: dlq_item_not_found
                      message: delivery not found or not in DLQ
                      request_id: req_01H7abc12345
  /agents:
    get:
      operationId: listAgents
      tags:
        - Agents
      summary: List agents in the caller's tenant
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: status
          in: query
          schema:
            type: string
            enum:
              - active
              - inactive
      responses:
        '200':
          description: Page of agents
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentListResponse'
              examples:
                with_items:
                  value:
                    agents:
                      - id: ag_01H7abc12345
                        tenant_id: tn_01H7abc12345
                        name: production-classifier
                        status: active
                        delivery_mode: poll
                        team_id: team_01H7abc12345
                        created_at: '2026-05-01T10:00:00Z'
                        updated_at: '2026-05-28T15:00:00Z'
                    total: 1
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: createAgent
      tags:
        - Agents
      summary: Register a new agent
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAgentRequest'
            examples:
              poll_mode:
                value:
                  name: production-classifier
                  description: Classifies incoming order events by priority
                  delivery_mode: poll
                  team_id: team_01H7abc12345
      responses:
        '201':
          description: Agent created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                created:
                  value:
                    id: ag_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    name: production-classifier
                    description: Classifies incoming order events by priority
                    status: active
                    delivery_mode: poll
                    team_id: team_01H7abc12345
                    created_at: '2026-05-28T15:00:00Z'
                    updated_at: '2026-05-28T15:00:00Z'
        '400':
          description: Validation error
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_name:
                  value:
                    error:
                      code: validation_error
                      message: name is required
                      request_id: req_01H7abc12345
  /agents/{id}:
    get:
      operationId: getAgent
      tags:
        - Agents
      summary: Get an agent by id
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
      responses:
        '200':
          description: Agent detail
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                detail:
                  value:
                    id: ag_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    name: production-classifier
                    description: Classifies incoming order events by priority
                    status: active
                    delivery_mode: poll
                    team_id: team_01H7abc12345
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-28T15:00:00Z'
        '404':
          description: Agent not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: agent_not_found
                      message: agent not found
                      request_id: req_01H7abc12345
    patch:
      operationId: updateAgent
      tags:
        - Agents
      summary: Update an agent's metadata or delivery settings
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAgentRequest'
            examples:
              switch_to_push:
                value:
                  delivery_mode: push
                  delivery_endpoint: https://agent.example.com/webhook
      responses:
        '200':
          description: Agent updated
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                updated:
                  value:
                    id: ag_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    name: production-classifier
                    description: Classifies incoming order events by priority
                    status: active
                    delivery_mode: push
                    delivery_endpoint: https://agent.example.com/webhook
                    team_id: team_01H7abc12345
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-28T15:05:00Z'
        '404':
          description: Agent not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /agents/{id}:deactivate:
    post:
      operationId: deactivateAgent
      tags:
        - Agents
      summary: Set agent status to inactive
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Agent deactivated
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    type: string
                    enum:
                      - inactive
              examples:
                done:
                  value:
                    status: inactive
        '404':
          description: Agent not found or not active
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /agents/{id}:reactivate:
    post:
      operationId: reactivateAgent
      tags:
        - Agents
      summary: Set agent status back to active
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Agent reactivated
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    type: string
                    enum:
                      - active
              examples:
                done:
                  value:
                    status: active
        '404':
          description: Agent not found or not inactive
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /agents/{id}/events:
    get:
      operationId: listAgentEvents
      tags:
        - Agents
      summary: List recent delivered events for an agent
      description: |
        Returns the latest N delivered (status=acknowledged) events for the
        specified agent. Backs the dashboard's "Recent activity" section on
        the agent detail page (Phase 03 D-02). Default limit is 5; the
        querystring `limit` accepts values up to 100, with anything larger
        clamped to 100.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 5
          example: 5
      responses:
        '200':
          description: Recent events (may be empty)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentEventListResponse'
              examples:
                with_events:
                  value:
                    events:
                      - event_id: evt_01H7abc12345
                        event_type: deploy.succeeded
                        delivered_at: '2026-06-01T15:30:00Z'
                      - event_id: evt_01H7abc12346
                        event_type: alert.fired
                        delivered_at: '2026-06-01T15:25:00Z'
                empty:
                  value:
                    events: []
        '404':
          description: Agent not found in the requesting tenant
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: agent_not_found
                      message: agent not found
                      request_id: req_01H7abc12345
  /agents/{id}/test-event:
    post:
      operationId: postAgentTestEvent
      tags:
        - Agents
      summary: Send a synthetic dashboard test event to an agent
      description: |
        Manufactures a synthetic CloudEvent server-side
        (`type=test.dashboard`, `source=dashboard.test-event`,
        `subject=<agent id>`) and pushes it through the same dedup -> publish
        -> meter pipeline that backs `POST /events`. Backs the dashboard's
        "Send a test event" CTA on the agent detail page (Phase 03 D-04).
        Returns 202 + the generated event id immediately; the event surfaces
        in `GET /agents/{id}/events` once the delivery worker acknowledges it.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Test event accepted for asynchronous delivery
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - event_id
                properties:
                  event_id:
                    type: string
                    description: Server-generated CloudEvent id (`evt_dash_` prefix).
              examples:
                accepted:
                  value:
                    event_id: evt_dash_AbCdEfGhIjKlMnOpQrStUvWx
        '404':
          description: Agent not found in the requesting tenant
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: agent_not_found
                      message: agent not found
                      request_id: req_01H7abc12345
  /agents/{id}/keys:
    get:
      operationId: listAgentKeys
      tags:
        - Agent Keys
      summary: List API keys for an agent (no plaintext, no hash)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Keys (without plaintext or hash)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentKeyListResponse'
              examples:
                with_keys:
                  value:
                    keys:
                      - id: key_01H7abc12345
                        prefix: ak_live_AbCdEfGh
                        scopes:
                          - send
                          - receive
                        last_used_at: '2026-05-28T14:55:00Z'
                        created_at: '2026-05-01T10:00:00Z'
    post:
      operationId: createAgentKey
      tags:
        - Agent Keys
      summary: Mint a new API key (plaintext returned ONCE)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAgentKeyRequest'
            examples:
              standard_scopes:
                value:
                  scopes:
                    - send
                    - receive
                    - memory:read
                    - memory:write
                  expires_at: '2027-05-28T00:00:00Z'
      responses:
        '201':
          description: >
            Key created. The `key` field is the plaintext API key — store it
            securely.

            This is the ONLY time the plaintext is returned.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedAgentKey'
              examples:
                created:
                  value:
                    id: key_01H7abc12345
                    prefix: ak_live_AbCdEfGh
                    key: ak_live_AbCdEfGhIjKlMnOpQrStUvWxYz0123456
                    scopes:
                      - send
                      - receive
                      - memory:read
                      - memory:write
                    expires_at: '2027-05-28T00:00:00Z'
                    created_at: '2026-05-28T15:00:00Z'
        '400':
          description: Invalid scopes
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_scope:
                  value:
                    error:
                      code: invalid_scopes
                      message: 'invalid scope: bogus'
                      request_id: req_01H7abc12345
        '409':
          description: Maximum 5 active keys per agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                max_keys:
                  value:
                    error:
                      code: max_keys_reached
                      message: maximum of 5 active keys per agent
                      request_id: req_01H7abc12345
  /agents/{id}/keys/{key_id}:
    delete:
      operationId: revokeAgentKey
      tags:
        - Agent Keys
      summary: Revoke an agent's API key
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: key_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Key revoked
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '404':
          description: Key not found or already revoked
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: key_not_found
                      message: key not found or already revoked
                      request_id: req_01H7abc12345
  /agents/{id}/interests:
    get:
      operationId: listInterests
      tags:
        - Interests
      summary: List interest declarations for an agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Interests (no pagination)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterestListResponse'
              examples:
                with_items:
                  value:
                    interests:
                      - id: int_01H7abc12345
                        tenant_id: tn_01H7abc12345
                        agent_id: ag_01H7abc12345
                        event_type: com.example.order.*
                        filter_expression: amount_cents > 10000
                        priority: 10
                        enrichment_depth: light
                        active: true
                        created_at: '2026-05-01T10:00:00Z'
                        updated_at: '2026-05-01T10:00:00Z'
        '403':
          description: Caller is an agent attempting to manage another agent's interests
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                cross_agent:
                  value:
                    error:
                      code: forbidden
                      message: agents can only manage their own interests
                      request_id: req_01H7abc12345
    post:
      operationId: createInterest
      tags:
        - Interests
      summary: Create an interest declaration for an agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInterestRequest'
            examples:
              standard:
                value:
                  event_type: com.example.order.*
                  filter_expression: amount_cents > 10000
                  priority: 10
                  enrichment_depth: light
      responses:
        '201':
          description: Interest created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Interest'
              examples:
                created:
                  value:
                    id: int_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    agent_id: ag_01H7abc12345
                    event_type: com.example.order.*
                    filter_expression: amount_cents > 10000
                    priority: 10
                    enrichment_depth: light
                    active: true
                    created_at: '2026-05-28T15:00:00Z'
                    updated_at: '2026-05-28T15:00:00Z'
        '400':
          description: Invalid glob pattern or enrichment_depth
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                bad_pattern:
                  value:
                    error:
                      code: invalid_pattern
                      message: glob pattern not allowed
                      request_id: req_01H7abc12345
        '409':
          description: >-
            An interest with the same event_type already exists for this agent
            (Phase 03 D-14)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                duplicate:
                  value:
                    error:
                      code: interest_already_exists
                      message: an interest with this event_type already exists
                      request_id: req_01H7abc12345
  /agents/{id}/interests/{interest_id}:
    delete:
      operationId: deleteInterest
      tags:
        - Interests
      summary: Delete an interest declaration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: interest_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Interest deleted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '404':
          description: Interest not found or already inactive
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels:
    get:
      operationId: listChannels
      x-mcp-tool:
        name: channels_list
        title: List channels
        args-type: ChannelsListArgs
      tags:
        - Channels
      summary: List channels in the caller's tenant
      description: >
        Returns all channels in the caller's tenant scope. Use offset/limit for
        pagination.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: Channels page
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelListResponse'
              examples:
                with_items:
                  value:
                    channels:
                      - id: ch_01H7abc12345
                        tenant_id: tn_01H7abc12345
                        name: ops-alerts
                        description: Operational alerts for production
                        type: open
                        created_by_type: agent
                        created_by_id: ag_01H7abc12345
                        created_at: '2026-05-01T10:00:00Z'
                        updated_at: '2026-05-01T10:00:00Z'
                    total: 1
    post:
      operationId: createChannel
      x-mcp-tool:
        name: channels_create
        title: Create a channel
        args-type: CreateChannelArgs
      tags:
        - Channels
      summary: Create a channel
      description: >
        Creates a new channel in the caller's tenant. Channel names must be
        unique within the tenant.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChannelRequest'
            examples:
              open_channel:
                value:
                  name: ops-alerts
                  description: Operational alerts for production
                  type: open
      responses:
        '201':
          description: Channel created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                created:
                  value:
                    id: ch_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    name: ops-alerts
                    description: Operational alerts for production
                    type: open
                    created_by_type: agent
                    created_by_id: ag_01H7abc12345
                    created_at: '2026-05-28T15:00:00Z'
                    updated_at: '2026-05-28T15:00:00Z'
        '409':
          description: Channel name already in use
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                taken:
                  value:
                    error:
                      code: channel_name_taken
                      message: channel name already exists in this tenant
                      request_id: req_01H7abc12345
  /channels/{id}:
    get:
      operationId: getChannel
      tags:
        - Channels
      summary: Get a channel by id
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Channel detail
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                detail:
                  value:
                    id: ch_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    name: ops-alerts
                    description: Operational alerts for production
                    type: open
                    created_by_type: agent
                    created_by_id: ag_01H7abc12345
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-01T10:00:00Z'
        '404':
          description: Channel not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{id}/members:
    get:
      operationId: listChannelMembers
      tags:
        - Channels
      summary: List members of a channel
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Members (no pagination)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelMemberListResponse'
              examples:
                with_members:
                  value:
                    members:
                      - id: mbr_01H7abc12345
                        channel_id: ch_01H7abc12345
                        participant_type: agent
                        participant_id: ag_01H7abc12345
                        role: owner
                        joined_at: '2026-05-01T10:00:00Z'
    post:
      operationId: addChannelMember
      tags:
        - Channels
      summary: Add a member to a channel
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddMemberRequest'
            examples:
              add_agent:
                value:
                  participant_type: agent
                  participant_id: ag_01H7def67890
                  role: member
      responses:
        '201':
          description: Member added
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelMember'
              examples:
                added:
                  value:
                    id: mbr_01H7def67890
                    channel_id: ch_01H7abc12345
                    participant_type: agent
                    participant_id: ag_01H7def67890
                    role: member
                    joined_at: '2026-05-28T15:00:00Z'
        '409':
          description: Already a member
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                already:
                  value:
                    error:
                      code: already_member
                      message: participant is already a member of this channel
                      request_id: req_01H7abc12345
  /channels/{id}/members/{participant_type}/{participant_id}:
    delete:
      operationId: removeChannelMember
      tags:
        - Channels
      summary: Remove a member from a channel
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: participant_type
          in: path
          required: true
          schema:
            type: string
            enum:
              - agent
              - org_admin
        - name: participant_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Member removed
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '404':
          description: Member not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{id}/feeds:
    get:
      operationId: listChannelFeeds
      tags:
        - Channels
      summary: List feeds (event subscriptions) for a channel
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Feeds (no pagination)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelFeedListResponse'
              examples:
                with_feeds:
                  value:
                    feeds:
                      - id: feed_01H7abc12345
                        channel_id: ch_01H7abc12345
                        event_type_glob: com.example.order.*
                        filter_expression: amount_cents > 10000
                        active: true
                        created_at: '2026-05-01T10:00:00Z'
    post:
      operationId: createChannelFeed
      tags:
        - Channels
      summary: Subscribe a channel to an event glob (create feed)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFeedRequest'
            examples:
              orders_only:
                value:
                  event_type_glob: com.example.order.*
                  filter_expression: amount_cents > 10000
      responses:
        '201':
          description: Feed created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelFeed'
              examples:
                created:
                  value:
                    id: feed_01H7abc12345
                    channel_id: ch_01H7abc12345
                    event_type_glob: com.example.order.*
                    filter_expression: amount_cents > 10000
                    active: true
                    created_at: '2026-05-28T15:00:00Z'
        '400':
          description: Invalid glob pattern
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{id}/feeds/{feed_id}:
    delete:
      operationId: deleteChannelFeed
      tags:
        - Channels
      summary: Delete a channel feed
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: feed_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Feed deleted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '404':
          description: Feed not found or already inactive
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories:
    get:
      operationId: listMemories
      tags:
        - Memories
      summary: List memories accessible to the caller
      x-mcp-tool:
        name: memory_list
        title: List memories
        args-type: ListMemoriesArgs
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: type
          in: query
          schema:
            type: string
            enum:
              - episodic
              - semantic
              - procedural
              - entity
              - reflection
              - reasoning
        - name: scope
          in: query
          schema:
            type: string
            enum:
              - agent
              - team
              - institutional
        - name: search
          in: query
          schema:
            type: string
          description: Substring match against title and content (handler-side LIKE).
      responses:
        '200':
          description: Memories page
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryListResponse'
              examples:
                with_items:
                  value:
                    memories:
                      - id: mem_01H7abc12345
                        tenant_id: tn_01H7abc12345
                        agent_id: ag_01H7abc12345
                        scope: agent
                        type: semantic
                        status: active
                        title: Customer prefers async support
                        content: >-
                          Customer cs_42 historically responds to email within
                          6h...
                        confidence: 0.85
                        importance: 0.6
                        metadata: {}
                        created_at: '2026-05-01T10:00:00Z'
                        updated_at: '2026-05-01T10:00:00Z'
                        accessed_at: '2026-05-28T14:00:00Z'
                    total: 1
    post:
      operationId: createMemory
      tags:
        - Memories
      summary: Create a memory
      x-mcp-tool:
        name: memory_create
        title: Create a memory
        args-type: CreateMemoryArgs
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMemoryRequest'
            examples:
              semantic:
                value:
                  title: Customer prefers async support
                  content: Customer cs_42 historically responds to email within 6h...
                  type: semantic
                  scope: agent
                  confidence: 0.85
                  importance: 0.6
                  source_event_id: evt_01H7abc12345
      responses:
        '201':
          description: Memory created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
              examples:
                created:
                  value:
                    id: mem_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    agent_id: ag_01H7abc12345
                    scope: agent
                    type: semantic
                    status: active
                    title: Customer prefers async support
                    content: Customer cs_42 historically responds to email within 6h...
                    confidence: 0.85
                    importance: 0.6
                    metadata: {}
                    source_event_id: evt_01H7abc12345
                    created_at: '2026-05-28T15:00:00Z'
                    updated_at: '2026-05-28T15:00:00Z'
                    accessed_at: '2026-05-28T15:00:00Z'
        '400':
          description: Validation error (e.g., invalid type, missing title, content >32KB)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                bad_type:
                  value:
                    error:
                      code: validation_error
                      message: >-
                        type must be one of: episodic, semantic, procedural,
                        entity, reflection, reasoning
                      request_id: req_01H7abc12345
  /memories/health:
    get:
      operationId: getMemoryHealth
      tags:
        - Memories
      summary: Memory-plane health metrics for the caller's tenant
      description: >-
        Returns a read-only, tenant-scoped health snapshot of the memory plane:
        duplicate_count (open merge proposals), orphan_count (memories with no
        incoming MENTIONS edge), stale_count (below the decay/archive floor),
        embed_gap_count (active rows missing a vector), reflection_count, and a
        decay_distribution histogram over memory importance. Read-only — it
        never blocks concurrent writes.
      x-mcp-tool:
        name: memory_health
        title: Memory-plane health metrics
        args-type: MemoryHealthArgs
      responses:
        '200':
          description: Memory-plane health snapshot
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryHealthResponse'
              examples:
                seeded:
                  value:
                    duplicate_count: 3
                    orphan_count: 4
                    stale_count: 2
                    embed_gap_count: 5
                    reflection_count: 6
                    decay_distribution:
                      '1': 5
                      '10': 4
                empty:
                  value:
                    duplicate_count: 0
                    orphan_count: 0
                    stale_count: 0
                    embed_gap_count: 0
                    reflection_count: 0
                    decay_distribution: {}
  /memories/{id}:
    get:
      operationId: getMemory
      tags:
        - Memories
      summary: Get a memory by id
      x-mcp-tool:
        name: memory_get
        title: Get a memory
        args-type: GetMemoryArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: mem_01H7abc12345
      responses:
        '200':
          description: Memory detail
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
              examples:
                detail:
                  value:
                    id: mem_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    agent_id: ag_01H7abc12345
                    scope: agent
                    type: semantic
                    status: active
                    title: Customer prefers async support
                    content: Customer cs_42 historically responds to email within 6h...
                    confidence: 0.85
                    importance: 0.6
                    metadata: {}
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-01T10:00:00Z'
                    accessed_at: '2026-05-28T14:00:00Z'
        '400':
          description: Invalid memory ID format
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: memory_not_found
                      message: memory not found
                      request_id: req_01H7abc12345
    patch:
      operationId: updateMemory
      tags:
        - Memories
      summary: Update mutable fields of a memory
      x-mcp-tool:
        name: memory_update
        title: Update a memory
        args-type: UpdateMemoryArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateMemoryRequest'
            examples:
              update_confidence:
                value:
                  confidence: 0.9
                  importance: 0.7
      responses:
        '200':
          description: Memory updated
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
              examples:
                updated:
                  value:
                    id: mem_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    agent_id: ag_01H7abc12345
                    scope: agent
                    type: semantic
                    status: active
                    title: Customer prefers async support
                    content: Customer cs_42 historically responds to email within 6h...
                    confidence: 0.9
                    importance: 0.7
                    metadata: {}
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-28T15:05:00Z'
                    accessed_at: '2026-05-28T15:05:00Z'
        '400':
          description: Validation error (no fields supplied, content >32KB, etc.)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: deleteMemory
      tags:
        - Memories
      summary: Soft-delete a memory
      x-mcp-tool:
        name: memory_delete
        title: Soft-delete a memory
        args-type: DeleteMemoryArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Memory soft-deleted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories/search:
    post:
      operationId: searchMemories
      tags:
        - Memories
      summary: Semantic search across memories
      x-mcp-tool:
        name: memory_search
        title: Search memories (hybrid)
        args-type: SearchMemoriesArgs
      description: >
        Per PRD-02. Performs scope-aware semantic search across the caller's

        accessible memories (agent → team → institutional ascending). Results
        are

        ranked by composite score combining semantic similarity, confidence, and

        recency. The exact ranking signals may evolve; the request/response keys

        documented here are stable.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MemorySearchRequest'
            examples:
              standard:
                value:
                  query: customer support preferences
                  scope: agent
                  memory_types:
                    - semantic
                  limit: 10
                  min_score: 0.5
      responses:
        '200':
          description: Ranked search results
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemorySearchResponse'
              examples:
                with_results:
                  value:
                    results:
                      - memory:
                          id: mem_01H7abc12345
                          tenant_id: tn_01H7abc12345
                          agent_id: ag_01H7abc12345
                          scope: agent
                          type: semantic
                          status: active
                          title: Customer prefers async support
                          content: >-
                            Customer cs_42 historically responds to email within
                            6h...
                          confidence: 0.85
                          importance: 0.6
                          metadata: {}
                          created_at: '2026-05-01T10:00:00Z'
                          updated_at: '2026-05-01T10:00:00Z'
                          accessed_at: '2026-05-28T14:00:00Z'
                        score: 0.92
                    total: 1
        '400':
          description: Invalid search request (e.g., empty query)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories/{id}/relationships:
    post:
      operationId: createMemoryRelationship
      tags:
        - Memories
      summary: Create a directed relationship between two memories
      x-mcp-tool:
        name: memory_relate
        title: Create a memory relationship
        args-type: RelateMemoriesArgs
      description: >
        Creates a directed relationship from the memory in the path (`id`) to

        another memory (`target_memory_id`). The server enforces a closed enum

        for `relationship_type` — values outside the enum are rejected with

        HTTP 400 `validation_error`. Backed by
        `MemoryHandler.CreateRelationship`

        in `api/internal/handlers/memory.go`.


        Valid `relationship_type` values:
          - caused_by
          - related_to
          - contradicts
          - supports
          - derived_from
          - part_of

        `strength` and `metadata` are optional. If `strength` is omitted the

        server applies a default; if `metadata` is omitted it is stored as null.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Source memory id.
          example: mem_01H7abc12345
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMemoryRelationshipRequest'
            examples:
              derived_from_with_strength:
                summary: Mark target as the source this memory was derived from
                value:
                  target_memory_id: mem_neighbor1234
                  relationship_type: derived_from
                  strength: 0.85
                  metadata:
                    confidence: 0.9
                    source: manual
              minimal_caused_by:
                summary: Minimal payload (only required fields)
                value:
                  target_memory_id: mem_neighbor1234
                  relationship_type: caused_by
      responses:
        '201':
          description: Relationship created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryRelationship'
              examples:
                created:
                  summary: Newly created relationship
                  value:
                    id: rel_01H7abc12345
                    tenant_id: ten_01H7abc12345
                    source_memory_id: mem_01H7abc12345
                    target_memory_id: mem_neighbor1234
                    relationship_type: derived_from
                    strength: 0.85
                    metadata:
                      confidence: 0.9
                      source: manual
                    created_at: '2026-06-01T12:34:56Z'
        '400':
          description: >-
            Validation error (e.g., missing target_memory_id, invalid
            relationship_type)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Source or target memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Relationship already exists
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Semantic constraint violated (e.g., self-referential relationship)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      operationId: listMemoryRelationships
      tags:
        - Memories
      summary: List relationships for a memory (graph traversal)
      x-mcp-tool:
        name: memory_relationships
        title: List memory relationships (BFS)
        args-type: ListRelationshipsArgs
      description: >
        Performs a breadth-first traversal of the memory graph starting at `id`

        and returns each reached memory along with its hop depth and the

        cumulative product of edge strengths along the traversal path.


        Backed by `MemoryHandler.ListRelationships` in

        `api/internal/handlers/memory.go`. Returns the items the caller's tenant

        is permitted to see; tenant scoping is enforced server-side.


        The `count` field is the number of items returned, not a paginated
        total.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Source memory id (UUID).
          example: mem_01H7abc12345
        - name: depth
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 3
            default: 1
          description: |
            Number of relationship hops to traverse. Server clamps to [1, 3];
            values <= 0 default to 1, values > 3 are capped at 3.
          example: 1
      responses:
        '200':
          description: Traversal result (zero or more reachable memories)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryRelationshipListResponse'
              examples:
                with_items:
                  value:
                    relationships:
                      - memory_id: mem_neighbor1234
                        depth: 1
                        strength_product: 0.85
                      - memory_id: mem_far_neighb56
                        depth: 2
                        strength_product: 0.42
                    count: 2
                empty:
                  value:
                    relationships: []
                    count: 0
        '400':
          description: Validation error (e.g., id is not a UUID)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_id:
                  value:
                    error:
                      code: validation_error
                      message: invalid memory ID format
                      request_id: req_01H7abc12345
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unauthorized:
                  value:
                    error:
                      code: authentication_failed
                      message: missing or invalid bearer token
                      request_id: req_01H7abc12345
        '501':
          description: Relationship API not available on this deployment
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                disabled:
                  value:
                    error:
                      code: service_unavailable
                      message: relationship API not available
                      request_id: req_01H7abc12345
  /memories/{id}/relationships/{rel_id}:
    delete:
      operationId: deleteMemoryRelationship
      tags:
        - Memories
      summary: Delete a memory relationship
      x-mcp-tool:
        name: memory_unrelate
        title: Delete a memory relationship
        args-type: UnrelateMemoriesArgs
      description: |
        Removes a single directed relationship by its server-assigned id.
        Backed by `MemoryHandler.DeleteRelationship` in
        `api/internal/handlers/memory.go`. Tenant scoping is enforced
        server-side; the caller must be authenticated as an agent in the
        owning tenant.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Source memory id (UUID).
          example: mem_01H7abc12345
        - name: rel_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Relationship id returned by createMemoryRelationship.
          example: rel_01H7abc12345
          x-go-json-name: relationship_id
      responses:
        '204':
          description: Relationship deleted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '400':
          description: Validation error (id or rel_id is not a UUID)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_rel_id:
                  value:
                    error:
                      code: validation_error
                      message: invalid relationship ID format
                      request_id: req_01H7abc12345
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unauthorized:
                  value:
                    error:
                      code: authentication_failed
                      message: missing or invalid bearer token
                      request_id: req_01H7abc12345
        '404':
          description: Relationship not found (or already deleted)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: relationship not found
                      request_id: req_01H7abc12345
        '501':
          description: Relationship API not available on this deployment
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                disabled:
                  value:
                    error:
                      code: service_unavailable
                      message: relationship API not available
                      request_id: req_01H7abc12345
  /memories/{id}/citations:
    get:
      operationId: listMemoryCitations
      tags:
        - Memories
      summary: List citations for a memory
      description: >
        Returns the Citation nodes linked to this memory via `CITES` edges in

        Neo4j (Phase 03 read path). Citations are written by the extraction

        pipeline when source documents are ingested (Phase 02 EXT-01).


        Returns an empty `citations` array (with HTTP 200) for memories created

        manually without a source document — this is NOT an error condition.


        Backed by `SearchHandler.Citations` in `api/internal/search/handler.go`,

        which calls the memory-bridge gRPC `GetMemoryCitations` RPC with a 2s

        deadline and falls back to Postgres lineage if the bridge is
        unavailable.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Memory UUID.
          example: mem_01H7abc12345
      responses:
        '200':
          description: Citations list (may be empty for memories without source documents).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryCitationListResponse'
              examples:
                with_citations:
                  value:
                    citations:
                      - document_id: ''
                        document_name: Angareion Architecture Guide
                        page: 4
                        snippet: The sensing pipeline processes CloudEvents...
                        source_uri: https://storage.example.com/docs/guide.pdf#chunk=42
                        source_id: ''
                        confidence: 0.85
                        version: 1
                        freshness:
                          stale: false
                          stale_since: null
                    total: 1
                empty:
                  value:
                    citations: []
                    total: 0
        '400':
          description: Invalid memory ID format.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized — missing or invalid bearer token.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Memory not found.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                not_found:
                  value:
                    error:
                      code: memory_not_found
                      message: memory not found
                      request_id: req_01H7abc12345
  /memories/{id}/promote:
    post:
      operationId: promoteMemory
      tags:
        - Memories
      summary: Promote a memory's scope (agent → team or institutional)
      x-mcp-tool:
        name: memory_promote
        title: Promote a memory's scope
        args-type: PromoteMemoryArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromoteMemoryRequest'
            examples:
              to_team:
                value:
                  scope: team
                  team_id: team_01H7abc12345
              to_institutional:
                value:
                  scope: institutional
      responses:
        '200':
          description: Memory promoted (returns the updated memory)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Memory'
              examples:
                promoted_to_team:
                  value:
                    id: mem_01H7abc12345
                    tenant_id: tn_01H7abc12345
                    agent_id: ag_01H7abc12345
                    scope: team
                    team_id: team_01H7abc12345
                    type: semantic
                    status: active
                    title: Customer prefers async support
                    content: Customer cs_42 historically responds to email within 6h...
                    confidence: 0.85
                    importance: 0.6
                    metadata: {}
                    created_at: '2026-05-01T10:00:00Z'
                    updated_at: '2026-05-28T15:10:00Z'
                    accessed_at: '2026-05-28T15:10:00Z'
        '400':
          description: Validation error (e.g., team_id missing for team scope)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_team_id:
                  value:
                    error:
                      code: validation_error
                      message: team_id required for team scope promotion
                      request_id: req_01H7abc12345
        '403':
          description: Promotion not allowed (e.g., agent does not own the memory)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                forbidden:
                  value:
                    error:
                      code: forbidden
                      message: promotion not allowed
                      request_id: req_01H7abc12345
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /merge-proposals:
    get:
      operationId: listOpenMergeProposals
      tags:
        - Memories
      summary: List open merge proposals for the caller's tenant
      x-mcp-tool:
        name: memory_review_proposals
        title: Review open merge proposals
        args-type: MemoryReviewProposalsArgs
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: Open merge proposals page
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergeProposalListResponse'
              examples:
                empty:
                  value:
                    proposals: []
                    total: 0
    post:
      operationId: postMergeProposal
      tags:
        - Memories
      summary: Propose merging two duplicate memories
      x-mcp-tool:
        name: memory_merge_propose
        title: Propose a memory merge
        args-type: MemoryMergeProposeArgs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMergeProposalRequest'
            examples:
              propose:
                value:
                  memory_a_id: mem_01H7abc12345
                  memory_b_id: mem_01H7def67890
                  resolution_note: Both describe the same customer preference
      responses:
        '201':
          description: Merge proposal created
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergeProposal'
        '400':
          description: Validation error (invalid ids, or same memory on both sides)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: One of the referenced memories was not found in the tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /merge-proposals/{id}:resolve:
    post:
      operationId: resolveMergeProposal
      tags:
        - Memories
      summary: Resolve a merge proposal (org-admin only)
      description: >-
        Resolves an open merge proposal, keeping surviving_memory_id and
        collapsing the other. Org-admin only — enforced at the REST layer
        (D-08). A non-admin caller receives 403, surfaced here as a tool error.
      x-mcp-tool:
        name: memory_merge_resolve
        title: Resolve a merge proposal
        args-type: MemoryMergeResolveArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResolveMergeProposalRequest'
            examples:
              resolve:
                value:
                  surviving_memory_id: mem_01H7abc12345
                  resolution_note: Kept the more complete record
      responses:
        '200':
          description: Merge proposal resolved
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergeProposal'
        '400':
          description: Validation error (invalid or non-matching surviving_memory_id)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Caller is not an org-admin (single enforcement point, D-08)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Merge proposal not found in the tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /merge-proposals/{id}:reject:
    post:
      operationId: rejectMergeProposal
      tags:
        - Memories
      summary: Reject a merge proposal (org-admin only)
      description: >-
        Rejects an open merge proposal without collapsing either memory.
        Org-admin only — enforced at the REST layer (D-08). A non-admin caller
        receives 403, surfaced here as a tool error.
      x-mcp-tool:
        name: memory_merge_reject
        title: Reject a merge proposal
        args-type: MemoryMergeRejectArgs
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RejectMergeProposalRequest'
            examples:
              reject:
                value:
                  resolution_note: Not actually duplicates
      responses:
        '200':
          description: Merge proposal rejected
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergeProposal'
        '403':
          description: Caller is not an org-admin (single enforcement point, D-08)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Merge proposal not found in the tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /messages:
    post:
      operationId: sendMessage
      x-mcp-tool:
        name: messages_send
        title: Send a message
        args-type: SendMessageArgs
      tags:
        - Messaging
      summary: Send a message to a channel
      description: >
        Publishes a message to the specified channel. The caller must be a
        member

        of the channel. Produces a CloudEvents 1.0 envelope on `angareion.raw`

        with `actortype`/`actorid` extensions from the authenticated caller
        identity.

        Mention IDs are validated as channel members in the same transaction.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            examples:
              basic:
                value:
                  channel: '#general'
                  text: Hello, team!
              with_mentions:
                value:
                  channel: '#general'
                  text: Hey @agent-001, take a look
                  mentions:
                    - type: agent
                      id: ag_01H7abc12345
      responses:
        '202':
          description: Message accepted for delivery
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Mentions include non-members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvalidMentionsError'
        '502':
          description: Kafka publish failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /dm/{participant_ref}:
    get:
      operationId: resolveDM
      tags:
        - Messaging
      summary: Find or create a DM channel (without sending a message)
      description: >
        Finds or creates the direct channel between the authenticated caller and

        the target participant, returning the `channel_id`. No message is
        inserted.

        Idempotent: repeated calls with the same peer return the same channel
        ID.

        Use this to resolve the DM channel before the first message is typed.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: participant_ref
          in: path
          required: true
          description: |
            Target participant reference. Format: `{type}:{id}` where type is
            `agent` or `org_admin`. Example: `agent:ag_01H7abc12345`.
          schema:
            type: string
            pattern: ^(agent|org_admin):[a-zA-Z0-9_-]+$
            example: agent:ag_01H7abc12345
      responses:
        '200':
          description: DM channel resolved
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolveDMResponse'
        '400':
          description: Invalid participant_ref format or non-UUID peer ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: sendDM
      x-mcp-tool:
        name: dm_send
        title: Send a direct message
        args-type: SendDMArgs
      tags:
        - Messaging
      summary: Send a direct message (find-or-create DM channel)
      description: >
        Atomically finds or creates a direct channel between the caller and the

        target participant, then sends a message. For self-DM (participant_ref
        ==

        caller's own ID), creates a `type=self` channel. For 2-party DM, creates
        a

        `type=direct` channel named `direct:{sortedA}:{sortedB}`. 3+
        participants

        return 422.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: participant_ref
          in: path
          required: true
          description: |
            Target participant reference. Format: `{type}:{id}` where type is
            `agent` or `org_admin`. Example: `agent:ag_01H7abc12345`.
          schema:
            type: string
            pattern: ^(agent|org_admin):[a-zA-Z0-9_-]+$
            example: agent:ag_01H7abc12345
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendDMRequest'
            examples:
              basic:
                value:
                  text: Hey, quick question
      responses:
        '202':
          description: DM accepted for delivery
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: 3+ participant DM or invalid participant_ref
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{id}/messages:
    get:
      operationId: listChannelMessages
      tags:
        - Messaging
      summary: Keyset-paginated message scrollback for a channel
      description: >
        Returns messages in the channel, most recent first. The caller must be a

        channel member — non-members receive 404 (no existence leak). Uses
        keyset

        pagination via an opaque `cursor` string encoding `(delivery_id,
        created_at)`.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: ch_01H7abc12345
        - name: cursor
          in: query
          required: false
          description: Opaque cursor from a previous response `next_cursor` field.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: Messages page
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageListResponse'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Channel not found or caller is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /inbox:
    get:
      operationId: getInbox
      x-mcp-tool:
        name: inbox_list
        title: List inbox
        args-type: InboxListArgs
      tags:
        - Messaging
      summary: List channels the caller is a member of, with unread counts
      description: >
        Returns all channels where the caller is a participant, sorted by last

        message time descending. Each item includes a `last_message` preview and

        `unread_count` derived from `message_read_state`.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: unread_only
          in: query
          required: false
          schema:
            type: boolean
          description: >-
            When true, client-side filter returns only items with unread_count >
            0.
      responses:
        '200':
          description: Inbox items
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxResponse'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /messages/{id}/read:
    post:
      operationId: markMessageRead
      x-mcp-tool:
        name: inbox_read
        title: Mark message read
        args-type: MarkReadArgs
      tags:
        - Messaging
      summary: Advance the read pointer to a delivery
      description: >
        Monotone-forward only: if the specified delivery_id is older than the

        current read pointer, this is a no-op. On success emits an

        `inbox.read.advanced` SSE event to all sessions of the same caller.

        Tool errors are returned as `{error_code, message, hint?}` so MCP
        clients can surface user-friendly retry guidance (e.g., for
        `invalid_mentions`).
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          x-go-json-name: delivery_id
          description: delivery_id (UUID) of the message to mark read
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '204':
          description: Read pointer advanced (or already past this message — no-op)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Delivery not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{id}/mentions:
    get:
      operationId: listChannelMentions
      tags:
        - Messaging
      summary: List mention candidates (channel members only)
      description: >
        Returns members of the specified channel whose name or ID matches `q`.

        Scoped to channel members only — never tenant-wide. Used by mention
        pickers

        in chat UI and SDK tooling.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: ch_01H7abc12345
        - name: q
          in: query
          required: false
          description: Prefix search string for member name/ID
          schema:
            type: string
            maxLength: 100
      responses:
        '200':
          description: Mention candidates
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MentionCandidateListResponse'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Channel not found or caller is not a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /agents/{id}/sessions:
    get:
      operationId: listAgentSessions
      tags:
        - Agents
      summary: List sessions for an agent
      description: |
        Returns all sessions created for the agent, ordered most-recent first.
        Session state is derived server-side from last_seen_at: `active` if
        last seen within 24h, `idle` otherwise. No `revoked` state in v1.0
        (revocation is deferred to v1.1 per REQUIREMENTS.md TS-12).
        Limit defaults to 20; max 100.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Session list
          content:
            application/json:
              schema:
                type: object
                required:
                  - sessions
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: '#/components/schemas/AgentSessionItem'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
  /agents/{id}/sessions:issue:
    post:
      operationId: deviceFlowIssue
      tags:
        - Agents
      summary: Issue a device-flow user_code for an agent
      description: |
        Issues a fresh `XXXX-XXXX` user_code (Crockford-32, ~38 bits entropy)
        with a 10-minute TTL. Supersedes any prior `pending` code for the same
        agent_id (the prior code transitions to `expired`). PROTO-01.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceFlowIssueRequest'
            examples:
              standard:
                value: {}
      responses:
        '200':
          description: User code issued
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowIssueResponse'
              examples:
                standard:
                  summary: >-
                    Successful issue (see
                    tests/fixtures/device-flow-v1/issue-response.json)
                  value:
                    user_code: BCDF-GHJK
                    expires_at: '2026-06-10T18:10:00Z'
                    server_now: '2026-06-10T18:00:00Z'
                    poll_url: >-
                      https://api.angareion.dev/v1/agents/ag_01H7abc12345/sessions:poll
        '429':
          description: Rate limit exceeded (`slow_down`)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Retry-After:
              schema:
                type: integer
              description: Seconds before the next attempt is allowed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
              examples:
                slow_down:
                  summary: See tests/fixtures/device-flow-v1/error-slow_down.json
                  value:
                    error: slow_down
                    error_description: polling too frequently
                    retry_after: 5
  /agents/{id}/sessions:poll:
    get:
      operationId: deviceFlowPoll
      tags:
        - Agents
      summary: Poll the agent's most-recent device-flow attempt
      description: |
        Returns a discriminated union over `state` ∈
        `{waiting, verifying, connected, denied, expired}`. Every response
        includes `server_now` (PROTO-08). The `connected` variant **never**
        includes `api_key` — credentials only flow through `:complete`
        (D-02 / NEEDS-DECISION #2). PROTO-02.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
      responses:
        '200':
          description: Current state of the device-flow attempt
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowPollResponse'
              examples:
                waiting:
                  summary: tests/fixtures/device-flow-v1/poll-waiting.json
                  value:
                    state: waiting
                    user_code: BCDF-GHJK
                    expires_at: '2026-06-10T18:10:00Z'
                    server_now: '2026-06-10T18:00:30Z'
                verifying:
                  summary: tests/fixtures/device-flow-v1/poll-verifying.json
                  value:
                    state: verifying
                    claim:
                      client_name: Claude Desktop
                      client_version: 1.4.2
                      client_signature: null
                      host: macbook-pro.local
                    server_now: '2026-06-10T18:01:15Z'
                connected:
                  summary: >-
                    tests/fixtures/device-flow-v1/poll-connected.json — NO
                    api_key field
                  value:
                    state: connected
                    session_id: ses_01HK7SES0DEF1GHIJ2KLM3NOPQ
                    agent_id: ag_01H7abc12345
                    server_now: '2026-06-10T18:01:30Z'
                denied:
                  summary: tests/fixtures/device-flow-v1/poll-denied.json
                  value:
                    state: denied
                    denied_at: '2026-06-10T18:01:45Z'
                    server_now: '2026-06-10T18:01:45Z'
                expired:
                  summary: tests/fixtures/device-flow-v1/poll-expired.json
                  value:
                    state: expired
                    expired_at: '2026-06-10T18:10:00Z'
                    server_now: '2026-06-10T18:10:05Z'
        '429':
          description: Rate limit exceeded (`slow_down`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
  /sessions:claim:
    post:
      operationId: deviceFlowClaim
      tags:
        - Agents
      summary: Claim a device code (MCP client / CLI)
      description: |
        Code-as-credential (D-01, SEC-07): no Authorization header is required;
        the user_code in the request body is the bearer credential. Account
        scoping comes from the code's bound tenant_id; per-IP rate limit
        carries the security weight (30/hr/IP). Atomically transitions the
        device_code from `pending → claimed` via single-statement conditional
        UPDATE with RETURNING (Pitfall 2). PROTO-03.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceFlowClaimRequest'
            examples:
              standard:
                value:
                  user_code: BCDF-GHJK
                  client_info:
                    name: Claude Desktop
                    version: 1.4.2
                    signature: null
                    host: macbook-pro.local
      responses:
        '200':
          description: Claim accepted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowClaimResponse'
              examples:
                standard:
                  summary: tests/fixtures/device-flow-v1/claim-response.json
                  value:
                    claim_id: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
        '400':
          description: Code unknown / expired / superseded / already claimed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
              examples:
                code_not_found:
                  summary: tests/fixtures/device-flow-v1/error-code_not_found.json
                  value:
                    error: code_not_found
                    error_description: unknown user_code
                code_already_claimed:
                  summary: >-
                    tests/fixtures/device-flow-v1/error-code_already_claimed.json
                  value:
                    error: code_already_claimed
                    error_description: >-
                      this device code has already been claimed by another
                      client
                code_superseded:
                  summary: tests/fixtures/device-flow-v1/error-code_superseded.json
                  value:
                    error: code_superseded
                    error_description: >-
                      a newer code has been issued for this agent; this one is
                      no longer valid
                expired_token:
                  summary: tests/fixtures/device-flow-v1/error-expired_token.json
                  value:
                    error: expired_token
                    error_description: device code has expired; issue a new one
        '429':
          description: Rate limit exceeded (`slow_down`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
  /agents/{id}/sessions:confirm:
    post:
      operationId: deviceFlowConfirm
      tags:
        - Agents
      summary: Approve or reject a pending device-flow claim
      description: |
        The dashboard user explicitly approves or rejects the claim made by
        the MCP client. On `approve`, the server creates the `sessions` row
        and stores `session_id` + `api_key_hash` in the device_code's
        claim_metadata; the `:complete` endpoint reads these back rather than
        re-creating a session. On `reject`, the device_code transitions to
        `denied` and the event is audit-logged. The `verifying → connected`
        (i.e. `claimed → approved`) transition is the **only** path other
        than `:confirm:approve` that resolves the claim — server-side
        Storm-2372 mitigation (SEC-04). PROTO-04.
      security:
        - BearerAuth: []
        - OrgSessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ag_01H7abc12345
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceFlowConfirmRequest'
            examples:
              approve:
                value:
                  claim_id: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
                  action: approve
              reject:
                value:
                  claim_id: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
                  action: reject
      responses:
        '200':
          description: Claim resolved (approved or denied)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowConfirmResponse'
              examples:
                approve:
                  summary: tests/fixtures/device-flow-v1/confirm-approve-response.json
                  value:
                    state: approved
                    confirmed_at: '2026-06-10T18:01:30Z'
                reject:
                  summary: tests/fixtures/device-flow-v1/confirm-reject-response.json
                  value:
                    state: denied
                    confirmed_at: '2026-06-10T18:01:45Z'
        '400':
          description: claim_id missing/expired/already-resolved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
              examples:
                claim_unavailable:
                  summary: tests/fixtures/device-flow-v1/error-claim_unavailable.json
                  value:
                    error: claim_unavailable
                    error_description: >-
                      this code cannot be claimed (already claimed, expired, or
                      superseded)
  /sessions:complete:
    post:
      operationId: deviceFlowComplete
      tags:
        - Agents
      summary: Pick up credentials after the dashboard user approves
      description: |
        Polled by the MCP client / CLI after `:claim`. The `claim_id` is the
        bearer credential (no Authorization header). Returns
        `{session_id, agent_id, api_key, access_token, expires_at, server_now}`
        ONCE — the plaintext API key is never re-derivable. The `session_id`
        is the row created during `:confirm:approve`; `:complete` does not
        create a session. The `access_token` is a fresh JWT with a 1-hour TTL
        sourced from `auth.AgentIssuer.Issue`. PROTO-05 / D-02.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceFlowCompleteRequest'
            examples:
              standard:
                value:
                  claim_id: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
      responses:
        '200':
          description: Credentials returned (api_key + access_token, ONCE)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowCompleteResponse'
              examples:
                standard:
                  summary: tests/fixtures/device-flow-v1/complete-response.json
                  value:
                    session_id: ses_01HK7SES0DEF1GHIJ2KLM3NOPQ
                    agent_id: ag_01H7abc12345
                    api_key: ak_live_AAAA1234567890abcdefghijklmnop
                    access_token: eyJhbGc...
                    expires_at: '2026-06-10T19:01:30Z'
                    server_now: '2026-06-10T18:01:30Z'
        '400':
          description: Authorization pending / access denied / expired / claim unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
              examples:
                authorization_pending:
                  summary: >-
                    tests/fixtures/device-flow-v1/error-authorization_pending.json
                  value:
                    error: authorization_pending
                    error_description: user has not yet completed the confirm beat
                access_denied:
                  summary: tests/fixtures/device-flow-v1/error-access_denied.json
                  value:
                    error: access_denied
                    error_description: user explicitly rejected the connection
                expired_token:
                  value:
                    error: expired_token
                    error_description: device code has expired; issue a new one
                claim_unavailable:
                  value:
                    error: claim_unavailable
                    error_description: >-
                      this code cannot be claimed (already claimed, expired, or
                      superseded)
  /sessions/{id}/heartbeat:
    post:
      operationId: sessionHeartbeat
      tags:
        - Agents
      summary: Heartbeat — refresh session.last_seen_at
      description: |
        Updates `sessions.last_seen_at = NOW()` (database clock; Pitfall 7).
        Per-session rate limit of 1/min (SEC-10, Pitfall 6). PROTO-06.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: ses_01HK7SES0DEF1GHIJ2KLM3NOPQ
      responses:
        '200':
          description: Heartbeat accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionHeartbeatResponse'
              examples:
                standard:
                  summary: tests/fixtures/device-flow-v1/heartbeat-response.json
                  value:
                    last_seen_at: '2026-06-10T18:05:00Z'
                    next_allowed_at: '2026-06-10T18:06:00Z'
                    server_now: '2026-06-10T18:05:00Z'
        '429':
          description: Per-session heartbeat rate limit (1/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
  /sessions/by-code/{code}:
    get:
      operationId: lookupSessionByCode
      summary: Resolve agent_id for a device-flow code (deeplink lookup)
      description: |
        OPS-05. Unauthenticated lookup used by the /connect/{code} frontend
        deeplink to resolve the agent_id before navigating to
        /agents/{agent_id}/connect. Returns ONLY agent_id; never tenant_id,
        client_info, or claim_metadata. Codes in denied or expired state
        return 404 (indistinguishable from never-existed) to defeat
        enumeration.
      tags:
        - Agents
      security: []
      parameters:
        - in: path
          name: code
          required: true
          schema:
            type: string
            pattern: >-
              ^[BCDFGHJKMNPQRSTVWXYZ23456789]{4}-[BCDFGHJKMNPQRSTVWXYZ23456789]{4}$
      responses:
        '200':
          description: Code resolves to an agent
          content:
            application/json:
              schema:
                type: object
                required:
                  - agent_id
                additionalProperties: false
                properties:
                  agent_id:
                    type: string
        '400':
          description: Code did not match the required format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
        '404':
          description: Code not found, expired, or denied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
        '429':
          description: Per-IP rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceFlowError'
  /knowledge/ingest:
    post:
      operationId: ingestDocument
      x-mcp-tool:
        name: angareion_ingest_document
        title: Ingest a document
        args-type: IngestDocumentArgs
      tags:
        - Knowledge
      summary: Upload a document to the agent's knowledge base
      description: |
        Upload a document (PDF, DOCX, PPTX, XLSX, HTML, TXT, MD) to the agent's
        knowledge base. `file_content` must be standard base64-encoded bytes.
        Returns `{accepted: true, document_id, status: "pending"}` on 202.
        Use `angareion_list_documents` to poll for indexing status.
        Returns `{accepted: false, error_code: "permission_denied"}` when the
        agent lacks `knowledge:write`.
        Phase 02 D-09: optional `sensitivity` and `audience` fields; defaults
        applied server-side when absent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestDocumentRequest'
            examples:
              pdf_upload:
                value:
                  file_content: JVBERi0xLjQK...
                  filename: architecture-guide.pdf
                  mime_type: application/pdf
      responses:
        '202':
          description: Document accepted for ingestion
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestDocumentResponse'
              examples:
                accepted:
                  value:
                    accepted: true
                    document_id: doc_01H7abc12345
                    status: pending
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Caller lacks knowledge:write permission
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestDocumentResponse'
              examples:
                permission_denied:
                  value:
                    accepted: false
                    error_code: permission_denied
                    hint: >-
                      caller lacks knowledge:write permission; ask an admin to
                      grant it
  /knowledge/documents:
    get:
      operationId: listDocuments
      x-mcp-tool:
        name: angareion_list_documents
        title: List documents
        args-type: ListDocumentsArgs
      tags:
        - Knowledge
      summary: List all documents in the agent's tenant knowledge base
      description: >
        Flat list across all ingestion sources for the agent's tenant.

        Optional `offset` (default 0) and `limit` (default 20, max 100) for
        pagination.

        Returns `{documents: [...], total}`. Each document has `document_id`,
        `filename`,

        `mime_type`, `status` (pending|indexed|failed), `ingested_at`,

        `ingestion_source_id`, and `ingestion_source_name`.
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Document list
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentListResponse'
              examples:
                with_items:
                  value:
                    documents:
                      - document_id: doc_01H7abc12345
                        filename: architecture-guide.pdf
                        mime_type: application/pdf
                        status: indexed
                        ingested_at: '2026-05-01T10:00:00Z'
                        ingestion_source_id: src_01H7abc12345
                        ingestion_source_name: default
                    total: 1
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /knowledge/sources/{source_id}/documents/{document_id}/chunks:
    get:
      operationId: getDocumentChunks
      x-mcp-tool:
        name: angareion_get_document_chunks
        title: Get document chunks
        args-type: GetDocumentChunksArgs
      tags:
        - Knowledge
      summary: List the parsed chunks for a single document
      description: >
        Returns parsed chunks for the document ordered by `chunk_index`
        ascending.

        Requires `source_id` (UUID) and `document_id` (UUID).

        Optional `offset` (default 0) and `limit` (default 20, max 100).

        Each chunk has `chunk_id`, `document_id`, `page_number`, `char_offset`,

        `char_length`, `text`, and `created_at`.

        Use this to inspect what was extracted before searching or citing
        memories.
      parameters:
        - name: source_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Ingestion source UUID.
        - name: document_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Document UUID.
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Chunk list
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentChunkListResponse'
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Source or document not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /knowledge/sources/{source_id}/documents/{document_id}:
    delete:
      operationId: deleteDocument
      x-mcp-tool:
        name: angareion_delete_document
        title: Delete a document
        args-type: DeleteDocumentArgs
      tags:
        - Knowledge
      summary: Delete a document and all its chunks from the knowledge base
      description: |
        Permanently deletes a document and all its chunks from the agent's
        knowledge base. Requires `source_id` (UUID) and `document_id` (UUID).
        Returns `{deleted: true, document_id}` on success.
        Returns `{deleted: false, error_code: "not_found"}` when the document
        does not exist (structured result, NOT an HTTP 404 error).
        Chunks are removed from Postgres and the corresponding Neo4j Chunk nodes
        are deleted by the gateway.
      parameters:
        - name: source_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Ingestion source UUID.
        - name: document_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Document UUID.
      responses:
        '200':
          description: Document deleted or not found (structured result in body)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteDocumentResponse'
              examples:
                deleted:
                  value:
                    deleted: true
                    document_id: doc_01H7abc12345
                not_found:
                  value:
                    deleted: false
                    document_id: doc_01H7abc12345
                    error_code: not_found
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories/{id}/cite:
    get:
      operationId: citeMemory
      x-mcp-tool:
        name: angareion_cite_memory
        title: Cite a memory
        args-type: CiteMemoryArgs
      tags:
        - Knowledge
      summary: Return the full citation array for a memory
      description: >
        Returns every Document/Chunk the memory was derived from.

        Requires `memory_id` (UUID). Each citation has `document_id`,

        `document_name`, `page`, `snippet`, `source_uri`, `confidence`,

        `version`, and `freshness{stale, stale_since}`.

        Use this to verify provenance and surface source attribution to the
        user.

        Returns an empty `citations` array (HTTP 200) for memories created

        without a source document — this is NOT an error condition.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Memory UUID.
          x-go-json-name: memory_id
      responses:
        '200':
          description: Citation list (may be empty for memories without source documents)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemoryCitationListResponse'
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories/{id}/forget:
    post:
      operationId: forgetMemory
      x-mcp-tool:
        name: memory_forget
        title: Forget (invalidate) a memory
        args-type: ForgetMemoryArgs
      tags:
        - Memories
      summary: Reversibly invalidate a memory so it stops surfacing
      description: >
        Phase 04 R5. Reversible self-curation: sets the memory's `invalid_at`

        (invalidate-never-delete — the row is preserved) so it stops surfacing
        in

        current `memory_search`, but can be recovered within the reversibility

        window. This is NOT a hard delete (hard-delete/RTBF is a separate

        admin-gated path). Backed by `MemoryHandler.Forget` in

        `api/internal/handlers/memory.go` via the RetireOrMerger `OpForget`

        primitive.


        Idempotent: re-calling on an already-invalidated memory returns HTTP 200

        with `invalid_at` unchanged (no error, no second mutation). Winner-safe:

        forgetting a contradiction winner never resurrects the losers it

        invalidated. Requires `memory_id` (UUID).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Memory UUID.
          x-go-json-name: memory_id
      responses:
        '200':
          description: Memory invalidated (or already invalidated — idempotent success)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required:
                  - memory_id
                  - forgotten
                  - invalid_at
                properties:
                  memory_id:
                    type: string
                    format: uuid
                  forgotten:
                    type: boolean
                  invalid_at:
                    type: string
                    format: date-time
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Memory not found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /memories/{id}/lineage:
    get:
      operationId: explainMemoryLineage
      x-mcp-tool:
        name: memory_explain_lineage
        title: Explain a memory's lineage
        args-type: ExplainLineageArgs
      tags:
        - Memories
      summary: Return a memory's merge lineage and supersession trace
      description: >
        Phase 04 R6. Read-only provenance inspection. Returns the memory's

        `merge_lineage` rows (which memories were merged into or away from it)
        plus

        the `SUPERSEDES`/`DERIVED_FROM` graph trace from the memory-bridge. A

        freshly-created, never-merged/never-superseded memory returns HTTP 200
        with

        an empty ancestor set (NOT 404) — an empty lineage is a valid answer,
        not an

        error. Backed by `MemoryHandler.ExplainLineage` in

        `api/internal/handlers/memory.go`. Requires `memory_id` (UUID).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Memory UUID.
          x-go-json-name: memory_id
      responses:
        '200':
          description: >-
            Lineage (may be empty for a never-merged memory — this is NOT an
            error)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                type: object
                required:
                  - memory_id
                  - merge_lineage
                  - superseded
                properties:
                  memory_id:
                    type: string
                    format: uuid
                  merge_lineage:
                    type: array
                    items:
                      type: object
                      properties:
                        loser_memory_id:
                          type: string
                          format: uuid
                        winner_memory_id:
                          type: string
                          format: uuid
                        origin:
                          type: string
                  superseded:
                    type: boolean
                  supersedes_target:
                    type:
                      - string
                      - 'null'
                    format: uuid
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /specialists/{name}:
    get:
      operationId: loadSpecialist
      x-mcp-tool:
        name: specialist_load
        title: Load a specialist
        args-type: LoadSpecialistArgs
      tags:
        - Specialists
      summary: Load a specialist preamble by name
      description: >
        Returns the latest specialist preamble (system_prompt,
        memory_search_directives,

        output_format_rules, version_num) for the bound agent's tenant.

        Returns a structured `{error: {code: "not_found"}}` when no such
        specialist

        exists; `{error: {code: "unavailable"}}` after 3 retries on transport
        failure.

        Errors are returned as structured results — NOT HTTP error codes — so
        MCP

        clients can surface user-friendly retry guidance.
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
          description: Specialist name (slug).
          x-go-jsonschema: the specialist name to load
      responses:
        '200':
          description: Specialist loaded or structured error
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpecialistLoadResponse'
              examples:
                loaded:
                  value:
                    name: pmm-intel
                    version_num: 3
                    system_prompt: You are a PMM intelligence assistant...
                    description: Surfaces market intelligence for PMMs
                not_found:
                  value:
                    error:
                      code: not_found
                      message: specialist "pmm-intel" not found in this tenant
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /specialists:
    get:
      operationId: listSpecialists
      x-mcp-tool:
        name: specialist_list
        title: List specialists
        args-type: ListSpecialistsArgs
      tags:
        - Specialists
      summary: List all specialists available to the bound agent's tenant
      description: >
        Returns all specialists ordered alphabetically by the server (do not
        re-sort

        on the client). Each specialist has `name`, `version_num`,
        `description`,

        and `updated_at`.
      responses:
        '200':
          description: Specialist list
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpecialistListResponse'
              examples:
                with_items:
                  value:
                    specialists:
                      - name: pmm-intel
                        version_num: 3
                        description: Surfaces market intelligence for PMMs
                        updated_at: '2026-05-01T10:00:00Z'
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /specialists/{name}/session:
    delete:
      operationId: unloadSpecialist
      x-mcp-tool:
        name: specialist_unload
        title: Unload a specialist session
        args-type: UnloadSpecialistArgs
      tags:
        - Specialists
      summary: Clear the server-side session record for a loaded specialist
      description: >
        Removes the server-side session record for a (agent, specialist) pair.

        This is observability bookkeeping only — it does NOT remove the
        specialist's

        preamble from the current conversation context; those instructions are
        already

        in the conversation and keep applying. To stop following a specialist,
        load

        a different one (its reset clause supersedes the previous preamble) or
        start

        a fresh session.

        Idempotent — returns success even if no session was recorded.

        Phase 04.5 D-17 / SPEC R14.
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
          description: Specialist name (slug).
          x-go-jsonschema: the specialist name to unload
      responses:
        '200':
          description: Session cleared (or was already absent — idempotent)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpecialistUnloadResponse'
              examples:
                unloaded:
                  value:
                    unloaded: true
                    name: pmm-intel
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /compose:
    post:
      operationId: compose
      x-mcp-tool:
        name: angareion_compose
        title: Compose a synthesis
        args-type: ComposeArgs
      tags:
        - Compose
      summary: Compose an LLM synthesis for a workflow run
      description: >
        Retrieves memory context (excluding sensitivity=confidential per SPEC
        P1),

        calls the tenant's LLM with prompt caching on the shared prefix, and
        returns

        a draft text plus cited memory IDs.

        Required: `template_id` (workflow template slug, v1:
        "launch-a-feature"),

        `run_id` (UUID identifying this workflow run — same `run_id` twice
        returns

        `duplicate_run` error).

        Optional: `context_memory_ids` ([]string of memory UUIDs to consider),

        `launch_brief_text` (description of the feature being launched),

        `prompt_id` (reserved for future recipes; leave empty for v1).

        On SPEC R6 duplicate_run, returns error `code=duplicate_run`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ComposeRequest'
            examples:
              launch_feature:
                value:
                  template_id: launch-a-feature
                  run_id: 550e8400-e29b-41d4-a716-446655440000
                  launch_brief_text: Announcing the new analytics dashboard for PMMs
      responses:
        '200':
          description: Composed synthesis
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComposeResponse'
              examples:
                success:
                  value:
                    draft_text: We are thrilled to announce...
                    cited_memory_ids:
                      - mem_01H7abc12345
        '401':
          description: Unauthorized
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Duplicate run (same run_id already in progress)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /handoffs:
    post:
      operationId: saveHandoff
      tags:
        - Handoffs
      summary: Save working state under a name
      x-mcp-tool:
        name: handoff_save
        title: Save a handoff
        args-type: SaveHandoffArgs
      description: |
        Persists a named snapshot of working state for the calling agent.
        Saving to a `name` that already exists overwrites the prior payload for
        that name in place and increments `version_num`; the prior payload is
        not retained.

        Extraction is client-side: the gateway never receives a transcript, so
        the `payload` supplied here is everything a resuming session will have.

        `tenant_id` and `agent_id` are read from the JWT and cannot be supplied
        in the body. Unknown top-level keys are rejected with 400.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveHandoffRequest'
            examples:
              pause_point:
                value:
                  name: q3-pricing
                  summary: Reworking the Q3 pricing page copy and the tier table
                  payload:
                    completed:
                      - Rewrote the hero section
                    remaining:
                      - Tier comparison table still uses the old tier names
                    blockers:
                      - Legal has not signed off on the enterprise wording yet
                    decisions:
                      - title: Dropped the enterprise tier from the table
                        rationale: >-
                          Sales asked for it to stay, but the table becomes
                          unreadable at four columns on mobile and enterprise is
                          quote-only anyway.
                    next_action: Update the tier table to the new names
                    context_notes: Copy lives in frontend/apps/app/src/pricing/
                  citations:
                    - memory_id: 550e8400-e29b-41d4-a716-446655440000
                  artifacts:
                    - frontend/apps/app/src/pricing/hero.tsx
                    - https://github.com/example/repo/pull/1234
                  origin_client: claude-code
                  scope: agent
      responses:
        '201':
          description: Handoff saved
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '400':
          description: |
            Validation error — missing `name`, a `scope` other than `agent`, a
            `payload` whose known fields do not match the documented shape, or
            an unknown top-level key.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: |
            No agent identity resolvable from the request. An org-user token
            reaches this route and receives 401 — handoffs are agent-scoped.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      operationId: listHandoffs
      tags:
        - Handoffs
      summary: List or search the calling agent's handoffs
      x-mcp-tool:
        name: handoff_list
        title: List handoffs
        args-type: ListHandoffsArgs
      description: |
        Returns the calling agent's own handoffs, most-recently-saved first.

        With `q` absent this is a plain listing. With `q` present the same
        response shape is returned, narrowed by a merged keyword and semantic
        match over saved names and summaries.

        An empty array is a normal, successful result: it means this agent has
        no saved handoffs, not that anything failed.
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: |
            Optional free-text term. When present, results are narrowed by a
            merged keyword and semantic match instead of being a plain listing.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: |
            Applies to the plain listing only. The searched branch is bounded by
            `limit` and does not paginate.
      responses:
        '200':
          description: Handoff summaries, most-recently-saved first (possibly empty)
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/HandoffSummary'
              examples:
                empty:
                  value: []
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /handoffs/resume:
    post:
      operationId: resumeHandoff
      tags:
        - Handoffs
      summary: Resume previously saved working state
      x-mcp-tool:
        name: handoff_resume
        title: Resume a handoff
        args-type: ResumeHandoffArgs
      description: >
        Resolves a handoff and returns its rendered brief. Resolution tries an

        exact name match first, then an ID match, then a semantic match over

        saved names and summaries.


        This operation has TWO distinct 200 shapes, told apart by the response

        `Content-Type` header:


        | Content-Type | Meaning |

        |---|---|

        | `text/markdown; charset=utf-8` | A unique match. The body is the brief
        itself, and the handoff was resumed — its last-resumed timestamp is
        stamped and its expiry reset to a full TTL. |

        | `application/json` | Ambiguous. The body is a candidate list and
        NOTHING was resumed. Present the choices to the user and resume the
        chosen one by name or id. |


        A literal `resume` is never treated as a handoff name: this is a fixed

        route segment, so a saved handoff named `resume` is reached by ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResumeHandoffRequest'
            examples:
              by_name:
                value:
                  query: q3-pricing
              by_description:
                value:
                  query: the thing where we dropped the enterprise tier
      responses:
        '200':
          description: |
            Either the rendered brief (`text/markdown; charset=utf-8`) on a
            unique match, or the candidate list (`application/json`) when the
            semantic step is ambiguous and nothing was resumed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            text/markdown:
              schema:
                type: string
                description: |
                  The rendered brief. Its opening lines are a reset clause that
                  supersedes prior instructions carried in the resuming session.
            application/json:
              schema:
                $ref: '#/components/schemas/ResumeCandidatesResponse'
        '400':
          description: Missing or empty `query`, or an unusable body
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            Nothing resolved. Another agent's name or ID is indistinguishable
            from one that does not exist.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /handoffs/{id}:
    get:
      operationId: getHandoff
      tags:
        - Handoffs
      summary: Get one handoff by id
      description: |
        Scoped to the caller's tenant AND agent. Another agent's handoff is
        reported as 404, identical to one that does not exist, so IDs cannot be
        probed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Handoff UUID.
      responses:
        '200':
          description: Handoff detail
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found, malformed id, or owned by another agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: deleteHandoff
      tags:
        - Handoffs
      summary: Delete a handoff
      description: |
        Removes the handoff from the relational store and its vector point from
        the handoffs collection in the same request.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Handoff UUID.
      responses:
        '204':
          description: Deleted
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found, malformed id, or owned by another agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /handoffs/{id}/brief.md:
    get:
      operationId: getHandoffBriefMD
      tags:
        - Handoffs
      summary: Export the rendered brief for one handoff
      description: |
        Returns the deterministic Markdown brief — byte-for-byte the same bytes
        a successful resume returns. Exporting does NOT resume: no timestamp is
        stamped and no expiry is reset. The audit trail records this as an
        export, distinct from a resume.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Handoff UUID.
      responses:
        '200':
          description: The rendered brief
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            text/markdown:
              schema:
                type: string
                description: |
                  Served with `Content-Type: text/markdown; charset=utf-8`. The
                  opening lines are a reset clause.
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found, malformed id, or owned by another agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /handoffs/{id}/pin:
    post:
      operationId: pinHandoff
      tags:
        - Handoffs
      summary: Pin a handoff, exempting it from TTL reaping
      description: |
        A pinned handoff survives the expiry reaper indefinitely until it is
        unpinned or deleted.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Handoff UUID.
      responses:
        '200':
          description: The pinned handoff
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found, malformed id, or owned by another agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: unpinHandoff
      tags:
        - Handoffs
      summary: Unpin a handoff, returning it to normal TTL treatment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Handoff UUID.
      responses:
        '200':
          description: The unpinned handoff
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Handoff'
        '401':
          description: No agent identity resolvable from the request
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found, malformed id, or owned by another agent
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    OrgSessionCookie:
      type: apiKey
      in: cookie
      name: org_session
      description: |
        Httponly session cookie set by OrgAuthHandler at login, refresh, and
        password-set. Carries the org admin access JWT. Used by browser-based
        SSE connections where Authorization header is not available.
  schemas:
    TokenExchangeRequest:
      type: object
      required:
        - api_key
      properties:
        api_key:
          type: string
          minLength: 40
          maxLength: 40
          pattern: ^ak_live_[A-Za-z0-9]{32}$
          description: >-
            API key starting with `ak_live_` followed by 32 base62 chars (40
            total).
          example: ak_live_AbCdEfGhIjKlMnOpQrStUvWxYz0123456
    TokenExchangeResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_at
        - agent
      properties:
        access_token:
          type: string
          description: Signed JWT (HS256). Lifetime 1 hour.
        token_type:
          type: string
          enum:
            - Bearer
        expires_at:
          type: string
          format: date-time
        agent:
          type: object
          required:
            - id
            - name
            - tenant_id
          properties:
            id:
              type: string
              example: ag_01H7abc12345
            name:
              type: string
            team:
              type: string
            tenant_id:
              type: string
              example: tn_01H7abc12345
    Event:
      type: object
      description: >
        CloudEvents 1.0 envelope. The fields below are the spec required+core
        set.

        `data` is provider-defined.
      required:
        - specversion
        - type
        - source
        - id
      properties:
        specversion:
          type: string
          enum:
            - '1.0'
        type:
          type: string
          description: Reverse-DNS event type (e.g., `com.example.order.created`).
          example: com.example.order.created
        source:
          type: string
          description: URI identifying the source system.
          example: //orders.example.com
        id:
          type: string
          description: Unique event ID (used for dedup).
          example: evt_01H7abc12345
        time:
          type: string
          format: date-time
        datacontenttype:
          type: string
          example: application/json
        subject:
          type: string
        data:
          description: Provider-defined event payload.
    EventAcceptedResponse:
      type: object
      required:
        - event_id
        - status
      properties:
        event_id:
          type: string
          example: evt_01H7abc12345
        status:
          type: string
          enum:
            - accepted
            - duplicate
    Delivery:
      type: object
      required:
        - id
        - event_id
        - payload
        - attempt_count
        - created_at
      properties:
        id:
          type: string
          example: dlv_01H7abc12345
        event_id:
          type: string
          example: evt_01H7abc12345
        payload:
          description: The full enriched event payload delivered to the agent.
        attempt_count:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
    DeliveryListResponse:
      type: object
      required:
        - deliveries
        - count
      properties:
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/Delivery'
        count:
          type: integer
          minimum: 0
    AckRequest:
      type: object
      required:
        - delivery_ids
      properties:
        delivery_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            example: dlv_01H7abc12345
    AckResponse:
      type: object
      required:
        - acknowledged
      properties:
        acknowledged:
          type: integer
          minimum: 0
    DLQItem:
      type: object
      required:
        - id
        - event_id
        - agent_id
        - attempt_count
        - created_at
      properties:
        id:
          type: string
          example: dlv_01H7abc12345
        event_id:
          type: string
          example: evt_01H7abc12345
        agent_id:
          type: string
          example: ag_01H7abc12345
        attempt_count:
          type: integer
        dlq_reason:
          type: string
        dlq_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    DLQListResponse:
      type: object
      required:
        - dlq_items
        - total
      properties:
        dlq_items:
          type: array
          items:
            $ref: '#/components/schemas/DLQItem'
        total:
          type: integer
          minimum: 0
    ReplayResponse:
      type: object
      required:
        - status
        - delivery_id
      properties:
        status:
          type: string
          enum:
            - replayed
        delivery_id:
          type: string
    Agent:
      type: object
      required:
        - id
        - tenant_id
        - name
        - status
        - delivery_mode
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: ag_01H7abc12345
        tenant_id:
          type: string
          example: tn_01H7abc12345
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - active
            - inactive
        delivery_mode:
          type: string
          enum:
            - poll
            - push
            - sse
        delivery_endpoint:
          type:
            - string
            - 'null'
          format: uri
        team_id:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        created_by_user_id:
          type:
            - string
            - 'null'
          description: >-
            Org user UUID who registered the agent. Null for API-key-registered
            agents and legacy agents pre-role-model.
        owner_display_name:
          type:
            - string
            - 'null'
          description: >-
            Display name of the registering user, resolved via LEFT JOIN.
            Present only on admin ListAgents responses; omitted (not null) on
            user-scoped responses.
    AgentListResponse:
      type: object
      required:
        - agents
        - total
      properties:
        agents:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        total:
          type: integer
          minimum: 0
    CreateAgentRequest:
      type: object
      required:
        - name
        - delivery_mode
      properties:
        name:
          type: string
          minLength: 1
        description:
          type:
            - string
            - 'null'
        delivery_mode:
          type: string
          enum:
            - poll
            - push
            - sse
        team_id:
          type:
            - string
            - 'null'
    UpdateAgentRequest:
      type: object
      description: All fields optional; at least one must be provided.
      properties:
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        delivery_mode:
          type: string
          enum:
            - poll
            - push
            - sse
        delivery_endpoint:
          type:
            - string
            - 'null'
          format: uri
    AgentKey:
      type: object
      description: API key metadata (no plaintext, no hash).
      required:
        - id
        - prefix
        - scopes
        - created_at
      properties:
        id:
          type: string
          example: key_01H7abc12345
        prefix:
          type: string
          description: First 16 chars (`ak_live_` + 8 random) for identification.
          example: ak_live_AbCdEfGh
        scopes:
          type: array
          items:
            type: string
        last_used_at:
          type:
            - string
            - 'null'
          format: date-time
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        revoked_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    CreatedAgentKey:
      type: object
      description: Returned ONLY at creation time. Includes the plaintext `key`.
      required:
        - id
        - prefix
        - key
        - scopes
        - created_at
      properties:
        id:
          type: string
        prefix:
          type: string
        key:
          type: string
          description: >
            Plaintext API key (`ak_live_` + 32 base62 chars). Stored hashed
            server-side.

            This is the ONLY response that includes the plaintext.
        scopes:
          type: array
          items:
            type: string
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    AgentKeyListResponse:
      type: object
      required:
        - keys
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/AgentKey'
    CreateAgentKeyRequest:
      type: object
      required:
        - scopes
      properties:
        scopes:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - send
              - receive
              - memory:read
              - memory:write
              - admin
              - channel:read
              - channel:write
              - interests:read
              - interests:write
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
    AgentEvent:
      type: object
      required:
        - event_id
        - event_type
        - delivered_at
      description: |
        Minimal recent-activity row returned by `GET /agents/{id}/events`.
        Phase 03 D-02 / D-04 fields only — full event payloads are out of
        scope for the dashboard's first-impression view.
      properties:
        event_id:
          type: string
          description: CloudEvents id of the delivered event.
          example: evt_01H7abc12345
        event_type:
          type: string
          description: CloudEvents type field, copied from the original event payload.
          example: deploy.succeeded
        delivered_at:
          type: string
          format: date-time
          description: RFC3339 timestamp when the delivery row was created.
          example: '2026-06-01T15:30:00Z'
    AgentEventListResponse:
      type: object
      required:
        - events
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/AgentEvent'
    Interest:
      type: object
      required:
        - id
        - tenant_id
        - agent_id
        - event_type
        - priority
        - enrichment_depth
        - active
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: int_01H7abc12345
        tenant_id:
          type: string
        agent_id:
          type: string
        event_type:
          type: string
          description: Glob pattern (e.g., `com.example.order.*`).
        filter_expression:
          type:
            - string
            - 'null'
          description: Optional `key op value` filter (e.g., `amount_cents > 10000`).
        priority:
          type: integer
        enrichment_depth:
          type: string
          enum:
            - none
            - light
            - full
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    InterestListResponse:
      type: object
      required:
        - interests
      properties:
        interests:
          type: array
          items:
            $ref: '#/components/schemas/Interest'
    CreateInterestRequest:
      type: object
      required:
        - event_type
        - priority
        - enrichment_depth
      properties:
        event_type:
          type: string
          minLength: 1
        filter_expression:
          type:
            - string
            - 'null'
        priority:
          type: integer
          minimum: 0
        enrichment_depth:
          type: string
          enum:
            - none
            - light
            - full
    Channel:
      type: object
      required:
        - id
        - tenant_id
        - name
        - type
        - created_by_type
        - created_by_id
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: ch_01H7abc12345
        tenant_id:
          type: string
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        type:
          type: string
          enum:
            - open
            - private
            - direct
        created_by_type:
          type: string
          enum:
            - agent
            - org_admin
        created_by_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ChannelListResponse:
      type: object
      required:
        - channels
        - total
      properties:
        channels:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        total:
          type: integer
          minimum: 0
    CreateChannelRequest:
      type: object
      required:
        - name
        - type
      properties:
        name:
          type: string
          minLength: 1
        description:
          type:
            - string
            - 'null'
          x-go-pointer: true
        type:
          type: string
          enum:
            - open
            - private
            - direct
    ChannelMember:
      type: object
      required:
        - id
        - channel_id
        - participant_type
        - participant_id
        - role
        - joined_at
      properties:
        id:
          type: string
          example: mbr_01H7abc12345
        channel_id:
          type: string
        participant_type:
          type: string
          enum:
            - agent
            - org_admin
        participant_id:
          type: string
        role:
          type: string
          enum:
            - owner
            - admin
            - member
        joined_at:
          type: string
          format: date-time
    ChannelMemberListResponse:
      type: object
      required:
        - members
      properties:
        members:
          type: array
          items:
            $ref: '#/components/schemas/ChannelMember'
    AddMemberRequest:
      type: object
      required:
        - participant_type
        - participant_id
      properties:
        participant_type:
          type: string
          enum:
            - agent
            - org_admin
        participant_id:
          type: string
        role:
          type: string
          enum:
            - owner
            - admin
            - member
          default: member
    ChannelFeed:
      type: object
      required:
        - id
        - channel_id
        - event_type_glob
        - active
        - created_at
      properties:
        id:
          type: string
          example: feed_01H7abc12345
        channel_id:
          type: string
        event_type_glob:
          type: string
        filter_expression:
          type:
            - string
            - 'null'
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
    ChannelFeedListResponse:
      type: object
      required:
        - feeds
      properties:
        feeds:
          type: array
          items:
            $ref: '#/components/schemas/ChannelFeed'
    CreateFeedRequest:
      type: object
      required:
        - event_type_glob
      properties:
        event_type_glob:
          type: string
          minLength: 1
        filter_expression:
          type:
            - string
            - 'null'
    Memory:
      type: object
      required:
        - id
        - tenant_id
        - agent_id
        - scope
        - type
        - status
        - title
        - content
        - confidence
        - importance
        - metadata
        - created_at
        - updated_at
        - accessed_at
      properties:
        id:
          type: string
          format: uuid
          example: mem_01H7abc12345
        tenant_id:
          type: string
        agent_id:
          type: string
        project_id:
          type:
            - string
            - 'null'
        scope:
          type: string
          enum:
            - agent
            - team
            - institutional
        team_id:
          type:
            - string
            - 'null'
        type:
          type: string
          enum:
            - episodic
            - semantic
            - procedural
            - entity
            - reflection
            - reasoning
        status:
          type: string
          enum:
            - active
            - deleted
        title:
          type: string
          minLength: 1
        content:
          type: string
          minLength: 1
          maxLength: 32768
        confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
        importance:
          type: number
          format: float
          minimum: 0
          maximum: 1
        metadata:
          type: object
          additionalProperties: true
        source_type:
          type:
            - string
            - 'null'
        source_id:
          type:
            - string
            - 'null'
        source_url:
          type:
            - string
            - 'null'
          format: uri
        source_event_id:
          type:
            - string
            - 'null'
        freshness_date:
          type:
            - string
            - 'null'
          format: date-time
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        accessed_at:
          type: string
          format: date-time
        cited_memory_ids:
          type: array
          items:
            type: string
            format: uuid
    MemoryListResponse:
      type: object
      required:
        - memories
        - total
      properties:
        memories:
          type: array
          items:
            $ref: '#/components/schemas/Memory'
        total:
          type: integer
          minimum: 0
    MemoryHealthResponse:
      type: object
      description: >-
        Read-only, tenant-scoped memory-plane health snapshot (Phase 05 05-05,
        R5). All counts are >= 0; an empty tenant returns all-zero counts and an
        empty decay_distribution.
      required:
        - duplicate_count
        - orphan_count
        - stale_count
        - embed_gap_count
        - reflection_count
        - decay_distribution
      properties:
        duplicate_count:
          type: integer
          minimum: 0
          description: Open merge proposals (near-duplicate pairs awaiting resolution).
        orphan_count:
          type: integer
          minimum: 0
          description: Active memories with no incoming (:Entity)-[:MENTIONS]->() edge.
        stale_count:
          type: integer
          minimum: 0
          description: Active memories below the decay/archive importance floor.
        embed_gap_count:
          type: integer
          minimum: 0
          description: >-
            Active memories still missing a vector (PG active count minus Qdrant
            points).
        reflection_count:
          type: integer
          minimum: 0
          description: Tier-exempt type='reflection' active tally (D-01).
        decay_distribution:
          type: object
          additionalProperties:
            type: integer
            minimum: 0
          description: >-
            Histogram of active-memory importance bucketed into 10 equal-width
            buckets over [0,1] (width_bucket). Keys are bucket numbers as
            strings.
    CreateMemoryRequest:
      type: object
      required:
        - title
        - content
        - type
        - scope
      properties:
        title:
          type: string
          minLength: 1
        content:
          type: string
          minLength: 1
          maxLength: 32768
        type:
          type: string
          enum:
            - episodic
            - semantic
            - procedural
            - entity
            - reflection
            - reasoning
        scope:
          type: string
          enum:
            - agent
            - team
            - institutional
        team_id:
          type:
            - string
            - 'null'
          description: Required if scope=team and not in JWT context.
          x-go-pointer: true
        project_id:
          type:
            - string
            - 'null'
          x-go-pointer: true
        confidence:
          type: number
          minimum: 0
          maximum: 1
          x-go-pointer: true
        importance:
          type: number
          minimum: 0
          maximum: 1
          x-go-pointer: true
        metadata:
          type: object
          additionalProperties: true
        source_type:
          type:
            - string
            - 'null'
          x-go-pointer: true
        source_id:
          type:
            - string
            - 'null'
          x-go-pointer: true
        source_url:
          type:
            - string
            - 'null'
          format: uri
          x-go-pointer: true
        source_event_id:
          type:
            - string
            - 'null'
          x-go-pointer: true
        freshness_date:
          type:
            - string
            - 'null'
          format: date-time
          x-go-pointer: true
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          x-go-pointer: true
    UpdateMemoryRequest:
      type: object
      description: All fields optional; at least one must be provided.
      properties:
        title:
          type: string
          x-go-pointer: true
        content:
          type: string
          maxLength: 32768
          x-go-pointer: true
        confidence:
          type: number
          minimum: 0
          maximum: 1
          x-go-pointer: true
        importance:
          type: number
          minimum: 0
          maximum: 1
          x-go-pointer: true
        metadata:
          type: object
          additionalProperties: true
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          x-go-pointer: true
        freshness_date:
          type:
            - string
            - 'null'
          format: date-time
          x-go-pointer: true
    MemorySearchRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          minLength: 1
          description: Natural-language search query.
        scope:
          type: string
          enum:
            - agent
            - team
            - institutional
          description: >-
            Limit results to a specific scope. Default searches all accessible
            scopes.
        memory_types:
          type: array
          items:
            type: string
            enum:
              - episodic
              - semantic
              - procedural
              - entity
              - reflection
              - reasoning
          description: >-
            Filter results by memory type (array form; replaces single-type
            filter).
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
          x-go-pointer: true
        min_score:
          type: number
          minimum: 0
          maximum: 1
          description: Minimum similarity score (0..1) for returned memories.
          x-go-pointer: true
    MemorySearchResult:
      type: object
      required:
        - memory
        - score
      properties:
        memory:
          $ref: '#/components/schemas/Memory'
        score:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Composite ranking score (semantic + confidence + recency).
    MemorySearchResponse:
      type: object
      required:
        - results
        - total
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/MemorySearchResult'
        total:
          type: integer
          minimum: 0
    PromoteMemoryRequest:
      type: object
      required:
        - scope
      properties:
        scope:
          type: string
          enum:
            - team
            - institutional
          description: Target scope. Cannot be "agent" (only promotion supported).
        team_id:
          type:
            - string
            - 'null'
          description: Required when scope=team.
          x-go-pointer: true
    CreateMergeProposalRequest:
      type: object
      required:
        - memory_a_id
        - memory_b_id
      properties:
        memory_a_id:
          type: string
          format: uuid
          description: >-
            First memory in the proposed merge (must exist in the caller's
            tenant).
        memory_b_id:
          type: string
          format: uuid
          description: Second memory in the proposed merge (must differ from memory_a_id).
        resolution_note:
          type:
            - string
            - 'null'
          description: Optional free-text note explaining the proposal.
          x-go-pointer: true
    ResolveMergeProposalRequest:
      type: object
      required:
        - surviving_memory_id
      properties:
        surviving_memory_id:
          type: string
          format: uuid
          description: The memory to keep. Must match memory_a_id or memory_b_id.
        resolution_note:
          type:
            - string
            - 'null'
          description: Optional free-text note explaining the resolution.
          x-go-pointer: true
        merged_content:
          type:
            - string
            - 'null'
          description: Optional merged text to apply to the surviving memory.
          x-go-pointer: true
    RejectMergeProposalRequest:
      type: object
      properties:
        resolution_note:
          type:
            - string
            - 'null'
          description: Optional free-text note explaining the rejection.
          x-go-pointer: true
    MergeProposal:
      type: object
      properties:
        id:
          type: string
        tenant_id:
          type: string
        proposer_user_id:
          type: string
        memory_a_id:
          type: string
        memory_b_id:
          type: string
        surviving_memory_id:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - open
            - resolved
            - rejected
        proposed_at:
          type: string
          format: date-time
        resolved_at:
          type:
            - string
            - 'null'
          format: date-time
        resolved_by_user_id:
          type:
            - string
            - 'null'
        resolution_note:
          type:
            - string
            - 'null'
    MergeProposalListResponse:
      type: object
      properties:
        proposals:
          type: array
          items:
            $ref: '#/components/schemas/MergeProposal'
        total:
          type: integer
    CreateMemoryRelationshipRequest:
      type: object
      required:
        - target_memory_id
        - relationship_type
      additionalProperties: false
      properties:
        target_memory_id:
          type: string
          format: uuid
          description: >-
            Target memory id (UUID). The source memory id is taken from the path
            parameter.
          example: mem_neighbor1234
        relationship_type:
          type: string
          description: |
            Closed enum. The server validates this value against the list below
            and rejects any other value with HTTP 400 validation_error.
          enum:
            - caused_by
            - related_to
            - contradicts
            - supports
            - derived_from
            - part_of
          example: derived_from
        strength:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: >
            Optional edge weight in [0, 1]. Server applies a default if omitted

            (handler stores `nil` and the persistence layer chooses the
            default).
          example: 0.85
          x-go-pointer: true
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: |
            Optional freeform JSON object attached to the edge. May be omitted
            (sent as null) — the server stores either the object or no metadata.
          example:
            confidence: 0.9
            source: manual
    MemoryRelationship:
      type: object
      required:
        - id
        - tenant_id
        - source_memory_id
        - target_memory_id
        - relationship_type
        - strength
        - metadata
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Server-assigned relationship id.
          example: rel_01H7abc12345
        tenant_id:
          type: string
          format: uuid
          description: Tenant that owns this relationship (server-enforced).
          example: ten_01H7abc12345
        source_memory_id:
          type: string
          format: uuid
          description: Source memory id (the memory in the request path on create).
          example: mem_01H7abc12345
        target_memory_id:
          type: string
          format: uuid
          description: Target memory id.
          example: mem_neighbor1234
        relationship_type:
          type: string
          description: |
            Closed enum (server-validated on create). Same value as written by
            the caller — the server does not normalize.
          enum:
            - caused_by
            - related_to
            - contradicts
            - supports
            - derived_from
            - part_of
          example: derived_from
        strength:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Edge weight in [0, 1]. Always present in responses.
          example: 0.85
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: |
            Freeform JSON attached to the edge. May be `null` when the caller
            omitted it on create (json.RawMessage emits null in that case).
            Required in the response payload (always serialized) but the value
            itself may be null.
          example:
            confidence: 0.9
            source: manual
        created_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp.
          example: '2026-06-01T12:34:56Z'
    MemoryRelationshipTraversalItem:
      type: object
      required:
        - memory_id
        - depth
        - strength_product
      properties:
        memory_id:
          type: string
          description: Id of the memory reached by traversing from the source.
          example: mem_neighbor1234
        depth:
          type: integer
          minimum: 1
          maximum: 3
          description: Number of relationship hops from the source memory.
          example: 1
        strength_product:
          type: number
          format: double
          description: |
            Product of edge strengths along the traversal path (in [0,1]).
            A direct hop has strength_product equal to that edge's strength.
          example: 0.85
    MemoryRelationshipListResponse:
      type: object
      required:
        - relationships
        - count
      properties:
        relationships:
          type: array
          items:
            $ref: '#/components/schemas/MemoryRelationshipTraversalItem'
        count:
          type: integer
          minimum: 0
          description: Number of items in `relationships` (not a paginated total).
          example: 2
    CitationFreshness:
      type: object
      required:
        - stale
      description: |
        Forward-compat staleness tracking for a Citation. In v1, every citation
        is non-stale (stale=false, stale_since=null). Phase 08 will populate
        these fields when document supersession is implemented.
      properties:
        stale:
          type: boolean
          description: Whether the source document has been superseded by a newer version.
          example: false
        stale_since:
          type:
            - string
            - 'null'
          format: date-time
          description: ISO 8601 timestamp when the source was marked stale. Null in v1.
          example: null
    Citation:
      type: object
      required:
        - document_id
        - document_name
        - page
        - snippet
        - source_uri
        - source_id
        - confidence
        - version
        - freshness
      description: >
        A single document-chunk provenance link attached to a memory. Created by

        the extraction pipeline when a document is ingested; deduplicated by

        `source_uri` (or document content hash when source_uri is absent).


        **Stable identifier:** `source_uri` is the stable dedup identifier for a
        Citation

        across the system. There is no separate `id` field — clients that need a

        stable React key or cache key should use `source_uri` (or a composite of

        `source_uri` + `page` when discriminating chunk-level citations of the

        same document).


        **Neo4j path (v1 default):** `document_id` and `source_id` are returned

        as empty strings because the Neo4j-sourced read path does not carry

        Postgres UUIDs. They remain in the schema for forward compatibility with

        the Postgres fallback path.
      properties:
        document_id:
          type: string
          description: |
            Postgres document UUID. Empty string on the Neo4j read path (v1
            default per D-08); populated only when the Postgres fallback path
            is active. NOT a stable identifier — use `source_uri` instead.
          example: ''
        document_name:
          type: string
          description: Human-readable display name of the source document.
          example: Angareion Architecture Guide
        page:
          type: integer
          minimum: 0
          description: 1-indexed page number within the source document; 0 if unknown.
          example: 4
        snippet:
          type: string
          description: ~300 character excerpt from the originating chunk content.
          example: The sensing pipeline processes CloudEvents...
        source_uri:
          type: string
          description: |
            Absolute HTTPS deep-link to the source chunk. This is the **stable
            dedup identifier** for the Citation (Phase 01 NEO-04). Two memories
            citing the same `source_uri` share one Citation node in Neo4j.
          example: https://storage.example.com/docs/guide.pdf#chunk=42
        source_id:
          type: string
          description: |
            Ingestion source UUID — used for `/knowledge/sources/:source_id`
            deep-link. Empty string on the Neo4j read path (v1).
          example: ''
        confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Extraction confidence in the citation snippet (0.0–1.0).
          example: 0.85
        version:
          type: integer
          minimum: 1
          description: >-
            Document version. Always 1 in v1; Phase 08 fills supersession
            values.
          example: 1
        freshness:
          $ref: '#/components/schemas/CitationFreshness'
    MemoryCitationListResponse:
      type: object
      required:
        - citations
        - total
      description: |
        Response body of `GET /v1/memories/{id}/citations`. `citations` may be
        an empty array for memories created manually without a source document
        (this is NOT an error condition).
      properties:
        citations:
          type: array
          items:
            $ref: '#/components/schemas/Citation'
        total:
          type: integer
          minimum: 0
          description: Total number of citations for this memory.
          example: 2
    SendMessageRequest:
      type: object
      required:
        - channel
        - text
      properties:
        channel:
          type: string
          description: >
            Channel to post to. Accepts `#channel-name` (resolved by the server
            using the caller's tenant scope) or a bare channel UUID. The
            `c:<uuid>` shorthand is a client-side convention stripped by SDK
            clients before the HTTP call — the wire format is `#name` or a bare
            UUID.
          example: '#general'
        text:
          type: string
          minLength: 1
          maxLength: 10000
          description: Message text content.
          example: Hello, team!
        mentions:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/MentionEntry'
          description: |
            Participants to mention. Must be channel members.
            Non-member mentions return 422 with `invalid_mentions`.
    SendDMRequest:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 10000
          example: Hey, quick question
    ResolveDMResponse:
      type: object
      required:
        - channel_id
      description: >-
        Response from GET /dm/{participant_ref}. Returns the resolved DM channel
        ID.
      properties:
        channel_id:
          type: string
          description: UUID of the DM channel between the caller and the peer.
          example: ch_01H7abc12345
    SendMessageResponse:
      type: object
      required:
        - event_id
        - status
      properties:
        event_id:
          type: string
          description: CloudEvents ID of the published event.
          example: evt_01H7abc12345
        status:
          type: string
          enum:
            - accepted
          example: accepted
        channel_id:
          type: string
          description: Channel the message was posted to (present for DM responses).
          example: ch_01H7abc12345
        delivery_id:
          type: string
          format: uuid
          description: >-
            Delivery UUID for the posted message. Use with inbox.read to advance
            the read pointer.
          example: 550e8400-e29b-41d4-a716-446655440000
    MentionEntry:
      type: object
      required:
        - type
        - id
      properties:
        type:
          type: string
          enum:
            - agent
            - org_admin
          example: agent
        id:
          type: string
          description: Agent ID or org admin ID.
          example: ag_01H7abc12345
    InvalidMentionsError:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - invalid_mentions
          properties:
            code:
              type: string
              example: invalid_mentions
            message:
              type: string
              example: One or more mentions are not channel members
            invalid_mentions:
              type: array
              items:
                $ref: '#/components/schemas/MentionEntry'
    MessageItem:
      type: object
      required:
        - id
        - channel_id
        - sender_type
        - sender_id
        - text
        - created_at
      properties:
        id:
          type: string
          description: 'Delivery UUID (stable — used as SSE id: field)'
          example: 550e8400-e29b-41d4-a716-446655440000
        channel_id:
          type: string
          example: ch_01H7abc12345
        sender_type:
          type: string
          enum:
            - agent
            - org_admin
          example: agent
        sender_id:
          type: string
          example: ag_01H7abc12345
        text:
          type: string
          example: Hello, team!
        mentions:
          type: array
          items:
            $ref: '#/components/schemas/MentionEntry'
        created_at:
          type: string
          format: date-time
          example: '2026-06-08T12:00:00Z'
    MessageListResponse:
      type: object
      required:
        - messages
        - has_more
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/MessageItem'
        has_more:
          type: boolean
          description: True if more messages exist before the oldest item in this page.
          example: true
        next_cursor:
          type: string
          description: Opaque cursor. Present when `has_more` is true.
          example: eyJkZWxpdmVyeV9pZCI6Ii4uLiJ9
    InboxItem:
      type: object
      required:
        - channel_id
        - channel_name
        - channel_type
        - unread_count
      properties:
        channel_id:
          type: string
          example: ch_01H7abc12345
        channel_name:
          type: string
          example: ops-alerts
        channel_type:
          type: string
          enum:
            - open
            - private
            - direct
            - self
          example: open
        unread_count:
          type: integer
          minimum: 0
          example: 3
        last_message:
          $ref: '#/components/schemas/MessageItem'
    InboxResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/InboxItem'
    MentionCandidate:
      type: object
      required:
        - type
        - id
      properties:
        type:
          type: string
          enum:
            - agent
            - org_admin
        id:
          type: string
        display_name:
          type: string
    MentionCandidateListResponse:
      type: object
      required:
        - candidates
      properties:
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/MentionCandidate'
    DeviceFlowIssueRequest:
      type: object
      description: Empty request body; agent_id is in the path, tenant from auth.
      additionalProperties: false
    DeviceFlowIssueResponse:
      type: object
      required:
        - user_code
        - expires_at
        - server_now
        - poll_url
      properties:
        user_code:
          type: string
          description: Crockford-32 user-visible code, format `XXXX-XXXX`.
          pattern: >-
            ^[BCDFGHJKMNPQRSTVWXYZ23456789]{4}-[BCDFGHJKMNPQRSTVWXYZ23456789]{4}$
          example: BCDF-GHJK
        expires_at:
          type: string
          format: date-time
          description: Code expiry (10 min after issuance).
          example: '2026-06-10T18:10:00Z'
        server_now:
          type: string
          format: date-time
          description: Server clock at response time (PROTO-08, Pitfall 9).
          example: '2026-06-10T18:00:00Z'
        poll_url:
          type: string
          format: uri
          description: Convenience URL the dashboard MAY use to poll state.
    DeviceFlowClientInfo:
      type: object
      description: >
        User-visible context only — NEVER a security boundary (SEC-08, Pitfall
        4).

        Captured into `device_codes.claim_metadata.client_info` during `:claim`.
      required:
        - name
        - version
      properties:
        name:
          type: string
          example: Claude Desktop
        version:
          type: string
          example: 1.4.2
        signature:
          type:
            - string
            - 'null'
          description: Reserved for future MCP-client attestation; nullable in v1.0.
        host:
          type:
            - string
            - 'null'
          example: macbook-pro.local
    DeviceFlowPollWaiting:
      type: object
      required:
        - state
        - user_code
        - expires_at
        - server_now
      properties:
        state:
          type: string
          enum:
            - waiting
        user_code:
          type: string
          example: BCDF-GHJK
        expires_at:
          type: string
          format: date-time
        server_now:
          type: string
          format: date-time
    DeviceFlowPollVerifying:
      type: object
      required:
        - state
        - claim
        - server_now
      properties:
        state:
          type: string
          enum:
            - verifying
        claim:
          type: object
          required:
            - client_name
            - client_version
          properties:
            client_name:
              type: string
              example: Claude Desktop
            client_version:
              type: string
              example: 1.4.2
            client_signature:
              type:
                - string
                - 'null'
            host:
              type:
                - string
                - 'null'
        server_now:
          type: string
          format: date-time
    DeviceFlowPollConnected:
      type: object
      description: |
        The `connected` poll variant. **MUST NOT** include an `api_key` property
        — credentials only flow through `:complete` (D-02 / NEEDS-DECISION #2).
        ROADMAP success criterion #2 makes this assertion explicit.
      required:
        - state
        - session_id
        - agent_id
        - server_now
      additionalProperties: false
      properties:
        state:
          type: string
          enum:
            - connected
        session_id:
          type: string
          example: ses_01HK7SES0DEF1GHIJ2KLM3NOPQ
        agent_id:
          type: string
          example: ag_01H7abc12345
        server_now:
          type: string
          format: date-time
    DeviceFlowPollDenied:
      type: object
      required:
        - state
        - denied_at
        - server_now
      properties:
        state:
          type: string
          enum:
            - denied
        denied_at:
          type: string
          format: date-time
        server_now:
          type: string
          format: date-time
    DeviceFlowPollExpired:
      type: object
      required:
        - state
        - expired_at
        - server_now
      properties:
        state:
          type: string
          enum:
            - expired
        expired_at:
          type: string
          format: date-time
        server_now:
          type: string
          format: date-time
    DeviceFlowPollResponse:
      description: |
        Discriminated union over `state`. Every variant carries `server_now`.
        See WIRE-SPEC.md §1.2 and §3 (state machine).
      oneOf:
        - $ref: '#/components/schemas/DeviceFlowPollWaiting'
        - $ref: '#/components/schemas/DeviceFlowPollVerifying'
        - $ref: '#/components/schemas/DeviceFlowPollConnected'
        - $ref: '#/components/schemas/DeviceFlowPollDenied'
        - $ref: '#/components/schemas/DeviceFlowPollExpired'
      discriminator:
        propertyName: state
        mapping:
          waiting: '#/components/schemas/DeviceFlowPollWaiting'
          verifying: '#/components/schemas/DeviceFlowPollVerifying'
          connected: '#/components/schemas/DeviceFlowPollConnected'
          denied: '#/components/schemas/DeviceFlowPollDenied'
          expired: '#/components/schemas/DeviceFlowPollExpired'
    DeviceFlowClaimRequest:
      type: object
      required:
        - user_code
        - client_info
      properties:
        user_code:
          type: string
          pattern: >-
            ^[BCDFGHJKMNPQRSTVWXYZ23456789]{4}-[BCDFGHJKMNPQRSTVWXYZ23456789]{4}$
          example: BCDF-GHJK
        client_info:
          $ref: '#/components/schemas/DeviceFlowClientInfo'
    DeviceFlowClaimResponse:
      type: object
      required:
        - claim_id
      properties:
        claim_id:
          type: string
          example: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
    DeviceFlowConfirmRequest:
      type: object
      required:
        - claim_id
        - action
      properties:
        claim_id:
          type: string
          example: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
        action:
          type: string
          enum:
            - approve
            - reject
    DeviceFlowConfirmResponse:
      type: object
      required:
        - state
        - confirmed_at
      properties:
        state:
          type: string
          enum:
            - approved
            - denied
        confirmed_at:
          type: string
          format: date-time
    DeviceFlowCompleteRequest:
      type: object
      required:
        - claim_id
      properties:
        claim_id:
          type: string
          example: clm_01HK7CLM0DEF1GHIJ2KLM3NOPQ
    DeviceFlowCompleteResponse:
      type: object
      description: |
        Credentials returned exactly ONCE. The plaintext `api_key` is never
        re-derivable; the `access_token` is a fresh JWT with a 1-hour TTL
        sourced from `auth.AgentIssuer.Issue`. `expires_at = server_now + 1h`.
        Per CLAUDE.md guardrail: `access_token` MUST live in memory only,
        never localStorage.
      required:
        - session_id
        - agent_id
        - api_key
        - access_token
        - expires_at
        - server_now
      properties:
        session_id:
          type: string
          example: ses_01HK7SES0DEF1GHIJ2KLM3NOPQ
        agent_id:
          type: string
          example: ag_01H7abc12345
        api_key:
          type: string
          description: >-
            Plaintext API key (returned ONCE; SHA-256 hash is what's persisted
            server-side).
          example: ak_live_AAAA1234567890abcdefghijklmnop
        access_token:
          type: string
          description: Fresh JWT (1-hour TTL).
        expires_at:
          type: string
          format: date-time
          description: server_now + 1h.
        server_now:
          type: string
          format: date-time
    SessionHeartbeatResponse:
      type: object
      required:
        - last_seen_at
        - next_allowed_at
        - server_now
      properties:
        last_seen_at:
          type: string
          format: date-time
        next_allowed_at:
          type: string
          format: date-time
          description: Earliest server time the next heartbeat is allowed (1/min limit).
        server_now:
          type: string
          format: date-time
    AgentSessionItem:
      type: object
      required:
        - session_id
        - state
        - client_name
        - client_version
        - connected_at
        - last_activity_at
      description: |
        A single agent session record returned by GET /agents/{id}/sessions.
        State is derived server-side from last_seen_at. client_name and
        client_version are sourced from device_codes.claim_metadata; they
        default to "—" when the session pre-dates device-flow stamping.
      properties:
        session_id:
          type: string
          example: 550e8400-e29b-41d4-a716-446655440000
        state:
          type: string
          enum:
            - active
            - idle
          description: '`active` if last_seen_at within 24h, `idle` otherwise.'
        client_name:
          type: string
          example: Claude Desktop
          description: MCP client display name from device_codes; '—' if not available.
        client_version:
          type: string
          example: 2.1.0
          description: MCP client version from device_codes; '—' if not available.
        connected_at:
          type: string
          format: date-time
          description: >-
            ISO 8601 UTC timestamp when the session was created
            (sessions.created_at).
        last_activity_at:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp of last heartbeat (sessions.last_seen_at).
    DeviceFlowError:
      type: object
      description: |
        Structured-error envelope for the device-flow endpoints (PROTO-07).
        `retry_after` is OPTIONAL (omitted when not applicable, present on
        rate-limit-bearing errors). Note: `code_expired` is NOT in this
        vocabulary — code expiry uses the canonical RFC 8628 `expired_token`.
      required:
        - error
        - error_description
      properties:
        error:
          type: string
          enum:
            - authorization_pending
            - slow_down
            - expired_token
            - access_denied
            - code_already_claimed
            - code_not_found
            - code_superseded
            - claim_unavailable
        error_description:
          type: string
        retry_after:
          type: integer
          minimum: 0
          description: Seconds before the next attempt is allowed (rate-limit responses).
    IngestDocumentRequest:
      type: object
      required:
        - file_content
        - filename
        - mime_type
      properties:
        file_content:
          type: string
          description: Standard base64-encoded bytes of the document.
        filename:
          type: string
          description: Original filename including extension.
          example: architecture-guide.pdf
        mime_type:
          type: string
          enum:
            - application/pdf
            - >-
              application/vnd.openxmlformats-officedocument.wordprocessingml.document
            - >-
              application/vnd.openxmlformats-officedocument.presentationml.presentation
            - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
            - text/html
            - text/markdown
            - text/plain
          description: MIME type of the document.
        sensitivity:
          type: string
          enum:
            - public
            - internal
            - confidential
            - restricted
          description: >-
            Optional sensitivity label. Server defaults to "internal" when
            absent.
        audience:
          type: object
          description: >
            Optional 4-dimension audience tag JSONB.

            Format: {regions:[...], industries:[...], product_lines:[...],
            segments:[...]}.

            Server defaults to {} when absent.
          additionalProperties:
            type: array
            items:
              type: string
    IngestDocumentResponse:
      type: object
      required:
        - accepted
      properties:
        accepted:
          type: boolean
          description: true on 202, false on error.
        document_id:
          type: string
          description: UUID of the created document (present when accepted=true).
          example: doc_01H7abc12345
        status:
          type: string
          enum:
            - pending
            - indexed
            - failed
          description: Initial indexing status (present when accepted=true).
        error_code:
          type: string
          enum:
            - permission_denied
            - bad_file_content
            - no_auth
            - default_source_unavailable
          description: Structured error code (present when accepted=false).
        hint:
          type: string
          description: Human-readable guidance for the error (present when accepted=false).
    DocumentItem:
      type: object
      required:
        - document_id
        - filename
        - mime_type
        - status
        - ingested_at
        - ingestion_source_id
        - ingestion_source_name
      properties:
        document_id:
          type: string
          format: uuid
        filename:
          type: string
        mime_type:
          type: string
        status:
          type: string
          enum:
            - pending
            - indexed
            - failed
        ingested_at:
          type: string
          format: date-time
        ingestion_source_id:
          type: string
          format: uuid
        ingestion_source_name:
          type: string
    DocumentListResponse:
      type: object
      required:
        - documents
        - total
      properties:
        documents:
          type: array
          items:
            $ref: '#/components/schemas/DocumentItem'
        total:
          type: integer
          description: Total count of documents across all sources.
    DocumentChunkItem:
      type: object
      required:
        - chunk_id
        - document_id
        - chunk_index
        - text
        - created_at
      properties:
        chunk_id:
          type: string
          format: uuid
        document_id:
          type: string
          format: uuid
        page_number:
          type: integer
          description: Page number the chunk was extracted from (0-indexed).
        char_offset:
          type: integer
          description: Character offset within the page.
        char_length:
          type: integer
          description: Length of the chunk in characters.
        chunk_index:
          type: integer
          description: Sequential chunk index within the document (0-indexed).
        text:
          type: string
          description: Extracted text content of the chunk.
        created_at:
          type: string
          format: date-time
    DocumentChunkListResponse:
      type: object
      required:
        - chunks
        - total
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/DocumentChunkItem'
        total:
          type: integer
    DeleteDocumentResponse:
      type: object
      required:
        - deleted
        - document_id
      properties:
        deleted:
          type: boolean
          description: true on successful deletion, false when document not found.
        document_id:
          type: string
          format: uuid
          description: The document UUID that was (or was not) deleted.
        error_code:
          type: string
          enum:
            - not_found
          description: Structured error code (present when deleted=false).
    SpecialistItem:
      type: object
      required:
        - name
        - version_num
        - description
        - updated_at
      properties:
        name:
          type: string
          description: Specialist slug (unique within tenant).
        version_num:
          type: integer
          description: Monotonically increasing version counter.
        description:
          type: string
          description: Short human-readable description.
        updated_at:
          type: string
          format: date-time
    SpecialistLoadResponse:
      type: object
      description: |
        Successful load returns specialist fields directly.
        Error returns {error: {code, message}} — NOT an HTTP error code.
      properties:
        name:
          type: string
        version_num:
          type: integer
        description:
          type: string
        system_prompt:
          type: string
        memory_search_directives:
          type: string
        output_format_rules:
          type: string
        updated_at:
          type: string
          format: date-time
        error:
          type: object
          description: Present when specialist could not be loaded.
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - not_found
                - unavailable
            message:
              type: string
    SpecialistListResponse:
      type: object
      required:
        - specialists
      properties:
        specialists:
          type: array
          items:
            $ref: '#/components/schemas/SpecialistItem'
          description: Alphabetically ordered by the server (do not re-sort client-side).
    SpecialistUnloadResponse:
      type: object
      required:
        - unloaded
        - name
      properties:
        unloaded:
          type: boolean
          description: Always true (idempotent — success even if no session existed).
        name:
          type: string
          description: The specialist name that was unloaded.
        error:
          type: object
          description: Present only when the session service was unavailable.
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - unavailable
            message:
              type: string
    ComposeRequest:
      type: object
      required:
        - template_id
        - run_id
      properties:
        template_id:
          type: string
          description: 'Workflow template slug (v1 value: "launch-a-feature").'
          example: launch-a-feature
        run_id:
          type: string
          format: uuid
          description: |
            UUID identifying this workflow run. Submitting the same run_id twice
            while a compose is in flight returns a 409 duplicate_run response.
          example: 550e8400-e29b-41d4-a716-446655440000
        context_memory_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Optional list of memory UUIDs to include as additional context.
        launch_brief_text:
          type: string
          description: >
            Optional free-text description of the feature or topic being
            composed.

            Used as part of the prompt context.
        prompt_id:
          type: string
          description: Reserved for future recipe variants; leave empty for v1.
    ComposeResponse:
      type: object
      required:
        - draft_text
        - cited_memory_ids
      properties:
        draft_text:
          type: string
          description: LLM-generated synthesis draft.
        cited_memory_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Memory UUIDs retrieved and cited in the synthesis.
    SaveHandoffRequest:
      type: object
      required:
        - name
      additionalProperties: false
      description: |
        The save body. There is deliberately no `tenant_id` and no `agent_id`:
        both come from the JWT, and an unknown top-level key is rejected with
        400 rather than silently ignored.
      properties:
        name:
          type: string
          description: |
            Short, human-typeable, and reusable across the life of the work.
            Saving to an existing name overwrites that name's payload in place.
          example: q3-pricing
        summary:
          type: string
          description: One-line description of what this working state covers.
        payload:
          type: object
          additionalProperties: true
          description: |
            The structured working state. The properties below are the KNOWN
            fields — they are validated for renderability, and unrecognised
            fields are preserved untouched rather than rejected, so client-side
            extraction can add fields without a server release.

            `blockers` and each `decisions[].rationale` are copied verbatim from
            the session by the extracting client and are never re-summarised.
          properties:
            completed:
              type: array
              items:
                type: string
              description: What is done and does not need to be redone.
            remaining:
              type: array
              items:
                type: string
              description: What is not done yet.
            blockers:
              type: array
              items:
                type: string
              description: |
                What is in the way of finishing. Copied verbatim from the
                session; never paraphrased.
            decisions:
              type: array
              description: Decisions taken, each with the reasoning behind it.
              items:
                type: object
                required:
                  - title
                properties:
                  title:
                    type: string
                  rationale:
                    type: string
                    description: |
                      Copied verbatim from the session; never paraphrased. A
                      paraphrased rationale invites a resumed session to
                      re-litigate a settled decision.
            next_action:
              type: string
              description: The single concrete thing to do first on resume.
            context_notes:
              type: string
              description: |
                Anything a fresh session would otherwise have to rediscover.
        citations:
          type: array
          description: |
            Memory IDs already known to be relevant to this work. Each entry is
            an OBJECT, not a bare UUID: the server snapshots each cited memory's
            title at save time and stores it alongside the ID so the two cannot
            drift apart.

            A client-supplied `title` is accepted and ignored — the stored title
            is always the server's snapshot. Accepting the field keeps a
            read-modify-write cycle over a previously-returned handoff working.

            A citation whose memory does not resolve is stored with an empty
            title and the save still succeeds.
          items:
            type: object
            required:
              - memory_id
            properties:
              memory_id:
                type: string
                format: uuid
                description: The cited memory's UUID.
              title:
                type: string
                description: Accepted and ignored on save; server-snapshotted.
        artifacts:
          type: array
          items:
            type: string
          description: |
            File paths, URLs, and branch names touched by this work. Stored as
            supplied and never validated for inner structure, so a client may
            send richer entries than the documented string form.
        origin_client:
          type: string
          x-go-pointer: true
          description: |
            Identifier of the surface that saved this handoff, for example
            `claude-code`. Each surface sets a fixed literal for itself.
          example: claude-code
        scope:
          type: string
          enum:
            - agent
          default: agent
          description: |
            `agent` is the only value accepted in v1. A `team` value is rejected
            with 400 before anything is persisted; team-scoped handoff sharing
            is not available in v1.
    ResumeHandoffRequest:
      type: object
      required:
        - query
      additionalProperties: false
      properties:
        query:
          type: string
          description: |
            An exact handoff name, a handoff UUID, or a free-text description of
            the work in your own words when the exact name is not known.
          example: the thing where we dropped the enterprise tier
    HandoffCitation:
      type: object
      required:
        - memory_id
        - title
      description: |
        A cited memory ID paired with the title snapshotted at save time. The
        title is empty when the memory did not resolve for the saving agent.
      properties:
        memory_id:
          type: string
          format: uuid
        title:
          type: string
    Handoff:
      type: object
      description: The full single-handoff representation.
      required:
        - id
        - name
        - summary
        - payload
        - citations
        - artifacts
        - scope
        - version_num
        - expires_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        summary:
          type: string
        payload:
          type: object
          additionalProperties: true
          description: |
            Returned byte-for-byte as it was supplied on save, including any
            fields not named in SaveHandoffRequest.
        citations:
          type: array
          items:
            $ref: '#/components/schemas/HandoffCitation'
        artifacts:
          type: array
          items:
            type: string
          description: Returned byte-for-byte as supplied on save.
        origin_client:
          type:
            - string
            - 'null'
        scope:
          type: string
          enum:
            - agent
        version_num:
          type: integer
          description: |
            Incremented by the store each time this name is saved over. Starts
            at 1.
        pinned_at:
          type:
            - string
            - 'null'
          format: date-time
        last_resumed_at:
          type:
            - string
            - 'null'
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: |
            Reset to a full TTL from the moment of a successful resume, not
            extended from whatever remained.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    HandoffSummary:
      type: object
      description: |
        The list representation. Deliberately omits `payload`, which can be
        large and is not needed to choose between handoffs.
      required:
        - id
        - name
        - summary
        - scope
        - version_num
        - pinned
        - expires_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        summary:
          type: string
        origin_client:
          type:
            - string
            - 'null'
        scope:
          type: string
          enum:
            - agent
        version_num:
          type: integer
        pinned:
          type: boolean
          description: Convenience mirror of whether `pinned_at` is set.
        pinned_at:
          type:
            - string
            - 'null'
          format: date-time
        last_resumed_at:
          type:
            - string
            - 'null'
          format: date-time
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    HandoffCandidate:
      type: object
      required:
        - id
        - name
        - summary
        - updated_at
      description: |
        One entry of the ambiguous-resume response. Carries only what is needed
        to choose; the payload is deliberately absent.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        summary:
          type: string
        updated_at:
          type: string
          format: date-time
    ResumeCandidatesResponse:
      type: object
      required:
        - resumed
        - candidates
        - message
      description: |
        The ambiguous-resume body, served as bare `application/json`. Nothing
        was resumed: no timestamp was stamped, no expiry was reset, and no brief
        was rendered.
      properties:
        resumed:
          type: boolean
          enum:
            - false
          description: |
            Always `false` here. Stated explicitly so a client reading only the
            HTTP status cannot mistake 200 for a successful resume.
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/HandoffCandidate'
        message:
          type: string
          description: Human-readable explanation of why nothing was resumed.
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code from a fixed enum.
              enum:
                - validation_error
                - validation_failed
                - invalid_event
                - invalid_key
                - key_expired
                - agent_inactive
                - authentication_failed
                - unauthorized
                - forbidden
                - insufficient_scope
                - not_found
                - agent_not_found
                - memory_not_found
                - interest_not_found
                - channel_not_found
                - key_not_found
                - dlq_item_not_found
                - feed_not_found
                - member_not_found
                - conflict
                - already_member
                - max_keys_reached
                - channel_name_taken
                - payload_too_large
                - business_rule_violation
                - invalid_pattern
                - invalid_scopes
                - rate_limited
                - internal_error
                - service_unavailable
                - publish_failed
            message:
              type: string
              description: Human-readable explanation. Safe to surface to end users.
            request_id:
              type: string
              description: Correlates with the X-Request-Id response header.
              example: req_01H7abc12345
            details:
              type: object
              additionalProperties: true
              description: Optional structured details (e.g., validation field map).
      example:
        error:
          code: validation_error
          message: name is required
          request_id: req_01H7abc12345
          details:
            field: name
  headers:
    XRequestId:
      description: Per-request correlation ID. Echo this in support tickets.
      schema:
        type: string
        example: req_01H7abc12345
    XRateLimitLimit:
      description: Rate-limit ceiling for this credential (calls per window).
      schema:
        type: integer
        example: 100
    XRateLimitRemaining:
      description: Calls remaining in the current rate-limit window.
      schema:
        type: integer
        example: 87
    XRateLimitReset:
      description: Unix epoch seconds when the rate-limit window resets.
      schema:
        type: integer
        example: 1735689600
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: >
        Client-supplied key making POST/PATCH operations idempotent (per PRD-03
        §4.1).

        Repeated requests with the same key return the original response without

        executing again. Server retains the key for at least 24 hours.
      example: idem_01H7abc12345
