> ## 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.

# Create a session

> Creates a durable session bound to a repository. The session exists before any turn runs; submitting the first message is a separate operation.
A hosted home (one that names its own directories) runs every session inside a container. A session there that names no `environment` opens with none, and its first turn makes it one of its own: `conversation-<sessionId>`, from the home's sandbox image, owned by the caller; archiving the conversation or destroyEnvironment takes it away, its clone and branch included (a branch it pushed stays on GitHub). One that names a repository in `clone` gets that container when it is created rather than at its first turn, holding the repository's clone. A hosted home whose deployment names no sandbox image refuses that create 422 and nothing is created. On any home, `container` asks for that container at create, with nothing cloned: how a person's own home opens a conversation in one. A `worktree` for a session in a container is refused 422 on any home.



## OpenAPI

````yaml /pt/openapi/v3-current/narya.yaml post /v1/sessions
openapi: 3.1.0
info:
  title: Narya Host API
  version: 1.0.0
  description: >-
    The contract between the Narya host and every client. On your own machine,
    one long-lived host serves this API over a Unix socket in your Narya home.
    The terminal client and the one-shot command that Lerian ships drive this
    API, and a client you write drives the same one.


    Requests authenticate with a bearer token issued by the identity provider
    the host is configured with. On your own machine, the host also accepts a
    request with no token from your own operating system user. Once signed-in
    work has claimed the home for an organization, it does so only while this
    machine is signed in to that organization. Such a request gets 401 with
    NRY-0011 on work that reaches your organization beyond the machine. An
    operation that declares another security scheme also accepts that
    credential. Any other request without a valid credential gets 401 with
    NRY-0011. A caller whose role lacks the permission an operation needs gets
    403 with NRY-0028.


    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 and message. Codes are NRY- followed by four
    digits. Cursors are opaque.
servers: []
security:
  - bearerAuth: []
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: extensions
    description: >-
      Host-side extensions and the operations each exposes over the wire. This
      is the generic lane a host extension uses to serve its own client half (a
      TUI component, a web panel) or any API-only consumer, without adding
      routes to this contract.
  - 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: environments
    description: >-
      Where a session's code lives and its commands run — this machine, or a
      container narya operates. A session that names none runs here.
  - 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.
  - name: sharing
    description: >-
      Publishing a session from a developer's own home to the organisation's,
      and what is held back before a byte leaves the machine.
  - name: refinements
    description: >-
      What this owner has taught narya and allowed it to keep — distilled facts,
      and the skills, agents and commands the model wrote for itself. Propose,
      read, consent, roll back. Nothing here fires until a person answers.
  - name: platform
    description: >-
      Calls Lerian's control plane made to a home it hosts, as this home
      recorded them.
paths:
  /v1/sessions:
    post:
      tags:
        - sessions
      summary: Create a session
      description: >-
        Creates a durable session bound to a repository. The session exists
        before any turn runs; submitting the first message is a separate
        operation.

        A hosted home (one that names its own directories) runs every session
        inside a container. A session there that names no `environment` opens
        with none, and its first turn makes it one of its own:
        `conversation-<sessionId>`, from the home's sandbox image, owned by the
        caller; archiving the conversation or destroyEnvironment takes it away,
        its clone and branch included (a branch it pushed stays on GitHub). One
        that names a repository in `clone` gets that container when it is
        created rather than at its first turn, holding the repository's clone. A
        hosted home whose deployment names no sandbox image refuses that create
        422 and nothing is created. On any home, `container` asks for that
        container at create, with nothing cloned: how a person's own home opens
        a conversation in one. A `worktree` for a session in a container is
        refused 422 on any home.
      operationId: createSession
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionCreate'
      responses:
        '201':
          description: The created session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/RequestBodyTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  schemas:
    SessionCreate:
      type: object
      description: >-
        Request to create a session.

        There is no owner field and its absence is the rule rather than an
        omission — the host writes the owner from the identity it authenticated
        the request as.
      properties:
        repository:
          type: string
          maxLength: 4096
          x-go-type-skip-optional-pointer: true
          description: >-
            Absolute path of the repository to work in, required unless `clone`
            is given and refused (422) beside it. With `worktree` present this
            is the checkout the request is ABOUT — the one the new worktree is
            added to — and the session is rooted in that new worktree instead,
            whose path comes back on the response's `Session.repository`
            (relative to the workspace on a hosted home).

            WHICH DIRECTORIES A HOST WILL NAME IS THE HOST'S OWN SETTING and
            never a field on this request. A host running on a developer's own
            machine takes the absolute path as given. A host that names its own
            directories resolves every path in this field under its workspace
            root — so a relative path is resolved there rather than refused —
            and refuses with 422 one that escapes it, including a `worktree`
            whose derived checkout would land outside. The same body is legal
            against both; only the set of nameable directories differs.
        title:
          type: string
          maxLength: 512
          description: >-
            Optional title, supplied by the caller. A session created without
            one is named by the host after its first answered turn on the main
            lane: a 3–6 word title written by the session's own model, charged
            to the session like its turns. A `narya -p` run and a detached one
            are not named, and a title the caller gave or set with
            setSessionTitle is never replaced. Until one exists a client renders
            the session by whatever it knows instead — the repository's own
            name, or the session id.
        agent:
          type: string
          maxLength: 128
          description: >-
            The agent recipe every turn of this session runs under unless the
            message opening it names another; none when absent. Refused 422 when
            no recipe by that name may be a session's own agent. It is checked
            against the repository named above before the session or its
            worktree exists, so a recipe only its clone, container, environment
            or new worktree defines is named on a message instead. A branch
            keeps it.
        provider:
          type: string
          minLength: 1
          description: >-
            The provider `model` is resolved within. Only beside `model` (422
            otherwise); both absent is the configured default.
        model:
          type: string
          description: >-
            Model override; the configured default when absent. Named without
            `provider`, it is a reference (`provider/id#level`, an alias or a
            bare id) the host resolves as `narya -p --model` does; named beside
            `provider`, it is resolved within that provider only, as
            setSessionModel resolves it. Either way the host asks the provider
            for its own listing when the catalogue lacks the model, and one
            nothing lists is refused 422 naming it.
        clientKind:
          $ref: '#/components/schemas/ClientKind'
          description: >-
            The kind of client creating this session, stamped on it for the rest
            of its life. `api` when absent — a client that does not name itself
            is a caller of this contract, and that is what gets recorded. Naming
            `unknown` records nothing, which reads back as `unknown` exactly
            like a session that predates the datum.
        environment:
          $ref: '#/components/schemas/EnvironmentId'
          description: >-
            Run this session inside the named environment, which must already
            exist (createEnvironment). Absent means the host's own machine on a
            person's own home unless `container` asks for one, and a container
            of the session's own on a hosted home. The environment holds the
            code, at its workspace `/workspace`, and runs the commands;
            `repository` is recorded as given and does not choose where the code
            is.
        clone:
          type: string
          maxLength: 200
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$
          description: >-
            A repository this organisation selected for Narya's GitHub App, as
            `owner/name`, cloned into the conversation's own container on a
            hosted home before the session exists: the container
            `conversation-<sessionId>`, its own clone, and its own branch
            `narya/conversation-<sessionId>` cut from the default branch. Two
            conversations naming one repository share nothing.

            The clone runs exactly as createEnvironment's `repository` does, and
            is refused the same way, in the same sentence naming the connect
            gesture, with nothing created. After it, the clone's own
            `.narya/setup.sh` runs once inside the container, by `sh`, unless
            `setupScript` is given; a repository without that file runs no
            setup. A clone or a setup that fails opens no conversation: 422
            carrying what it said, and nothing is kept. The create waits for
            both.

            Only on a hosted home and only without `environment` (422
            otherwise): a named environment holds its own repository.

            The clone is the repository: the request names no `repository`, and
            the session's `repository` reads as `owner/name`. Its project
            configuration, rules and styles are read from the clone inside the
            container, never from the host's disk.
        container:
          type: boolean
          description: >-
            Open the conversation in a container of its own before the session
            exists: `conversation-<sessionId>`, made from this home's sandbox
            image with nothing cloned into it, its workspace the session's tree.
            On a person's own home this is how a client asks for one, so
            instructions nobody vouches for run where the agent cannot reach the
            home, its token or its socket; a hosted home makes one for every
            session that names no environment, and true there makes it now
            rather than at the first turn. A home whose host names no sandbox
            image (NARYA_SANDBOX_IMAGE) refuses it with 422 saying so, and so
            does a body that also names `environment` or `worktree`. Absent or
            false is the session's ordinary machine.
        setupScript:
          type: string
          minLength: 1
          maxLength: 65536
          description: >-
            Run once by `sh` in the conversation's container in place of the
            repository's own `.narya/setup.sh`. Only with `clone` (422
            otherwise).
        style:
          type: string
          maxLength: 64
          description: >-
            Open the session in this named response style, so its very first
            turn is already shaped by it rather than a round trip later. One of
            the names configuration declares — the person's own `[styles.*]`, or
            those of the repository named above once its directory is trusted;
            any other is refused by name with the file that would declare it,
            before the session exists. A session opening in an `environment` or
            a `clone` chooses from the person's own alone: its clone's styles
            are chosen with setSessionStyle once it exists. Absent is narya's
            own voice, which is every session until somebody chooses.
        worktree:
          $ref: '#/components/schemas/SessionWorktree'
          description: >-
            Add a git worktree to `repository` first and root the session in
            THAT checkout instead. Absent means the session opens in
            `repository` itself, which is what a client that never sends it
            keeps getting. A hosted home refuses it (422): the checkout would be
            cut on the host's disk, where the session never works.
        visibility:
          $ref: '#/components/schemas/Visibility'
          description: >-
            Open the session already marked, so a conversation that was never
            meant for the team is not readable by it for the seconds between
            being created and being marked. Absent means `organisation`, which
            is what every session is until somebody says otherwise, and there is
            no third state to ask for.
        product:
          type: string
          minLength: 1
          maxLength: 64
          description: >-
            Open a product session over the Lerian product a
            `[tool_trees.<name>]` table marked `product = true` names: its tools
            are the operations that product's people call, called with the
            bearer of the request that starts each turn. A write (any method but
            GET) is never sent when the model calls it: it is stored as a
            pending permission ask holding the exact call, the turn ends, and
            only its owner's decidePermission sends it. The session holds one
            such item, a write or a question, at a time; a message closes a
            pending write as declined. It works in no repository and no
            container, so `repository`, `clone`, `container`, `environment`,
            `worktree` or `setupScript` beside it, and a name no product tree
            declares, are refused 422 before anything else is checked. It opens
            private and stays its owner's alone: any other member, a holder of
            sessions:read-private included, gets 404 as for an unknown id, and
            `visibility: organisation` is refused 422. Publishing, marking,
            branching or rewinding it answers 422.
      examples:
        - repository: /Users/dev/repos/midaz
          title: Fix flaky ledger test
          clientKind: tui
    Session:
      type: object
      description: >-
        A durable conversation container. Survives the process that created it;
        archiving hides it from active lists but its transcript remains
        queryable and exportable forever.
      required:
        - id
        - repository
        - status
        - clientKind
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: The session's id.
        title:
          type: string
          maxLength: 512
          description: Human-readable title.
        repository:
          type: string
          maxLength: 4096
          description: >-
            Absolute path of the directory the session works in; on a hosted
            home, the path relative to its workspace, which is all a client
            there is shown.

            It is a working-directory LABEL and carries no authority. The
            session's key is `id`, and whose session it is is recorded by the
            host from the identity it authenticated the request as — nothing is
            authorised, looked up or scoped to a tenant by this field. What it
            is read for is what a directory is for: where the tools are
            confined, which project's configuration and instruction files apply,
            and the `repository` filter on listSessions.
        status:
          type: string
          enum:
            - idle
            - running
            - waiting
            - archived
          description: >-
            Lifecycle: running while a turn executes or normal follow-ups chain;
            waiting after interrupt when admitted steering or follow-up inputs
            remain queued; idle only when neither execution nor queued input
            exists; archived accepts no new work.
        provider:
          type: string
          description: The model provider currently in use.
        model:
          type: string
          description: The model currently in use.
        variant:
          type: string
          description: >-
            The preset of request options this session's turns run under — the
            name, not the options it stands for, since what a name means is the
            catalogue's answer and can change while the session's own choice
            cannot. Absent on a session that never chose one, which is every
            session running its model at the model's own defaults.
        style:
          type: string
          maxLength: 64
          description: >-
            The named response style this session's answers are shaped by — the
            name, not the prompt it appends, since what a name means is
            configuration's answer and changes when somebody edits that file
            while the session's own choice does not. Absent on a session that
            never chose one, which is every session answering in narya's own
            voice. Unlike `variant` it survives a model switch: a style is a
            fact about how the conversation reads rather than about the model
            answering it. A client renders this and derives nothing — the names
            that exist are the host's answer (setSessionStyle refuses the rest).
        clientKind:
          $ref: '#/components/schemas/ClientKind'
          description: >-
            Which kind of client opened this session, recorded when it was
            created and never rewritten. Always present: `unknown` for a session
            created before this host recorded the datum, which is the honest
            answer rather than a guessed frontend.
        modelNotice:
          type: string
          maxLength: 1024
          description: >-
            One sentence stating what a client could not otherwise know about
            the model this session opened on or was just moved to, present on
            the responses to createSession and setSessionModel, and on the
            session-created event of a session createSession opened, so a reader
            of the event stream (`narya -p --json`) has it too. Three facts earn
            it. The first is a model narya chose itself rather than being told
            to use: no default_model configured and no model previously chosen,
            so the first provider holding a usable credential answered — or a
            model chosen earlier that the catalogue no longer carries, which was
            passed over. It names the alternatives that also qualified and how
            to choose another, because a choice nobody made has to be stated to
            be a choice at all rather than something that merely happened. The
            second is a level this session will run without: a model reference
            may carry one (see the variant field), and an endpoint that does not
            take the field runs at its own default instead — so a level named at
            creation or at a switch, or configured beside default_model, is said
            here rather than quoted in a picker while every request drops it.
            The third is a default_model no provider lists: it is refused, never
            replaced, so the session opens on no model and this sentence names
            the one configured. A client that renders the live event stream
            rather than the session's transcript has no other road to it.
            Otherwise absent, including for a model that was simply configured,
            chosen or named; absent on getSession and listSessions, which report
            state rather than how it came about.
        usage:
          $ref: '#/components/schemas/TokenUsage'
          description: >-
            What this session has consumed and cost across every turn it has run
            — a running total, not the last turn's figure. Carried by getSession
            and by every row of listSessions, so a client drawing a list of
            sessions can say what each one has cost without asking per row.
            Absent on a session that has never completed a turn, and its costUsd
            is absent whenever any one of that session's turns could not be
            priced: the sum over only the priced turns is smaller than what the
            session actually cost, and a figure that quietly understates money
            is worse than an admitted unknown.
        lastTurnUsage:
          $ref: '#/components/schemas/TokenUsage'
          description: >-
            What the newest round of this session's own conversation sent and
            produced — the LAST turn's figure, not the running total above.
            Carried by getSession only.

            It exists because the two are not interchangeable for the one
            question a reader reopening a long conversation asks: how much room
            is left before this compacts. That answer is the size of the prompt
            last sent, and usage above is a sum over every turn the session ever
            ran, which is a larger number growing without bound and means
            nothing as a fraction of a context window. The live event stream
            carries this figure on turn-finished, so a client watching a session
            has always had it; a client that READ the session had no source for
            it at all and drew no context figure until it spent a turn of its
            own. Restoring a recorded reading rather than deriving one is the
            whole of it — nothing here is computed from the transcript.

            The newest round of the MAIN lane: a delegation's rounds are another
            conversation's prompts, and the figure is about the reader's. Absent
            on a session that has never completed a round, and on one whose
            rounds all ran on delegates. Its costUsd is that one round's price,
            absent when the round could not be priced.
        pendingInputs:
          type: array
          description: >-
            Pending steering and follow-up references in durable operation
            order. Content is intentionally absent; fetch the transcript by
            entryId.
          items:
            $ref: '#/components/schemas/PendingInputReference'
        parentSessionId:
          type: string
          format: uuid
          description: When the session was branched, the session it branched from.
        summary:
          type: string
          maxLength: 4096
          description: >-
            What a model said this conversation tried and concluded. Absent
            until somebody asks for it (summarizeSession) — nothing summarises a
            branch because a rewind moved away from it. A model that answers
            past this length is trimmed to it, with a marker saying so rather
            than a sentence that just stops.
        summaryAt:
          type: string
          format: date-time
          description: >-
            When the summary was taken. Read against updatedAt it answers
            whether the summary predates the last thing that happened here.
        posture:
          $ref: '#/components/schemas/Posture'
          description: >-
            Whether somebody is there to answer a permission prompt raised here.
            Absent on a session nobody ever said either way about, which reads
            by its clientKind (Posture) — the posture is only ever recorded
            because somebody stated it (setSessionPosture), never inferred from
            whether a client happens to be connected.
        warming:
          $ref: '#/components/schemas/Warming'
          description: >-
            Whether this session pays to keep its provider cache alive while
            nobody is using it, and the most it may ever spend doing so. Absent
            on a session nobody ever turned it on for, which is every session
            until somebody does — warming is off unless somebody asked
            (setSessionWarming).
        viewedAt:
          type: string
          format: date-time
          description: >-
            When a reader last had this session in front of them
            (acknowledgeSessionViewed). Read against updatedAt it answers
            whether anything has happened here since somebody looked, which is
            what lets a client mark a background conversation as carrying news —
            and, because the fact is held here rather than in a window, what
            makes two clients agree about it and makes the answer survive a
            relaunch. ABSENT on a session nobody has ever viewed, which is not
            the same as viewed long ago: one has nothing to report, the other
            has everything that ever happened in it.
        lastOutcome:
          type: string
          enum:
            - finished
            - failed
          description: >-
            How the last turn here ended. It is what lets a client that was not
            running at the time tell a conversation that finished from one that
            failed. Absent on a session that has never run a turn. An interrupt
            is not among the values: somebody was there and stopped it
            themselves, so it is not news to bring back to them.
        cacheSpend:
          $ref: '#/components/schemas/CacheSpend'
          description: >-
            What this session's provider prompt cache has cost it, from both
            sides: what letting it expire cost, and what keeping it alive cost.
            Present only on getSession, and only once one of the two has
            happened — a session that has never paid for either carries nothing
            here rather than a pair of zeros.
        environment:
          $ref: '#/components/schemas/SessionEnvironment'
          description: >-
            Where this session's code lives and its commands run, present only
            when that is somewhere other than the host's own machine. Absent is
            the ordinary case and says the session runs here — a developer
            working on their own machine is told nothing, because there is
            nothing to tell. On a hosted home it says instead that the session's
            own container is not made yet: its first turn makes it.
        clone:
          type: string
          description: >-
            The repository cloned into the tree this session works in, as
            `owner/name`: the `clone` it was opened with, or the `repository` of
            the environment it runs on. Absent when nothing was cloned.
        product:
          type: string
          description: >-
            The product tree a product session reads through (createSession's
            `product`); absent on every other session.
        tasks:
          type: array
          description: >-
            The agent's own task list for this session — every item it has
            written, in the order it wrote them, whatever is in the store right
            now.

            It is here rather than behind a listing of its own because the list
            is a stored row and every client already reads the session when it
            attaches, so one optional field costs no round trip and no second
            operation. A change to the list is announced as `task-list`, which
            carries the same whole list for the same reason; this is where a
            client that was not attached at the time gets it.

            Present on getSession only, like `lastTurnUsage` and `cacheSpend`
            and for their reason: a listing that carried it would read every
            row's tasks to draw a page nobody asked for.

            ABSENT MEANS EMPTY, and it cannot mean anything narrower: the store
            holds rows, so a list nobody ever wrote and a list somebody cleared
            are the same read. The live stream is where those two differ — a
            clearing write announces `task-list` carrying an empty array — and a
            read that returned `[]` would be claiming to know which of the two
            it was looking at.
          items:
            $ref: '#/components/schemas/Task'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          description: >-
            When this session's own ROW last changed. Eight writes stamp it and
            nothing else does: its status, its provider and model, its style,
            its title, its repository, its environment binding, its archival,
            and the status a restart settles it into.

            IT IS NOT A LAST-ACTIVITY CLOCK, and reading it as one is the
            mistake this description exists to prevent. A transcript entry, a
            streamed event, a tool call, a summary and a viewed mark all leave
            it exactly where it was — so a session that is `running` carries THE
            MOMENT ITS TURN WAS CLAIMED, not the moment anything last happened
            in it — and a moment later than that only when one of the other
            seven writes landed during the turn: a rename mid-turn is allowed
            and restamps the row, moving this field forward with nothing having
            happened in the conversation. A caller asking whether a run has gone
            quiet reads the event stream; read off this field, "nothing for N
            minutes" is true of every healthy turn that has been running longer
            than N, and false of a stuck one somebody renamed.

            `SessionAttention.since` carries the same warning for a waiting
            session, and for the same reason.
        owner:
          type: string
          readOnly: true
          description: >-
            Who this session belongs to: the identifier of the person the
            request that created it was authenticated as, inside the
            organisation this host serves. It is who the session is attributed
            to and whose allowance its cache warming is billed against.

            The host writes it from the authenticated identity and there is no
            owner field on any create request — its absence there is the rule
            rather than an omission, because an owner a caller can name is an
            owner anybody can be, and this value is read later by whoever
            decides who pays and who may open a private conversation. A request
            body naming an owner changes nothing.

            `unclaimed` is the one value that is not a person: a store that
            existed before this host recorded owners, on a machine nobody has
            signed in on yet. The first sign-in against such a store replaces it
            everywhere.
        visibility:
          $ref: '#/components/schemas/Visibility'
          description: >-
            Who inside this organisation may read this session. Always present
            on a host that serves this field, unlike `posture` beside it: every
            session HAS a visibility, and the absence of a mark is the
            organisation's rather than a third state, so there is nothing for an
            absent field to mean. A client draws the mark from this value and
            derives nothing — whether a session is held back, and whether this
            reader may see it at all, are the host's answers.
        attention:
          $ref: '#/components/schemas/SessionAttention'
          description: >-
            What this conversation is waiting for, when it is waiting for a
            person. `status` says `waiting` and does not say what for.


            Carried by every row of listSessions and resolved for the whole page
            in one read, which is the point of it: a page of a hundred rows is
            enough to draw an overview of sessions, with no request per row.
            getSession carries the same field for its one session.


            ABSENT WHEN THERE IS NOTHING TO SAY, and absent rather than
            null-shaped: a conversation nobody is being asked about carries no
            field, not an empty object. It is also absent when the caller may
            not see the prompt itself — on a conversation somebody marked
            private, a reader who is not its owner and does not hold the
            read-private permission sees no attention even though the request
            behind it exists, because what this field may say is bounded by the
            same door the ask is behind.
      examples:
        - id: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          title: Fix flaky ledger test
          repository: /Users/dev/repos/midaz
          status: running
          provider: anthropic
          model: claude-sonnet-4-5
          clientKind: inline
          owner: user_01HZX3Q8N4KDPYV2A7C9M5T6RB
          usage:
            inputTokens: 152000
            outputTokens: 8400
            costUsd: 1.87
          createdAt: '2026-08-08T12:00:00.000Z'
          updatedAt: '2026-08-08T12:34:56.000Z'
    ClientKind:
      type: string
      enum:
        - inline
        - tui
        - oneshot
        - api
        - unknown
      x-enum-varnames:
        - ClientKindInline
        - ClientKindTui
        - ClientKindOneshot
        - ClientKindApi
        - ClientKindUnknown
      description: >-
        Which kind of client opened a session — the coarse classification a
        client filters and mixes on when it lists sessions it did not itself
        open. inline — the terminal client (`narya`). tui — the full-screen
        client (`narya --tui`). oneshot — a one-shot command (`narya -p`). api —
        a caller of this contract that named no kind; every narya frontend names
        itself, so this is what speaking the contract directly is. unknown —
        never recorded, which is every session created before this host carried
        the datum; a read answers `unknown` rather than picking a kind, and a
        create request naming it records nothing. It also answers who attends a
        session that recorded no posture: inline and tui have a person behind
        them, so a permission prompt is asked; oneshot, api and unknown have
        nobody, so a prompt is refused with its reason. In a repository nobody
        has trusted, an api or unknown session's turn is refused naming the
        trust gesture; a oneshot session runs there under the operator's rules
        without the repository's own files, whoever created it.
    EnvironmentId:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9][A-Za-z0-9._-]*$
      description: >-
        An environment's identifier, chosen by whoever creates it and unique on
        this host. It travels as one path segment, so its shape is bounded to
        what a path segment carries unchanged, exactly as a monitor's name is: a
        "/" would address no route and a "?" or "#" would end the segment early
        and address something else. One beginning `conversation-` is a hosted
        home's own, and a create naming it is 422.
    SessionWorktree:
      type: object
      description: >-
        Where a session is BORN when it is not born in a directory that already
        exists: narya adds a git worktree to the repository the request names,
        on a NEW branch cut from `base`, and the session opens in the checkout
        that created.

        WHERE THE NEW CHECKOUT GOES IS DERIVED AND IS NOT AN INPUT. It is a
        sibling of the repository named, suffixed with the branch, with every
        path separator in the branch flattened to a dash — so branch `feat/x` on
        `/srv/code/api` becomes `/srv/code/api-feat-x`, and a slashed branch can
        never produce a nested tree. The absolute path git actually wrote comes
        back on `Session.repository`, and that is the only answer to "which
        directory is this session working in": nobody supplied it, so nothing
        else can be asked for it.

        Everything git refuses is refused in git's own words, with 422: a branch
        that already exists, a destination directory that exists and is not
        empty, a base nothing resolves, and a repository that is not a checkout.
        Trust is asked about the new directory exactly as about any other, and
        an untrusted one opens normally and runs untrusted — the clause
        createSession already states.

        A birth refused AFTER git wrote the checkout — a worktree whose own
        `.narya/config.toml` will not load — leaves that checkout on disk. The
        branch and the files are real; once the configuration loads, a session
        opens on the directory the ordinary way.
      required:
        - branch
      properties:
        branch:
          type: string
          description: >-
            The branch to create. It must NOT already exist: this creates a
            branch rather than checking one out, so a session born here has
            somewhere of its own to commit. Its shape is git's business and any
            name git rejects is refused carrying git's own sentence — with one
            exception, which the host refuses itself, in its OWN words, before
            git runs at all: a name that is empty or begins with a dash. `git
            worktree add -b` hands the name to an inner `git branch` with no
            separator in front of it, so a dashed name is read there as an
            option and acts on the BASE ref instead of creating anything; an
            empty one derives a checkout directory named after nothing. Both are
            refused with 422, before git's parser can reach them.
        base:
          type: string
          description: >-
            The ref the new branch starts at. `HEAD` when absent, which is what
            a caller who just wants another checkout of this work needs.
      examples:
        - branch: feat/settlement
          base: main
    Visibility:
      type: string
      enum:
        - organisation
        - private
      x-enum-varnames:
        - VisibilityOrganisation
        - VisibilityPrivate
      description: >-
        Who inside this organisation may read a session. organisation — every
        member may, which is the default and what every session is until
        somebody marks it. private — its owner, plus a member holding
        `sessions:read-private`, plus whoever a share of it names.

        Two values and no third. There is no "unset": the absence of a mark IS
        the organisation's, so "nobody said" and "the team may read it" are one
        fact rather than two, and nothing in this product has a use for a
        visibility somebody would have to define.
    TokenUsage:
      type: object
      description: >-
        Token consumption totals and their cost. costUsd is priced from the
        catalogue's published rates for the model that ran, and is absent
        whenever no rate is known for it — a model configured by hand against a
        private endpoint, say. Absent means unknown; a false zero would mean
        free. On a session total it is absent unless every turn in the session
        could be priced.
      required:
        - inputTokens
        - outputTokens
      properties:
        inputTokens:
          type: integer
          minimum: 0
          description: Input tokens consumed.
        outputTokens:
          type: integer
          minimum: 0
          description: Output tokens produced.
        cacheReadTokens:
          type: integer
          minimum: 0
          description: Tokens served from the provider's prompt cache.
        cacheWriteTokens:
          type: integer
          minimum: 0
          description: Tokens written to the provider's prompt cache.
        costUsd:
          type: number
          minimum: 0
          description: Total cost in US dollars.
      examples:
        - inputTokens: 152000
          outputTokens: 8400
          cacheReadTokens: 120000
          cacheWriteTokens: 9000
          costUsd: 1.87
    PendingInputReference:
      type: object
      description: >-
        A content-free durable queued-input reference. Message content remains
        only in the transcript.
      required:
        - entryId
        - turnId
        - kind
        - sequence
        - enqueuedAt
      properties:
        entryId:
          type: string
          format: uuid
        turnId:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - steer
            - follow-up
        sequence:
          type: integer
          format: int64
          minimum: 1
        enqueuedAt:
          type: string
          format: date-time
      examples:
        - entryId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          kind: follow-up
          sequence: 7
          enqueuedAt: '2026-08-10T15:04:05.000Z'
    Posture:
      type: string
      enum:
        - attended
        - detached
      x-enum-varnames:
        - PostureAttended
        - PostureDetached
      description: >-
        Whether somebody is there to answer a permission prompt raised in a
        session. attended — a prompt is shown and waited on. detached — nobody
        is watching, so a prompt is refused and recorded with its reason rather
        than blocking the turn forever on a question nothing can answer. A
        session that recorded neither is attended when a terminal client
        (clientKind inline or tui) opened it, or it is a product session, and
        detached otherwise.

        Orthogonal to clientKind, and deliberately not a value of it: clientKind
        is stamped once at creation and never rewritten, whereas a session
        BECOMES detached when somebody says so. It is a recorded fact with
        exactly one transition (setSessionPosture) and is never inferred from
        the presence or absence of a connection — a client that crashed, a lid
        that closed and a pipe that dropped all leave it exactly as it was.
    Warming:
      type: object
      description: >-
        One session's standing instruction about its provider cache while nobody
        is using it: whether to pay to hold the cached prefix past the five
        minutes a provider keeps it for free, and the lifetime dollars this
        session may spend doing so.

        Both fields travel together because they are one decision. Turning
        warming on without saying what it may cost is not a decision anybody
        makes, and there is no default narya could supply that is not a guess
        about somebody else's bill.

        The ceiling is a LIFETIME figure for this session, not a daily one: a
        session is a bounded thing with an end, and "this conversation may cost
        me at most fifty cents to keep warm" is the sentence a person means. A
        second, separate ceiling bounds the whole store's spending per UTC day
        and is operator configuration rather than part of any session — either
        one being reached stops warming rather than warning about it.
      required:
        - enabled
        - ceilingUsd
      properties:
        enabled:
          type: boolean
          description: >-
            Whether this session is warmed. False with a ceiling recorded is a
            session somebody turned off, which keeps the number they chose for
            the day they turn it back on.
        ceilingUsd:
          type: number
          format: double
          minimum: 0
          description: >-
            The most this session may ever spend on warming, in US dollars,
            counted across its whole life. Reaching it stops warming this
            session and nothing else; every other session keeps its own
            allowance. Zero is a legitimate value and means the same as off.
      examples:
        - enabled: true
          ceilingUsd: 0.5
    CacheSpend:
      type: object
      description: >-
        What this session's provider prompt cache has cost it, read from both
        sides of one decision: what letting the cached prefix expire cost, and
        what refusing to let it expire cost.

        The two numbers are reported together because they are the same decision
        seen from either end. Somebody weighing whether to turn warming on for
        this session is comparing exactly these figures, and either one alone
        answers half the question.

        The headline is the re-billed prompt: rounds that paid full input rates
        to send again a prefix the provider had been holding, because the
        session sat idle past the cache's lifetime. A duplicated cache WRITE is
        a real event too and is deliberately not reported here — it is an
        internal metric, not what a person reads on a bill.

        Absent on a session where neither has ever happened, which is every
        session until one of them does. Never a zero-filled object standing in
        for "nothing yet".
      required:
        - rebilledRounds
        - rebilledTokens
        - rebilledUsd
        - warmingUsd
      properties:
        rebilledRounds:
          type: integer
          minimum: 0
          description: >-
            How many rounds paid to re-send an expired prefix. Zero here with a
            warming figure present is a session that has been kept warm and has
            therefore never paid for its own idleness — which is warming
            working.
        rebilledTokens:
          type: integer
          minimum: 0
          description: >-
            The prefix re-sent across those rounds: the part of each round's
            cache write that had genuinely been cached before, never the whole
            write. A resumed conversation also writes the message that ended the
            wait, and that content would have been written with the prefix alive
            too — charging it to the expiry would blame idleness for the weight
            of the message that ended it.
        rebilledUsd:
          type: number
          format: double
          minimum: 0
          description: >-
            What that expiry cost, in US dollars: for each round, the difference
            between what its prompt was billed and what the same prompt would
            have cost with the prefix still cached.

            It DECOMPOSES money already reported in `usage.costUsd` rather than
            adding to it. A re-billed round's whole price is in the session
            total like every other round's, and this says how much of that price
            the expiry was; adding the two together counts the same dollars
            twice.
        rebilledUsdComplete:
          type: boolean
          description: >-
            Absent means true. Present and false makes `rebilledUsd` a FLOOR —
            "at least this much" — because at least one re-billed round ran on a
            model this host cannot price, contributing a real expiry and no
            dollars. The same honesty `costComplete` carries on a usage report,
            for the same reason: a total that quietly skipped the rounds it
            could not price is a number a reader will believe.
        warmingUsd:
          type: number
          format: double
          minimum: 0
          description: >-
            What this session has spent keeping its cache alive, in US dollars,
            counted across its whole life and against the ceiling in
            `warming.ceilingUsd`. Reservations are included: a warm that was
            paid for and whose outcome was never established is money that may
            already be gone, and leaving it out would let it be spent twice.
      examples:
        - rebilledRounds: 4
          rebilledTokens: 18240
          rebilledUsd: 0.0312
          warmingUsd: 0.045
    SessionEnvironment:
      type: object
      description: >-
        A session's binding to an environment, as a client draws it beside the
        session's identity. The id and the state, and deliberately nothing else:
        everything a person acts on is here, which keeps a page of twenty-five
        sessions from carrying twenty-five setup transcripts.
      required:
        - id
        - state
      properties:
        id:
          $ref: '#/components/schemas/EnvironmentId'
        state:
          $ref: '#/components/schemas/EnvironmentState'
    Task:
      type: object
      description: >-
        One item of the agent's task list: what the work is, and where it
        stands.

        Two fields and no more. There is no id, because the list is replaced
        whole on every write and position is the only order there is — an
        identifier would be a second thing two surfaces could disagree about.
        There is no priority, because nothing renders or orders by one.
      required:
        - content
        - status
      properties:
        content:
          type: string
          description: What the task is, in the agent's own words.
        status:
          type: string
          enum:
            - pending
            - in_progress
            - completed
            - cancelled
          description: >-
            Where the item stands. `cancelled` counts toward the total and not
            toward what is done — an abandoned step is still a step the list
            once held. The engine does not enforce "exactly one in_progress":
            that is guidance in the tool's own description, and a list holding
            two is drawn honestly rather than refused.
          x-enum-varnames:
            - TaskStatusPending
            - TaskStatusInProgress
            - TaskStatusCompleted
            - TaskStatusCancelled
      examples:
        - content: Reproduce the flaky ledger test
          status: completed
    SessionAttention:
      type: object
      description: One prompt standing on a conversation, waiting for a person.
      required:
        - kind
        - askId
        - since
      properties:
        kind:
          type: string
          enum:
            - permission
            - question
          description: >-
            Which of the two things a person answers is holding this
            conversation: a permission request for an action the agent wants to
            take, or a question the agent put to the user.


            A refinement round is deliberately not one of the values. It blocks
            no tool call, so a conversation standing on one is not waiting for
            anybody and must not be drawn as though a turn were parked.
        askId:
          type: string
          format: uuid
          description: >-
            The prompt itself — the permission id or the question id, according
            to `kind` — so a client acts on it directly instead of paging a
            listing to find which of several it just drew.
        since:
          type: string
          format: date-time
          description: >-
            When the PROMPT was raised, which is not when the session last
            changed. "Waiting forty minutes" has to mean the person has been
            kept waiting forty minutes; read off `updatedAt` it would mean only
            that nothing has happened here for forty minutes, which is true of
            an idle conversation nobody is blocked on.


            Where several prompts stand on one conversation this is the OLDEST,
            for the same reason: the question an overview of sessions asks is
            how long somebody has been waiting, not when the most recent thing
            appeared.
    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.
      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.
          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.
    EnvironmentState:
      type: string
      enum:
        - ready
        - degraded
        - gone
        - unusable
      x-enum-varnames:
        - EnvironmentStateReady
        - EnvironmentStateDegraded
        - EnvironmentStateGone
        - EnvironmentStateUnusable
      description: >-
        Whether an environment is answering, and — when it is not — whether
        waiting is the right move. The distinction is the whole point: a session
        waiting on an unreachable environment looks identical to a slow one
        otherwise, and what the person does next depends entirely on which it
        is. ready — answering; work proceeds. degraded — not answering right now
        and expected back: a restart or a dropped connection, which the host
        retries so the next call may simply succeed, or a container stopped
        outside narya, which the next turn starts. gone — destroyed, its name
        taken by another environment since, or the runtime no longer has it. A
        hosted home also removes a container nothing has used for 15 minutes and
        keeps its disks, the workspace and its restore points, and so does a
        person's own home for a conversation's own `conversation-<sessionId>`;
        it reads gone until a turn needs it. A session bound to it is then given
        a new container on its next turn, over the disks the old one left when
        those are still there, and is told which; on a person's own home a named
        environment that is gone needs a new one. unusable — its setup did not
        succeed; `setup` says what happened.
  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'
    RequestBodyTooLarge:
      description: >-
        The request body weighs more than this operation accepts (NRY-0024). The
        host holds one ceiling per operation and refuses at the door, in front
        of every handler: a body whose declared Content-Length is past the
        ceiling is refused before a byte of it is read, and a body that declares
        no length is read only as far as the ceiling and refused there. Nothing
        was read past that point and nothing was written.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: The request was well-formed but failed validation.
      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'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Enforced on every transport, with no exempt operation. A person's
        request must carry a JWT issued by the configured identity provider,
        which the host verifies itself against that issuer's key set: signature,
        issuer, expiry, and the person and organisation it names. Requests
        without a valid one receive 401 NRY-0011 — except a request with no
        token on a laptop's socket from the OS user the kernel names as the
        host's own, which is that machine's user.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.