> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beeos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# List messages in a conversation (paged by offset).

> Returns the durable conversation log. By default the response
EXCLUDES ephemeral streaming chunks (`agent_reply_delta`,
`agent_thought_chunk`, `agent_message_chunk`) — only finalized
turns and tool messages are returned, so UIs render a clean
transcript without pagination blowing up on token-level chunks.

Set `include_deltas=true` to also receive the per-token chunks
(only present when an in-flight turn is still streaming). Note
that `latest_offset` ALWAYS reflects the true server-side max
offset across both filtered and unfiltered rows, so resuming
with `since=<latest_offset>` is safe regardless of the filter.

SSE `/events` is unaffected by this filter — live consumers
receive every envelope including deltas as they happen.




## OpenAPI

````yaml /openapi/beeos-platform-v1.yaml get /api/v1/agents/{agentId}/conversations/{convId}/messages
openapi: 3.1.0
info:
  title: BeeOS OpenAPI (user contract)
  version: 1.1.0
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  description: |
    The **BeeOS OpenAPI** user contract. The sole implementer is
    **[`services/openapi-gateway`](../services/openapi-gateway)** (`openapi-gw`,
    local `:8095`). SDKs (Go / TypeScript) published under
    [`sdks/beeos-ai-sdk-go`](../../sdks/beeos-ai-sdk-go) and
    [`sdks/beeos-ai-sdk`](../../sdks/beeos-ai-sdk) are generated directly from
    this document.

    The main user Gateway (`services/gateway`, `:9080`) serves web / mobile /
    desktop clients and is **not** described by this document. It maintains its
    own route set independent of the SDK contract.

    **Auth:** every operation requires either a user **JWT** or a **`oag_`**
    User API Key, both on `Authorization: Bearer`. Agent-level protocol
    endpoints (A2A JSON-RPC with `bak_`, MCP) live in
    [beeos-agent-integration-v1.yaml](beeos-agent-integration-v1.yaml) and are
    hosted by A2A Gateway / MCP Gateway — not by `openapi-gateway`.

    ## Changelog

    ### 1.1.0 (ADR-0022 — dual-layer message storage)

    BREAKING for clients that depended on full streaming chunk replay
    via the persistent message log:

    * `GET /api/v1/agents/{agentId}/conversations/{convId}/messages`
      and `GET /api/v1/agents/{agentId}/tasks/{taskId}/messages` now
      **default-filter out** ephemeral streaming chunks
      (`agent_reply_delta`, `agent_thought_chunk`,
      `agent_message_chunk`). Add `?include_deltas=true` to opt back in.
    * `latest_offset` continues to reflect the TRUE server-side max
      offset across both filtered and unfiltered rows, so `since=` /
      cursor-style pagination is unaffected.
    * SSE `/events` is **NOT** affected — live consumers continue to
      receive every envelope as it happens.
    * Backwards-compatible recovery path: SSE `/events` may now emit
      a `backfill_truncated` event frame when the client reconnects
      with `Last-Event-ID` before the oldest retained chunk. SDKs
      that don't recognise the frame should treat it as a hint to
      resume from `oldest_redis_offset` instead (the field carrying
      the smallest offset still readable on the per-channel stream).
    * **Clarified contract**: SSE event `offset` is strictly monotonic
      per channel but **NOT guaranteed contiguous** (ADR-0022 §1.2).
      Producer-side write failures may leave small holes; clients MUST
      use `offset > since`, never `offset == since + 1`.

    BREAKING for clients that depended on the `oag_` API key scope
    vocabulary:

    * The per-route scope gate (`agents:read`, `agents:write`,
      `tasks:read`, `tasks:write`, `files:read`, `files:write`,
      `instances:read`, `instances:write`, plus the `admin:*` wildcard)
      has been **removed entirely**. The 37 routes that previously
      required scopes are now governed exclusively by owner-ACL — an
      `oag_` User API Key inherits full access to its owner's resources.
    * The `403 insufficient_scope` error code is no longer emitted.
      Existing callers that branched on it should fold the case into
      their generic 403 / `forbidden` handler.
    * SDK clients that explicitly passed `scopes` to `createAPIKey`
      should drop that argument. The Web / Desktop UIs no longer render
      scope badges on existing keys.
    * Existing `oag_` keys continue to work unchanged — the owner_id
      binding is preserved, and no key needs to be re-issued.

    BREAKING for clients of `GET /api/v1/providers` that read the
    `capabilities` object on the wire:

    * 7 of the 8 `capabilities` field names are now **camelCase**
      (`longRunning`, `browserUse`, `codeExec`, `fileSystem`,
      `customImage`, `maxDurationSec`, `costModel`) — matching this
      OpenAPI contract. Previous wire emitted **snake_case**
      (`long_running`, `browser_use`, …) because the server DTO bound
      `capabilities` to the raw protobuf message
      (`*pb.ProviderCapabilities`) whose auto-generated `json` tags
      use snake_case. SDK consumers (`@beeos-ai/sdk`,
      `github.com/beeos-ai/sdk-go`) generated from this spec were
      **already unable** to read any `capabilities` field — the
      camelCase property names produced by the generator never
      matched the snake_case the server actually sent. Raw HTTP
      callers that hand-coded against the old snake_case names must
      migrate; SDK callers gain access to the field set for the first
      time without code changes.
    * Note: the `device` boolean was always a single word — its wire
      name (`device`) is unchanged. Only the seven multi-word
      capability fields are affected by the rename.
    * `meta`, `id`, `name`, `description`, `version` are unchanged.
    * A new drift-guard (`internal/dto/contract_test.go`) now locks
      the server DTO to the spec property names, so the two cannot
      silently diverge again.

    ### 1.0.0

    Initial OpenAPI contract (ADR-001).
servers:
  - url: https://openapi.beeos.ai
    description: Production
  - url: https://openapi-staging.beeos.ai
    description: Staging
  - url: http://localhost:8095
    description: Local dev (goreman)
security:
  - bearerAuth: []
tags:
  - name: deploy
    description: Provider / region / model catalog.
  - name: instances
    description: Agent instance lifecycle (deploy, start, stop, restart, destroy).
  - name: computer
    description: |
      Synchronous desktop control (linux / windows / mac instances):
      screenshot, click, move, scroll, type, key. Coordinates are
      absolute device pixels (origin top-left); query
      `GET /instances/{id}/computer` for the live screen geometry and
      supported action set. Android instances are controlled via the
      `mobile` tag instead (calling `computer` on an android instance
      returns 404). Aligns with Anthropic / OpenAI "computer use".
  - name: mobile
    description: |
      Synchronous mobile (android) control: screenshot, tap, swipe,
      scroll, type, key, press_button. Coordinates are absolute device
      pixels. Query `GET /instances/{id}/mobile` for live geometry and
      supported actions. Desktop instances are controlled via the
      `computer` tag (calling `mobile` on a desktop instance returns 404).
  - name: agents
    description: Agent listing, details, and synchronous invocation.
  - name: tasks
    description: |
      Async agent tasks (the canonical task core — see ADR-0017
      `0017-unified-task-core`). `tasks` differs from `agents/invoke` in three
      ways: it returns 202 immediately with a `task_id` (no blocking on the
      reply), exposes a polling endpoint plus an SSE event stream, and supports
      cancel/continue lifecycle controls.
  - name: conversations
    description: >
      Long-lived multi-turn dialogs. Unlike tasks (single-shot, finite, terminal

      status), conversations are open until the caller closes them — messages

      are individually persisted and addressable by offset. Use the conversation

      API when you need to interleave caller and agent turns over an extended

      session; use `tasks` when a single prompt has a bounded reply with
      optional

      pause-and-resume.
  - name: files
    description: |
      Presigned URLs for caller uploads (`POST /files/presign-upload`) and
      result downloads (`GET /files/presign-download`) — the canonical
      attachment mechanism shared by `agents/{id}/invoke` and
      `agents/{id}/tasks`. Callers obtain a short-lived signed URL, upload the
      bytes directly to object storage (S3 / MinIO), and pass the resulting
      `file_id` in subsequent invoke / task requests via `attachments`.
paths:
  /api/v1/agents/{agentId}/conversations/{convId}/messages:
    parameters:
      - name: agentId
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
      - name: convId
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
    get:
      tags:
        - conversations
      summary: List messages in a conversation (paged by offset).
      description: |
        Returns the durable conversation log. By default the response
        EXCLUDES ephemeral streaming chunks (`agent_reply_delta`,
        `agent_thought_chunk`, `agent_message_chunk`) — only finalized
        turns and tool messages are returned, so UIs render a clean
        transcript without pagination blowing up on token-level chunks.

        Set `include_deltas=true` to also receive the per-token chunks
        (only present when an in-flight turn is still streaming). Note
        that `latest_offset` ALWAYS reflects the true server-side max
        offset across both filtered and unfiltered rows, so resuming
        with `since=<latest_offset>` is safe regardless of the filter.

        SSE `/events` is unaffected by this filter — live consumers
        receive every envelope including deltas as they happen.
      operationId: listConversationMessages
      parameters:
        - name: since
          in: query
          required: false
          description: |
            Return messages with offset > `since`. Combine with `limit` for
            paginated reads. Default is 0 (start of conversation).
          schema:
            type: integer
            format: int64
            minimum: 0
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int32
            minimum: 1
            maximum: 200
        - name: include_deltas
          in: query
          required: false
          description: |
            When `true`, include ephemeral streaming chunk rows
            (`agent_reply_delta`, `agent_thought_chunk`,
            `agent_message_chunk`) in the response. Default `false`.
            Accepts `true|1|yes` (case-insensitive); any other value
            is treated as `false`.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Page of messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListMessagesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        5XX:
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    ListMessagesResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          required:
            - data
          properties:
            data:
              type: object
              required:
                - messages
                - latest_offset
                - has_more
              properties:
                messages:
                  type: array
                  items:
                    $ref: '#/components/schemas/MessageDTO'
                latest_offset:
                  type: integer
                  format: int64
                  description: Channel's current max offset for "caught up" detection.
                next_since:
                  type: string
                  description: >-
                    Public offset cursor to pass as the next request's since
                    value.
                has_more:
                  type: boolean
                  description: True when another page is available via next_since.
    SuccessEnvelope:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          const: true
    MessageDTO:
      type: object
      required:
        - message_id
        - offset
        - type
      properties:
        message_id:
          type: string
        offset:
          type: integer
          format: int64
        type:
          type: string
        publisher_id:
          type: string
        in_reply_to:
          type: string
        payload:
          description: Opaque JSON payload of the message.
        created_at:
          type: string
          format: date-time
        body:
          type: string
          description: |
            Cumulative agent reply text. Mutated in place across PATCH
            updates while state == "streaming"; frozen when state
            transitions to a terminal value (`completed` / `failed` /
            `refused` / `cancelled`).
          x-beeos-status: ga
        state:
          $ref: '#/components/schemas/MessageState'
          description: |
            Envelope lifecycle (canonical vocabulary, see MessageState).
            Pre-v3 rows omit this field; v3 consumers should treat
            absent state as `completed`.
          x-beeos-status: ga
        stop_reason:
          $ref: '#/components/schemas/StopReason'
          description: |
            Terminal reason set when state becomes non-streaming
            (canonical vocabulary, see StopReason). Only meaningful
            when state ∈ {completed, failed, refused, cancelled}.
          x-beeos-status: ga
        parts:
          type: array
          description: |
            Structured part list for rich replies (thinking / tool_use /
            tool_result / file / source / custom). Empty / absent for
            plain text replies — consumers should render `body` only in
            that case. Discriminated union; see the Part schema for the
            per-type field set.
          items:
            $ref: '#/components/schemas/Part'
          x-beeos-status: ga
        updated_at:
          type: string
          format: date-time
          description: |
            Server's last-write timestamp (RFC3339 with nanosecond
            precision, UTC). Equals `created_at` after the initial POST
            and is bumped on every successful PATCH (streaming snapshot
            updates AND terminal state transitions). Terminal rows are
            immutable, so `updated_at` freezes once `state` leaves
            `streaming`. SSE replay clients SHOULD use this to ack
            progress checkpoints; REST clients SHOULD use it to detect
            mid-stream progress vs `created_at`-only first reads.
          x-beeos-status: ga
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: '#/components/schemas/Error'
    MessageState:
      type: string
      description: |
        Lifecycle state of a v3 message envelope. `streaming` is the
        sole non-terminal state; everything else is immutable.

        State transitions:
          - `streaming -> completed`  (normal LLM/agent finish)
          - `streaming -> failed`     (transport / model error)
          - `streaming -> refused`    (content filter / agent decline)
          - `streaming -> cancelled`  (user_stop / timeout)
        Once terminal, the row is immutable and any PATCH returns
        `410 Gone` with `code = message_terminal`.
      enum:
        - streaming
        - completed
        - failed
        - refused
        - cancelled
      x-beeos-status: ga
    StopReason:
      type: string
      description: |
        Reason for entering a terminal state. Borrowed from the
        Anthropic vocabulary (`end_turn` / `max_tokens` / `tool_use`)
        and augmented with platform-level terminations.

        `agent_lost` is set by the Message Service stale-streaming
        reaper when an agent crashes / disconnects without finalising
        its v3 envelope — the row is transitioned from
        `state=streaming` to `state=failed` after the configured
        idle threshold so blocking `/wait` callers and SSE consumers
        receive a terminal frame instead of hanging indefinitely.
      enum:
        - end_turn
        - max_tokens
        - tool_use
        - user_stop
        - timeout
        - error
        - refused
        - content_filter
        - agent_lost
      x-beeos-status: ga
    Part:
      description: |
        Discriminated union of structured supplementary content
        carried alongside the main `body` text. Six recognized
        shapes cover 99% of LLM/agent use cases; `custom` is the
        extension escape hatch.

        Consumers MUST default-tolerate unknown `type` values — new
        types are added by appending to the union and old consumers
        should treat them as `custom`.
      oneOf:
        - $ref: '#/components/schemas/PartThinking'
        - $ref: '#/components/schemas/PartToolUse'
        - $ref: '#/components/schemas/PartToolResult'
        - $ref: '#/components/schemas/PartFile'
        - $ref: '#/components/schemas/PartSource'
        - $ref: '#/components/schemas/PartCustom'
      discriminator:
        propertyName: type
        mapping:
          thinking:
            $ref: '#/components/schemas/PartThinking'
          tool_use:
            $ref: '#/components/schemas/PartToolUse'
          tool_result:
            $ref: '#/components/schemas/PartToolResult'
          file:
            $ref: '#/components/schemas/PartFile'
          source:
            $ref: '#/components/schemas/PartSource'
          custom:
            $ref: '#/components/schemas/PartCustom'
      x-beeos-status: ga
    Error:
      type: object
      required:
        - code
        - message
      properties:
        type:
          type: string
          description: |
            RFC 9457 problem-type slug. Most SDK consumers key on
            `code` rather than `type`; this field is kept for
            structural parity with the wider apierror catalogue.
          enum:
            - api_error
            - invalid_request_error
            - authentication_error
            - permission_error
            - rate_limit_error
            - not_found_error
            - conflict_error
            - validation_error
        code:
          type: string
          description: >
            Stable machine-readable error code. See

            [docs/reference/errors.md](https://github.com/beeos-ai/beeos/blob/main/docs/reference/errors.md)

            for the complete catalogue.
          examples:
            - agent_not_found
            - agent_service_unavailable
            - service_timeout
            - conflict
            - forbidden
            - invalid_param
            - rate_limited
            - internal_error
        message:
          type: string
          description: Human-readable explanation; presentation-only, never key on this.
        request_id:
          type: string
          description: |
            Per-request correlation id propagated end-to-end (also returned
            in the `X-Request-Id` response header). Include this value when
            reporting errors so the server-side trace can be located.
        param:
          type: string
          description: |
            Field path of the offending parameter for validation-style
            errors (`code` in `missing_param` / `invalid_param`). Empty for
            errors that don't reference a specific field.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: |
            Optional structured key-value hints for the client (retry
            strategy, related resource ids, etc.). Always a string-to-string
            map.
    PartThinking:
      type: object
      description: Chain-of-thought / reasoning snippet (model output, opt-in).
      required:
        - type
        - text
      properties:
        type:
          type: string
          const: thinking
        text:
          type: string
        state:
          $ref: '#/components/schemas/PartState'
      x-beeos-status: ga
    PartToolUse:
      type: object
      description: Tool-call invocation issued by the agent.
      required:
        - type
        - id
        - name
      properties:
        type:
          type: string
          const: tool_use
        id:
          type: string
          description: >-
            Unique tool-call id; matches the `tool_use_id` on the corresponding
            tool_result.
        name:
          type: string
        arguments:
          description: JSON arguments passed to the tool. Shape is tool-specific.
        state:
          $ref: '#/components/schemas/PartState'
      x-beeos-status: ga
    PartToolResult:
      type: object
      description: >-
        Tool-call output, paired with the originating tool_use via
        `tool_use_id`.
      required:
        - type
        - tool_use_id
        - content
      properties:
        type:
          type: string
          const: tool_result
        tool_use_id:
          type: string
        content:
          description: |
            JSON-encoded tool output. Plain text tool outputs SHOULD be
            wrapped as `{"text": "..."}` so the structural type stays
            uniform — but the schema accepts both for ergonomics.
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        is_error:
          type: boolean
          description: True iff the tool call errored (model should treat as such).
      x-beeos-status: ga
    PartFile:
      type: object
      description: |
        File attachment (image / audio / video / document). MIME type
        distinguishes modalities — the schema deliberately collapses
        image/audio/video into a single shape so adding new modalities
        is a renderer-side change rather than a schema migration.
      required:
        - type
        - url
        - mime_type
      properties:
        type:
          type: string
          const: file
        url:
          type: string
          format: uri
        mime_type:
          type: string
        name:
          type: string
        size:
          type: integer
          format: int64
      x-beeos-status: ga
    PartSource:
      type: object
      description: Citation / reference returned by retrieval-augmented agents.
      required:
        - type
        - url
      properties:
        type:
          type: string
          const: source
        url:
          type: string
          format: uri
        title:
          type: string
        snippet:
          type: string
      x-beeos-status: ga
    PartCustom:
      type: object
      description: |
        Extension escape hatch. New structured payloads (step markers,
        artifact refs, audio frames, code edits, …) can be carried
        under `custom.kind` without a schema migration. Old consumers
        SHOULD skip unrecognised kinds; new consumers route by
        `kind`.
      required:
        - type
        - kind
        - data
      properties:
        type:
          type: string
          const: custom
        kind:
          type: string
          description: Caller-controlled extension discriminant.
        data:
          description: Arbitrary JSON payload tied to `kind`.
      x-beeos-status: ga
    PartState:
      type: string
      description: |
        Per-part lifecycle. Only `thinking` and `tool_use` parts
        distinguish a streaming phase from a completed one; other
        part shapes ignore this field.
      enum:
        - streaming
        - done
      x-beeos-status: ga
  responses:
    BadRequest:
      description: >-
        Malformed request body, query parameter, or path parameter (`code` is
        one of `invalid_json` / `invalid_body` / `invalid_param` /
        `missing_param`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: >-
        Missing or invalid `Authorization` header (`code` is one of
        `unauthorized` / `invalid_token` / `missing_token`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Caller doesn't own the resource. `code` is `forbidden`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: >-
        The agent / task / instance / conversation / webhook referenced doesn't
        exist or isn't visible to the caller. `code` is typically
        `agent_not_found`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: >-
        Per-caller × endpoint quota exceeded. `code` is `rate_limited`. Honour
        `Retry-After` if present.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServerError:
      description: >
        Server-side fault. `code` is one of:


        - `internal_error` (500) — unexpected

        - `agent_offline` / `agent_service_unavailable` (503) — agent or
        messaging
          subsystem unreachable
        - `auth_unavailable` / `auth_transient` (503) — Auth gRPC transient

        - `service_timeout` (504) — gateway gave up waiting for the agent


        See
        [docs/reference/errors.md](https://github.com/beeos-ai/beeos/blob/main/docs/reference/errors.md).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Pass a user **JWT** or a **`oag_`** User API Key on the
        `Authorization: Bearer <token>` header. Both are validated by
        `openapi-gateway` against the Auth service.

        Both credential types are **user-scoped**: every key (and every
        JWT) is bound to exactly one owner, and every route grants the
        caller full access to that owner's own resources. Cross-tenant
        access is denied by owner-ACL inside the handlers — there is no
        per-route scope vocabulary on this API.

        > **Removed in v1.1.0:** the legacy `agents:*` / `tasks:*` /
        > `files:*` / `instances:*` scope set has been dropped together
        > with the `403 insufficient_scope` error. Existing `oag_` keys
        > automatically gain full owner-level access and do **not** need
        > to be re-issued. SDK calls that previously passed `scopes` to
        > `createAPIKey` should drop the argument. See the changelog at
        > the bottom of this spec for the full migration note.

````