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

# Read a session's transcript

> One page of the session's transcript, oldest first: the questions a person asked, the answers, and the tool calls and results between them. This is how a client that did not watch a session happen shows its conversation — a tab opening a session created hours ago, a switcher moving into another one. Replaying the event stream is not a substitute for it: entry-appended carries identity and nothing else, so a client reconstructing a conversation from events alone renders every answer with none of the questions.
Each page carries lastEventId, the event-stream position it was read at. A client pages to the end and then subscribes to GET /v1/events for this session with Last-Event-ID set to the LAST page's value: every entry committed at or before that read is already in hand, every event after it arrives on the stream, and nothing is delivered twice. Entries and that position are read from one database snapshot, so no entry can slip between the two.
A turn still in flight is deliberately not in it. The fragments of a message that has not committed as an entry are not transcript yet — a client attaching mid-turn receives the rest of that turn on the stream, and reads the entry the turn commits when it ends by asking for the entries after the last sequence it holds.
nextCursor is always present: a transcript is append-only, so the cursor is a resume position rather than a promise that more exists already. hasMore is what says whether more exists right now.
An assistant round's reasoning blocks are not served here, and neither are their signatures. They are the model's own scratchpad — rendering them in the assistant's voice claims the model said what it only considered — and each signature is opaque provider material that never leaves the host. What the entry DOES carry is a summary of the run (Entry.reasoning): what the model called it, how many blocks it took and how long it ran. Those are facts about the thinking rather than any of it, and they are what lets a client reading a session back draw the one collapsed line the live client draws, in the place the thinking happened. An entry that held nothing else is still listed, carrying no text, so a transcript's sequence numbering stays honest.



## OpenAPI

````yaml /en/openapi/v3-current/narya.yaml get /v1/sessions/{sessionId}/entries
openapi: 3.1.0
info:
  title: Narya Host API
  version: 1.0.0
  description: >-
    The contract between the Narya host and every client. One long-lived host
    runs on your machine and serves this API over a Unix socket in your Narya
    home. Narya creates the socket owner-only, and file permissions are the
    whole authorization. There is no password, no token and no TLS. The terminal
    client and the one-shot command that Lerian ships drive this API, and a
    client you write drives the same one.


    Results never arrive on the response of the request that caused them.
    Submitting a message returns 202 with a turn id. Everything the turn
    produces streams over GET /v1/events as server-sent events with a typed
    envelope, and a stream resumes from a Last-Event-ID header.


    One error envelope: code, title, message, and fields on 422. Codes are NRY-
    followed by four digits. A paged list answers items, limit and a nextCursor
    when more remains. Cursors are opaque.
servers: []
security: []
tags:
  - name: host
    description: The host process itself — version, uptime, mode, store.
  - name: sessions
    description: Durable conversation containers. Archive, never destroy.
  - name: messages
    description: Submitting work into a session and interrupting it.
  - name: events
    description: The server-sent event stream every client consumes.
  - name: lanes
    description: Parallel tracks inside a session — main, subagent, side.
  - name: agents
    description: Named recipes — instructions, tools, model, policy. Read-only in v1.
  - name: ladder
    description: >-
      What a person can type: the skills and command files in force for one
      repository, and expanding one into text. Five origins merged, nearest
      winning a name, the repository's own rungs gated on trust.
  - name: permissions
    description: Pending permission asks, decisions, and the decision audit.
  - name: intercom
    description: Sessions on one machine finding and messaging each other.
  - name: packages
    description: The one thing a user installs — resources, Go code, or both.
  - name: workflows
    description: Deterministic multi-agent orchestration runs.
  - name: providers
    description: Model suppliers, their auth state, and the model catalogue.
  - name: monitors
    description: >-
      Long-running watchers a session keeps beside its conversation — a test
      runner in watch mode, a build, a log being followed. Started by the model
      or by the person, always listed, always killable.
  - name: records
    description: The queryable local record of everything that happened.
  - name: schedules
    description: >-
      Work the host's own clock starts with nobody present — a repository, a
      prompt, a rule and what one fire may spend. Cancel, never destroy.
paths:
  /v1/sessions/{sessionId}/entries:
    parameters:
      - $ref: '#/components/parameters/SessionIdParam'
    get:
      tags:
        - sessions
      summary: Read a session's transcript
      description: >-
        One page of the session's transcript, oldest first: the questions a
        person asked, the answers, and the tool calls and results between them.
        This is how a client that did not watch a session happen shows its
        conversation — a tab opening a session created hours ago, a switcher
        moving into another one. Replaying the event stream is not a substitute
        for it: entry-appended carries identity and nothing else, so a client
        reconstructing a conversation from events alone renders every answer
        with none of the questions.

        Each page carries lastEventId, the event-stream position it was read at.
        A client pages to the end and then subscribes to GET /v1/events for this
        session with Last-Event-ID set to the LAST page's value: every entry
        committed at or before that read is already in hand, every event after
        it arrives on the stream, and nothing is delivered twice. Entries and
        that position are read from one database snapshot, so no entry can slip
        between the two.

        A turn still in flight is deliberately not in it. The fragments of a
        message that has not committed as an entry are not transcript yet — a
        client attaching mid-turn receives the rest of that turn on the stream,
        and reads the entry the turn commits when it ends by asking for the
        entries after the last sequence it holds.

        nextCursor is always present: a transcript is append-only, so the cursor
        is a resume position rather than a promise that more exists already.
        hasMore is what says whether more exists right now.

        An assistant round's reasoning blocks are not served here, and neither
        are their signatures. They are the model's own scratchpad — rendering
        them in the assistant's voice claims the model said what it only
        considered — and each signature is opaque provider material that never
        leaves the host. What the entry DOES carry is a summary of the run
        (Entry.reasoning): what the model called it, how many blocks it took and
        how long it ran. Those are facts about the thinking rather than any of
        it, and they are what lets a client reading a session back draw the one
        collapsed line the live client draws, in the place the thinking
        happened. An entry that held nothing else is still listed, carrying no
        text, so a transcript's sequence numbering stays honest.
      operationId: listSessionEntries
      parameters:
        - $ref: '#/components/parameters/CursorParam'
        - $ref: '#/components/parameters/LimitParam'
      responses:
        '200':
          description: One page of transcript entries, oldest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  parameters:
    SessionIdParam:
      name: sessionId
      in: path
      required: true
      description: The session's id.
      schema:
        type: string
        format: uuid
    CursorParam:
      name: cursor
      in: query
      required: false
      description: >-
        Opaque pagination cursor from a previous page's nextCursor or
        prevCursor.
      schema:
        type: string
        maxLength: 1024
    LimitParam:
      name: limit
      in: query
      required: false
      description: Maximum items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
  schemas:
    EntryPage:
      type: object
      description: >-
        One page of a session's transcript, oldest first, plus the seam a client
        attaches the live event stream at.
      required:
        - items
        - limit
        - hasMore
        - nextCursor
        - lastEventId
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Entry'
        limit:
          type: integer
          minimum: 1
        hasMore:
          type: boolean
          description: >-
            Whether more entries already exist after this page. False does not
            mean the transcript is finished — an append-only log always can grow
            — it means nothing more had been written when the page was read.
        nextCursor:
          type: string
          description: >-
            Where the next read resumes. Always present, including on the last
            page: a client that has caught up keeps it and re-reads from it
            later to collect whatever committed in between.
        lastEventId:
          type: string
          description: >-
            The event-stream position this page was read at — the id of the
            newest event this session had at the moment the entries were read,
            in the same database snapshot. Subscribe to GET /v1/events for this
            session with Last-Event-ID set to the last page's value and the
            stream continues exactly where the transcript stops. "0" when the
            session has no events at all.
      examples:
        - items:
            - id: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
              sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
              laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
              sequence: 1
              kind: user-message
              createdAt: '2026-08-08T12:01:03.000Z'
              text: where does the ledger validate a transaction's asset code?
          limit: 25
          hasMore: false
          nextCursor: c2VxOjE
          lastEventId: '1005'
    Entry:
      type: object
      description: >-
        One row of a session's append-only transcript. kind names what the row
        records, and the field carrying its content follows from kind: text for
        the message kinds, toolCall for a tool call, toolResult for that call's
        outcome. An entry whose content this surface does not serve — an
        assistant round's reasoning blocks — carries none of the three, and a
        client renders nothing for it rather than an empty message in the
        assistant's voice.
      required:
        - id
        - sessionId
        - laneId
        - sequence
        - kind
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        sessionId:
          type: string
          format: uuid
        laneId:
          type: string
          format: uuid
        sequence:
          type: integer
          format: int64
          minimum: 1
          description: >-
            The entry's position in this session's transcript, ascending and
            unique within the session. It is also the paging position: the
            cursor of a page resumes after the last sequence it returned.
        kind:
          type: string
          enum:
            - user-message
            - assistant-message
            - tool-call
            - tool-result
            - summary
            - system
            - intercom
            - monitor-event
        createdAt:
          type: string
          format: date-time
        text:
          type: string
          description: >-
            The entry's plain text, for the kinds that carry text: a person's
            message, the assistant's answer, a compaction summary, a system note
            nobody typed, an inter-session message. Absent on the tool kinds and
            on an entry whose content is not served.
        label:
          type: string
          maxLength: 128
          description: >-
            The tag somebody put on this moment of the conversation
            (setEntryLabel), absent when nobody named it. This is the read half
            of that write: without it a label can be set and then never found —
            no client can show the tag on the entry it belongs to, confirm a
            write landed, or offer to clear one with its current value.
            SessionTreeNode.labels says a whole session carries "the version
            that worked" and never which of its entries does.
        excludedReason:
          type: string
          description: >-
            Why this entry is no longer part of what the model is sent, absent
            while it still is. An entry with a reason has left the active window
            for good — the first producer is a message that overflowed the
            model's context window, and overflowed it again after the
            conversation behind it was summarised, so it provably cannot be
            sent. It is served because the entry is still HERE: an exclusion is
            not a deletion, the entry keeps its own text, and a reader who sees
            the conversation jump would otherwise have no way to tell what was
            set aside or why. A client drawing a transcript can mark the row
            rather than hide it.
        restorePoint:
          type: boolean
          description: >-
            Whether the files can go back to how they stood at this moment —
            true when a restore point reaches this entry or an earlier one.
            False is not a failure: restore points begin at the steps a turn
            commits, and a conversation that ran before narya was snapshotting
            it has moments the files cannot return to. It is served here because
            it is what a client offering to rewind has to say on the row BEFORE
            somebody picks it. Discovering it after the choice wastes the whole
            gesture, and rewinding the conversation at that same moment works
            either way.
        agent:
          type: string
          description: >-
            The named recipe the turn this message opened runs under, absent on
            a session answering as itself and on every entry a turn writes. It
            is served because a client drawing a conversation it never watched
            has no other source for it: the session's current agent is what the
            NEXT message will run under, so attributing a turn from three agents
            ago to it would name the wrong recipe.
        model:
          type: string
          description: >-
            The model that produced this assistant message, absent on the other
            kinds and on a round written before this was recorded. It closes a
            turn's attribution for a client reading the conversation back: the
            same fact a live watcher takes off the stream.
        provider:
          type: string
          description: Which supplier issued the model above, absent for the same reasons.
        stopReason:
          type: string
          description: >-
            Why this assistant round ended, when it ended somewhere other than
            its own end — max-tokens, content-filter, interrupted. Absent on a
            round that finished normally, which is what carries no admission on
            the line closing the turn.
        reasoning:
          $ref: '#/components/schemas/EntryReasoning'
        toolCall:
          $ref: '#/components/schemas/EntryToolCall'
        toolResult:
          $ref: '#/components/schemas/EntryToolResult'
        attachments:
          type: array
          description: >-
            The pictures this row carried, absent on every row that carried none
            — which is nearly every row of nearly every conversation, so an
            empty array would be a field spent on saying nothing.

            It exists because a client reading a conversation back has no other
            source for it. A picture somebody attached is not in the entry's
            text: the text is the words they typed, and until this the only
            trace of the file was an "@" marker inside those words, which cannot
            say what kind of thing was attached, whether the bytes are still
            there, or how tall it is. So a client drawing a transcript it never
            watched happen drew nothing where a screenshot was.

            The records carry metadata alone. Entry.attachments says a picture
            is there and what shape it is; GET
            /v1/sessions/{sessionId}/media/{mediaKey} is the one road to its
            bytes.
          items:
            $ref: '#/components/schemas/EntryAttachment'
      examples:
        - id: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          sequence: 1
          kind: user-message
          createdAt: '2026-08-08T12:01:03.000Z'
          text: where does the ledger validate a transaction's asset code?
          label: the version that worked
    Error:
      type: object
      description: >-
        The single error envelope every operation returns. Codes are NRY-
        followed by four digits and are catalogued in the top-level
        x-error-catalog extension. fields appears only on 422 validation errors,
        mapping each offending property to its problem.
      required:
        - code
        - title
        - message
      properties:
        code:
          type: string
          pattern: ^NRY-[0-9]{4}$
          description: Machine-readable error code from the NRY catalogue.
        title:
          type: string
          maxLength: 256
          description: Short human-readable summary of the error class.
        message:
          type: string
          maxLength: 4096
          description: Specific, actionable description of what went wrong.
        fields:
          type: object
          description: Per-field validation problems. Present on 422 only.
          additionalProperties:
            type: string
      examples:
        - code: NRY-0002
          title: Session not found
          message: >-
            No session with id 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d exists on
            this host.
    EntryReasoning:
      type: object
      description: >-
        What one round of the model's own thinking WAS, without any of what it
        said. The blocks are still not served and neither are their signatures;
        these are three derived facts about the run — what the model called it,
        how many blocks it took, and how long it ran — and none of them is text
        in the assistant's voice. It exists because the collapsed line naming a
        run is part of the conversation (R10) and a client reading a session
        back had no source for it: a replayed turn that thought for four minutes
        showed nothing where the thinking happened. A client expanding one of
        these says the scratchpad is not kept rather than drawing an empty
        surface.
      properties:
        title:
          type: string
          description: >-
            The heading the model wrote as the first act of the run, absent when
            it named the run nothing. Parsed from the first block rather than
            served from it — a heading is what the model called its own work,
            and the work itself stays here.
        steps:
          type: integer
          minimum: 1
          description: How many reasoning blocks the round produced.
        durationMs:
          type: integer
          minimum: 1
          description: >-
            How long the round ran, measured from the entry before it to this
            one. Absent when the entry before it is not in this page, which is
            the first row of a page and nothing else — a client drops the
            segment rather than printing a duration of zero.
      examples:
        - title: Inspecting the ordering
          steps: 3
          durationMs: 4200
    EntryToolCall:
      type: object
      description: >-
        A tool-call entry's content: the call exactly as the model asked for it,
        arguments untruncated. The stream's tool-call-started event carries a
        bounded preview of the same call; this is the durable record it
        announces.
      required:
        - id
        - name
      properties:
        id:
          type: string
          description: The tool call's id, matching the tool-result entry that answers it.
        name:
          type: string
          description: The tool's name.
        arguments:
          type: object
          additionalProperties: true
      examples:
        - id: call_01
          name: read
          arguments:
            path: components/ledger/validate.go
    EntryToolResult:
      type: object
      description: >-
        A tool-result entry's content: the text the model read next and whether
        the call failed. The structured machine channel and the display metadata
        are not served here — a transcript read reconstructs the conversation,
        not a tool's private plumbing.
      required:
        - toolCallId
        - output
      properties:
        toolCallId:
          type: string
        output:
          type: string
        isError:
          type: boolean
        denied:
          type: boolean
          description: >-
            The call was REFUSED rather than attempted: a permission rule or a
            person turned it down, so the tool never ran. Always accompanied by
            isError, because the refusal is what the model read as this call's
            answer — this says which kind of failure it was.

            It exists because the two are different facts to a person reading a
            session back, and only one of them is worth acting on: a tool that
            ran and failed is the model's problem, and a tool that was refused
            is a rule the reader may want to change. The live stream has always
            distinguished them (tool-call-finished's `denied` status); the
            durable entry could not, so a client reading a conversation it did
            not watch had to report both as failures. Absent means the call was
            attempted, which is every entry recorded before this field existed.
      examples:
        - toolCallId: call_01
          output: func Validate(tx Transaction) error {
          isError: false
    EntryAttachment:
      type: object
      description: >-
        One file a transcript row carried, as this host stored it — metadata
        only, never the bytes.

        The BYTES are fetched rather than pointed at, and that is the whole
        shape of this schema. A path in a JSON field describes a file on the
        machine the engine runs on, and the client may be nowhere near that
        machine — so a path would be a road that works on a laptop and silently
        breaks the day the host is elsewhere. mediaKey names the file to the
        operation that serves it, and nothing else may be inferred from it.

        The PIXEL SIZE travels here rather than being sniffed from the bytes
        because of the order a client has to decide things in: how many rows to
        reserve for a picture is settled before whether to spend the fetch, and
        sniffing the size means fetching. bytes answers the second half of that
        decision. All three are absent on a record stored before this host
        recorded them, and on a file that has no pixels at all — a converted
        document is an attachment too.
      required:
        - mediaKey
        - name
        - mediaType
      properties:
        mediaKey:
          type: string
          description: >-
            What this file is called inside its own session's media store, and
            the whole of what GET /v1/sessions/{sessionId}/media/{mediaKey}
            needs. Opaque: the host composes it, and a client that takes it
            apart is reading a private layout.
        name:
          type: string
          description: >-
            What the file was called where the person had it. It is what a chip
            in a composer or a caption under a picture says, and what every
            sentence about an attachment that cannot be shown has to name — a
            placeholder reading only "an image" leaves nobody able to say which
            of three.
        mediaType:
          type: string
          description: >-
            The mime this host sniffed from the file's own leading bytes, never
            from its name. It is also what the media operation answers with. An
            attachment that is not an image/* is a document narya converted for
            the model to read.
        width:
          type: integer
          minimum: 1
          description: The picture's width in pixels.
        height:
          type: integer
          minimum: 1
          description: The picture's height in pixels.
        bytes:
          type: integer
          minimum: 1
          description: >-
            How much the stored file weighs. A client decides whether a picture
            is worth a fetch on this and on the pixel size, before it asks for
            anything.
      examples:
        - mediaKey: screenshot.png-2749183021
          name: screenshot.png
          mediaType: image/png
          width: 1568
          height: 1045
          bytes: 284117
  responses:
    BadRequest:
      description: Malformed request — invalid parameter, cursor, or JSON body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: The addressed resource does not exist on this host.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalServerError:
      description: The host failed — including a store that refuses writes (NRY-0012).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

````