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

# Stream events (SSE)

> The global event firehose as server-sent events (text/event-stream). Every client consumes this one stream; events are encoded once per update and fanned out per connection, and a slow client is dropped rather than blocking the others. Filter to one session with the sessionId query parameter. Each SSE frame's id field carries the event id; on reconnect, send it in the Last-Event-ID request header to resume from the first event after it — delivery survives client restarts. Event ids are monotonic integers and the replay buffer is time-boxed: when Last-Event-ID is older than the oldest retained event, the server starts the stream live rather than erroring; the client detects the gap between its last id and the first received id and must re-sync state through the GET endpoints before trusting the stream again. The data field of each frame is one JSON Event envelope, discriminated by its type property.
A REPLAY MAY BE SERVED IN BLOCKS. When the backlog past Last-Event-ID is long, the server writes a bounded block of frames and then ends the connection rather than materialising the whole backlog onto it. The block-end is an ordinary end of connection carrying no marker: a client resumes as it would from any drop, sending the last id it received as Last-Event-ID, and continues from exactly there. Nothing is lost and nothing is duplicated — a client that already skips ids at or below the last one it delivered needs no change at all, and a replay short enough to fit in one block is served exactly as before.
A CURSOR THIS STORE CANNOT HONOUR IS REFUSED, not served. A Last-Event-ID naming an event newer than anything the store holds gets 409 (NRY-0026) instead of an open stream: the replay past it is empty by definition and every live event is below it, so a 200 there is a connection that can never carry a frame — silent for the life of the process, with nothing timing out to say so. It is what a client holds after the store underneath it was replaced. A cursor that is merely OLD is unaffected and still starts the stream live. The remedy is to resume with no Last-Event-ID, never to retry the same cursor.



## OpenAPI

````yaml /en/openapi/v3-current/narya.yaml get /v1/events
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/events:
    get:
      tags:
        - events
      summary: Stream events (SSE)
      description: >-
        The global event firehose as server-sent events (text/event-stream).
        Every client consumes this one stream; events are encoded once per
        update and fanned out per connection, and a slow client is dropped
        rather than blocking the others. Filter to one session with the
        sessionId query parameter. Each SSE frame's id field carries the event
        id; on reconnect, send it in the Last-Event-ID request header to resume
        from the first event after it — delivery survives client restarts. Event
        ids are monotonic integers and the replay buffer is time-boxed: when
        Last-Event-ID is older than the oldest retained event, the server starts
        the stream live rather than erroring; the client detects the gap between
        its last id and the first received id and must re-sync state through the
        GET endpoints before trusting the stream again. The data field of each
        frame is one JSON Event envelope, discriminated by its type property.

        A REPLAY MAY BE SERVED IN BLOCKS. When the backlog past Last-Event-ID is
        long, the server writes a bounded block of frames and then ends the
        connection rather than materialising the whole backlog onto it. The
        block-end is an ordinary end of connection carrying no marker: a client
        resumes as it would from any drop, sending the last id it received as
        Last-Event-ID, and continues from exactly there. Nothing is lost and
        nothing is duplicated — a client that already skips ids at or below the
        last one it delivered needs no change at all, and a replay short enough
        to fit in one block is served exactly as before.

        A CURSOR THIS STORE CANNOT HONOUR IS REFUSED, not served. A
        Last-Event-ID naming an event newer than anything the store holds gets
        409 (NRY-0026) instead of an open stream: the replay past it is empty by
        definition and every live event is below it, so a 200 there is a
        connection that can never carry a frame — silent for the life of the
        process, with nothing timing out to say so. It is what a client holds
        after the store underneath it was replaced. A cursor that is merely OLD
        is unaffected and still starts the stream live. The remedy is to resume
        with no Last-Event-ID, never to retry the same cursor.
      operationId: streamEvents
      parameters:
        - name: sessionId
          in: query
          required: false
          description: Only stream events for this session.
          schema:
            type: string
            format: uuid
        - name: hostEvents
          in: query
          required: false
          description: >-
            When sessionId narrows the stream, also deliver the events that
            belong to no session at all — things true of the whole machine, like
            an installed package's files changing under everybody
            (ladder-changed). Off by default, so an existing consumer's stream
            is unchanged. Ignored without sessionId, where the unfiltered
            firehose already carries them.
          schema:
            type: boolean
            default: false
        - name: Last-Event-ID
          in: header
          required: false
          description: >-
            Resume the stream from the first event after this id, as defined by
            the SSE specification.
          schema:
            type: string
      responses:
        '200':
          description: >-
            An open server-sent event stream. Each frame's data field is one
            Event envelope.
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/Event'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  schemas:
    Event:
      description: >-
        The typed event envelope carried in each SSE frame's data field,
        discriminated by type.
      oneOf:
        - $ref: '#/components/schemas/SessionCreatedEvent'
        - $ref: '#/components/schemas/SessionStatusChangedEvent'
        - $ref: '#/components/schemas/SessionPostureChangedEvent'
        - $ref: '#/components/schemas/ModelSwitchEvent'
        - $ref: '#/components/schemas/TurnStartedEvent'
        - $ref: '#/components/schemas/MessageDeltaEvent'
        - $ref: '#/components/schemas/MessageStartEvent'
        - $ref: '#/components/schemas/MessageEndEvent'
        - $ref: '#/components/schemas/CompactionEvent'
        - $ref: '#/components/schemas/ContextPrunedEvent'
        - $ref: '#/components/schemas/EntryAppendedEvent'
        - $ref: '#/components/schemas/ToolCallStartedEvent'
        - $ref: '#/components/schemas/ToolCallUpdateEvent'
        - $ref: '#/components/schemas/ToolCallFinishedEvent'
        - $ref: '#/components/schemas/ToolCallReceiptEvent'
        - $ref: '#/components/schemas/PermissionAskedEvent'
        - $ref: '#/components/schemas/PermissionDecidedEvent'
        - $ref: '#/components/schemas/LaneStartedEvent'
        - $ref: '#/components/schemas/LaneFinishedEvent'
        - $ref: '#/components/schemas/TurnFinishedEvent'
        - $ref: '#/components/schemas/IntercomMessageEvent'
        - $ref: '#/components/schemas/WorkflowProgressEvent'
        - $ref: '#/components/schemas/LadderChangedEvent'
        - $ref: '#/components/schemas/CheckpointCreatedEvent'
        - $ref: '#/components/schemas/ExtensionEvent'
        - $ref: '#/components/schemas/MonitorEventEvent'
        - $ref: '#/components/schemas/MonitorListEvent'
        - $ref: '#/components/schemas/QueuedInputWithdrawnEvent'
        - $ref: '#/components/schemas/ScheduleFiredEvent'
        - $ref: '#/components/schemas/TaskListEvent'
        - $ref: '#/components/schemas/QuestionAskedEvent'
        - $ref: '#/components/schemas/SessionMovedEvent'
        - $ref: '#/components/schemas/CompactionStartedEvent'
      discriminator:
        propertyName: type
        mapping:
          session-created:
            $ref: '#/components/schemas/SessionCreatedEvent'
          session-status-changed:
            $ref: '#/components/schemas/SessionStatusChangedEvent'
          session-posture-changed:
            $ref: '#/components/schemas/SessionPostureChangedEvent'
          model-switch:
            $ref: '#/components/schemas/ModelSwitchEvent'
          turn-started:
            $ref: '#/components/schemas/TurnStartedEvent'
          message-delta:
            $ref: '#/components/schemas/MessageDeltaEvent'
          message-start:
            $ref: '#/components/schemas/MessageStartEvent'
          message-end:
            $ref: '#/components/schemas/MessageEndEvent'
          compaction:
            $ref: '#/components/schemas/CompactionEvent'
          context-pruned:
            $ref: '#/components/schemas/ContextPrunedEvent'
          entry-appended:
            $ref: '#/components/schemas/EntryAppendedEvent'
          tool-call-started:
            $ref: '#/components/schemas/ToolCallStartedEvent'
          tool-call-update:
            $ref: '#/components/schemas/ToolCallUpdateEvent'
          tool-call-finished:
            $ref: '#/components/schemas/ToolCallFinishedEvent'
          tool-call-receipt:
            $ref: '#/components/schemas/ToolCallReceiptEvent'
          permission-asked:
            $ref: '#/components/schemas/PermissionAskedEvent'
          permission-decided:
            $ref: '#/components/schemas/PermissionDecidedEvent'
          lane-started:
            $ref: '#/components/schemas/LaneStartedEvent'
          lane-finished:
            $ref: '#/components/schemas/LaneFinishedEvent'
          turn-finished:
            $ref: '#/components/schemas/TurnFinishedEvent'
          intercom-message:
            $ref: '#/components/schemas/IntercomMessageEvent'
          workflow-progress:
            $ref: '#/components/schemas/WorkflowProgressEvent'
          ladder-changed:
            $ref: '#/components/schemas/LadderChangedEvent'
          checkpoint-created:
            $ref: '#/components/schemas/CheckpointCreatedEvent'
          extension:
            $ref: '#/components/schemas/ExtensionEvent'
          monitor-event:
            $ref: '#/components/schemas/MonitorEventEvent'
          monitor-list:
            $ref: '#/components/schemas/MonitorListEvent'
          queued-input-withdrawn:
            $ref: '#/components/schemas/QueuedInputWithdrawnEvent'
          schedule-fired:
            $ref: '#/components/schemas/ScheduleFiredEvent'
          task-list:
            $ref: '#/components/schemas/TaskListEvent'
          question-asked:
            $ref: '#/components/schemas/QuestionAskedEvent'
          session-moved:
            $ref: '#/components/schemas/SessionMovedEvent'
          compaction-started:
            $ref: '#/components/schemas/CompactionStartedEvent'
    SessionCreatedEvent:
      description: Envelope for session-created.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: session-created
            payload:
              $ref: '#/components/schemas/SessionCreatedPayload'
      examples:
        - id: '1001'
          type: session-created
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:00:00.000Z'
          payload:
            session:
              id: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
              repository: /Users/dev/repos/midaz
              status: idle
              clientKind: inline
              createdAt: '2026-08-08T12:00:00.000Z'
              updatedAt: '2026-08-08T12:00:00.000Z'
    SessionStatusChangedEvent:
      description: Envelope for session-status-changed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: session-status-changed
            payload:
              $ref: '#/components/schemas/SessionStatusChangedPayload'
      examples:
        - id: '1002'
          type: session-status-changed
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:20:00.000Z'
          payload:
            status: waiting
            previousStatus: running
    SessionPostureChangedEvent:
      description: Envelope for session-posture-changed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: session-posture-changed
            payload:
              $ref: '#/components/schemas/SessionPostureChangedPayload'
      examples:
        - id: '1003'
          type: session-posture-changed
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T23:12:00.000Z'
          payload:
            posture: detached
    ModelSwitchEvent:
      description: Envelope for model-switch.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: model-switch
            payload:
              $ref: '#/components/schemas/ModelSwitchPayload'
      examples:
        - id: '1003'
          type: model-switch
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:25:00.000Z'
          payload:
            provider: anthropic
            model: claude-sonnet-4-5
    TurnStartedEvent:
      description: Envelope for turn-started.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: turn-started
            payload:
              $ref: '#/components/schemas/TurnStartedPayload'
      examples:
        - id: '1003'
          type: turn-started
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:00.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
    MessageDeltaEvent:
      description: Envelope for message-delta.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: message-delta
            payload:
              $ref: '#/components/schemas/MessageDeltaPayload'
      examples:
        - id: '1004'
          type: message-delta
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:02.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            delta: 'The test is flaky because '
    MessageStartEvent:
      description: Envelope for message-start.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: message-start
            payload:
              $ref: '#/components/schemas/MessageStartPayload'
      examples:
        - id: '1005'
          type: message-start
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:01.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            provider: anthropic
            model: claude-sonnet-4-5
    MessageEndEvent:
      description: Envelope for message-end.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: message-end
            payload:
              $ref: '#/components/schemas/MessageEndPayload'
      examples:
        - id: '1006'
          type: message-end
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:09.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            entryId: 7c1d3e5f-0a2b-4c6d-8e0f-1a3b5c7d9e2f
    CompactionEvent:
      description: Envelope for compaction.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: compaction
            payload:
              $ref: '#/components/schemas/CompactionPayload'
      examples:
        - id: '1008'
          type: compaction
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:02:00.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            reason: threshold
            entryId: 9e2f1a3b-5c7d-4e0f-8a2b-4c6d8e0f1a3b
    ContextPrunedEvent:
      description: Envelope for context-pruned.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - laneId
            - payload
          properties:
            type:
              const: context-pruned
            payload:
              $ref: '#/components/schemas/ContextPrunedPayload'
      examples:
        - id: '1009'
          type: context-pruned
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:02:04.000Z'
          payload:
            count: 12
    EntryAppendedEvent:
      description: Envelope for entry-appended.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - laneId
            - payload
          properties:
            type:
              const: entry-appended
            payload:
              $ref: '#/components/schemas/EntryAppendedPayload'
      examples:
        - id: '1005'
          type: entry-appended
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:01:03.000Z'
          payload:
            entryId: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
            kind: user-message
            turnId: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
    ToolCallStartedEvent:
      description: Envelope for tool-call-started.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: tool-call-started
            payload:
              $ref: '#/components/schemas/ToolCallStartedPayload'
      examples:
        - id: '1005'
          type: tool-call-started
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:10.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            toolCallId: call_01
            tool: shell
            input:
              command: go test ./components/ledger/...
    ToolCallUpdateEvent:
      description: Envelope for tool-call-update.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: tool-call-update
            payload:
              $ref: '#/components/schemas/ToolCallUpdatePayload'
      examples:
        - id: '1009'
          type: tool-call-update
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:30.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            toolCallId: call_01
            delta: "ok  \tgithub.com/LerianStudio/narya/internal/wire\t0.412s\n"
    ToolCallFinishedEvent:
      description: Envelope for tool-call-finished.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: tool-call-finished
            payload:
              $ref: '#/components/schemas/ToolCallFinishedPayload'
      examples:
        - id: '1006'
          type: tool-call-finished
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:52.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            toolCallId: call_01
            tool: shell
            status: ok
            durationMs: 42000
    ToolCallReceiptEvent:
      description: Envelope for tool-call-receipt.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: tool-call-receipt
            payload:
              $ref: '#/components/schemas/ToolCallReceiptPayload'
      examples:
        - id: '1007'
          type: tool-call-receipt
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:01:40.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            parentToolCallId: call_07
            tool: read
            status: ok
            title: Read a.md
    PermissionAskedEvent:
      description: Envelope for permission-asked.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: permission-asked
            payload:
              $ref: '#/components/schemas/PermissionAskedPayload'
      examples:
        - id: '1007'
          type: permission-asked
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:20:00.000Z'
          payload:
            permissionId: 7e5d3c1b-9a8f-4e6d-b2c4-0f1a3b5c7d9e
            action: shell
            resource: git push origin main
    PermissionDecidedEvent:
      description: Envelope for permission-decided.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: permission-decided
            payload:
              $ref: '#/components/schemas/PermissionDecidedPayload'
      examples:
        - id: '1008'
          type: permission-decided
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:21:00.000Z'
          payload:
            permissionId: 7e5d3c1b-9a8f-4e6d-b2c4-0f1a3b5c7d9e
            decision: deny
            decidedBy: user
    LaneStartedEvent:
      description: Envelope for lane-started.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: lane-started
            payload:
              $ref: '#/components/schemas/LaneStartedPayload'
      examples:
        - id: '1009'
          type: lane-started
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:10:00.000Z'
          payload:
            kind: subagent
            agent: reviewer
    LaneFinishedEvent:
      description: Envelope for lane-finished.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: lane-finished
            payload:
              $ref: '#/components/schemas/LaneFinishedPayload'
      examples:
        - id: '1010'
          type: lane-finished
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:30:00.000Z'
          payload:
            status: completed
    TurnFinishedEvent:
      description: Envelope for turn-finished.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: turn-finished
            payload:
              $ref: '#/components/schemas/TurnFinishedPayload'
      examples:
        - id: '1011'
          type: turn-finished
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:05:00.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            stopReason: completed
            usage:
              inputTokens: 15000
              outputTokens: 900
              costUsd: 0.12
            costUsd: 0.47
    IntercomMessageEvent:
      description: Envelope for intercom-message.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: intercom-message
            payload:
              $ref: '#/components/schemas/IntercomMessagePayload'
      examples:
        - id: '1012'
          type: intercom-message
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:31:00.000Z'
          payload:
            from: midaz-research
            content: Yes, the fee engine rounds half-even.
            kind: answer
            askId: 8a6b4c2d-0e1f-4a3b-9c5d-7e9f1a3b5c7d
    WorkflowProgressEvent:
      description: Envelope for workflow-progress.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: workflow-progress
            payload:
              $ref: '#/components/schemas/WorkflowProgressPayload'
      examples:
        - id: '1013'
          type: workflow-progress
          timestamp: '2026-08-08T13:04:00.000Z'
          payload:
            runId: 2d4e6f8a-0b2c-4d6e-8f0a-2b4c6d8e0f2a
            status: running
            completedSteps: 1
    LadderChangedEvent:
      description: >-
        Envelope for ladder-changed. Host-level, never session-scoped: what
        changed is a set of files on this machine, and every client on it is
        looking at the same set.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: ladder-changed
            payload:
              $ref: '#/components/schemas/LadderChangedPayload'
      examples:
        - id: '1015'
          type: ladder-changed
          timestamp: '2026-08-08T13:06:00.000Z'
          payload:
            notices:
              - 'packages: /review updated, /skill:extract added'
    CheckpointCreatedEvent:
      description: >-
        Envelope for checkpoint-created: the working tree as it stood at one
        step boundary is now recoverable.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: checkpoint-created
            payload:
              $ref: '#/components/schemas/CheckpointCreatedPayload'
      examples:
        - id: '1017'
          type: checkpoint-created
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-24T13:06:00.000Z'
          payload:
            turnId: 7c0a7e3f-2d4b-4a6c-8e0f-3b9d5f7a1c2e
            entryId: 9e2c1a4b-6d8f-40a1-b3c5-7e9f1a3b5d7f
            treeHash: 4b825dc642cb6eb9a060e54bf8d69288fbee4904
            kind: step
    ExtensionEvent:
      description: Envelope for extension-published events.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: extension
            payload:
              $ref: '#/components/schemas/ExtensionEventPayload'
      examples:
        - id: '1014'
          type: extension
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T13:05:00.000Z'
          payload:
            extension: lerian/ring-review
            name: review-progress
            payload:
              findingsSoFar: 4
              filesRemaining: 11
    MonitorEventEvent:
      description: Envelope for monitor-event.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: monitor-event
            payload:
              $ref: '#/components/schemas/MonitorEventPayload'
      examples:
        - id: '1206'
          type: monitor-event
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-23T12:01:03.000Z'
          payload:
            monitor: tests
            notice: exited
            text: monitor "tests" ended on its own with exit code 1
            exit: 1
            running: []
    MonitorListEvent:
      description: Envelope for monitor-list.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: monitor-list
            payload:
              $ref: '#/components/schemas/MonitorListPayload'
      examples:
        - id: '1207'
          type: monitor-list
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-23T12:01:04.000Z'
          payload:
            running: []
    QueuedInputWithdrawnEvent:
      description: Envelope for queued-input-withdrawn (task 24.2.4).
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: queued-input-withdrawn
            payload:
              $ref: '#/components/schemas/QueuedInputWithdrawnPayload'
      examples:
        - id: '1208'
          type: queued-input-withdrawn
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-23T12:02:10.000Z'
          payload:
            entryId: 7c1e8b3a-2d4f-4a6b-8c9d-1e2f3a4b5c6d
    ScheduleFiredEvent:
      description: >-
        Envelope for schedule-fired. Host-level, never session-scoped: the
        session it names was created a moment ago, so nobody can be watching it
        yet, and that is the whole reason the event exists.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: schedule-fired
            payload:
              $ref: '#/components/schemas/ScheduleFiredPayload'
      examples:
        - id: '1209'
          type: schedule-fired
          timestamp: '2026-09-04T09:00:04.000Z'
          payload:
            scheduleId: 8f2c1a4e-6b0d-4e2f-9a3c-5d7e1f0b2a4c
            sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
    TaskListEvent:
      description: Envelope for task-list.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: task-list
            payload:
              $ref: '#/components/schemas/TaskListPayload'
      examples:
        - id: '1301'
          type: task-list
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-09-04T12:01:04.000Z'
          payload:
            tasks:
              - content: Reproduce the flaky ledger test
                status: completed
    QuestionAskedEvent:
      description: Envelope for question-asked.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: question-asked
            payload:
              $ref: '#/components/schemas/QuestionAskedPayload'
      examples:
        - id: '1302'
          type: question-asked
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-09-04T12:02:00.000Z'
          payload:
            questionId: 4c2e9a71-5b3d-4e8f-9a1c-6d0b2f4e8a3c
            toolCallId: call_01
            questions:
              - question: Where should the export land?
                options:
                  - value: A new table
                    recommended: true
                  - value: Something else
                    other: true
    SessionMovedEvent:
      description: Envelope for session-moved.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: session-moved
            payload:
              $ref: '#/components/schemas/SessionMovedPayload'
      examples:
        - id: '1101'
          type: session-moved
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-09-05T10:00:00.000Z'
          payload:
            repository: /srv/code/api-feat-settlement
            previousRepository: /srv/code/api
    CompactionStartedEvent:
      description: Envelope for compaction-started.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: compaction-started
            payload:
              $ref: '#/components/schemas/CompactionStartedPayload'
      examples:
        - id: '1102'
          type: compaction-started
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:01:52.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            reason: threshold
    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.
    EventBase:
      type: object
      description: >-
        Fields shared by every event on the stream. id is the SSE event id —
        send it back as Last-Event-ID to resume. sessionId and laneId are
        present when the event belongs to a session or lane.
      required:
        - id
        - type
        - timestamp
      properties:
        id:
          type: string
          description: Monotonic per-stream event id, usable as Last-Event-ID.
        type:
          type: string
          description: The event type, discriminating the payload.
        sessionId:
          type: string
          format: uuid
        laneId:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time
      examples:
        - id: '1024'
          type: turn-started
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-08-08T12:34:56.000Z'
    SessionCreatedPayload:
      type: object
      description: A session came into existence.
      required:
        - session
      properties:
        session:
          $ref: '#/components/schemas/Session'
      examples:
        - session:
            id: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
            repository: /Users/dev/repos/midaz
            status: idle
            clientKind: inline
            createdAt: '2026-08-08T12:00:00.000Z'
            updatedAt: '2026-08-08T12:00:00.000Z'
    SessionStatusChangedPayload:
      type: object
      description: A session's status changed — the panel's live indicator.
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - idle
            - running
            - waiting
            - archived
        previousStatus:
          type: string
          enum:
            - idle
            - running
            - waiting
            - archived
      examples:
        - status: waiting
          previousStatus: running
    SessionPostureChangedPayload:
      type: object
      description: >-
        Somebody recorded whether anybody is watching this session — the
        promotion gesture (`narya session promote`, and the answer to the
        clients' exit prompt) seen from the stream.

        It is emitted only when the posture actually MOVED, so recording a
        posture a session already holds is silent. Nothing infers this event: a
        client connecting, crashing or disconnecting never produces one, because
        attendance is asserted and never inferred.
      required:
        - posture
      properties:
        posture:
          $ref: '#/components/schemas/Posture'
        previousPosture:
          $ref: '#/components/schemas/Posture'
      examples:
        - posture: detached
    ModelSwitchPayload:
      type: object
      description: >-
        The session's provider/model pair moved — mid-session model switching,
        whether a person picked the new model or the host substituted it. The
        pair named here is the one the NEXT turn will run on.
      required:
        - provider
        - model
      properties:
        provider:
          type: string
          description: The provider the session is on now, e.g. anthropic.
        model:
          type: string
          description: The model id the session is on now, as the catalogue names it.
      examples:
        - provider: anthropic
          model: claude-sonnet-4-5
    TurnStartedPayload:
      type: object
      description: A turn began executing.
      required:
        - turnId
      properties:
        turnId:
          type: string
          format: uuid
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
    MessageDeltaPayload:
      type: object
      description: >-
        A fragment of streamed text from the ASSISTANT. With no channel it is
        the answer — what the model said. With channel `reasoning` it is the
        model's own thinking, which is activity rather than answer: a client may
        show that thinking is happening, and must never fold it into what the
        assistant said. The opaque provider material that makes a reasoning
        block replayable never travels on this event.

        A running tool call's own output travels on `tool-call-update`, not
        here. It used to ride this event with a toolCallId and channel
        `display`; both fields remain declared because events recorded before
        that change replay forever, and a client reading an old session must
        still be able to decode them. Nothing emits them now.
      required:
        - turnId
        - delta
      properties:
        turnId:
          type: string
          format: uuid
        delta:
          type: string
          description: The next fragment of the message text.
        toolCallId:
          type: string
          description: >-
            Retired. Present only on events recorded before a running tool
            call's output moved to `tool-call-update`.
        channel:
          type: string
          enum:
            - display
            - reasoning
          description: >-
            Which channel this fragment came from. `reasoning` is the model
            thinking aloud; absent means the assistant's answer. `display` is
            retired — it marked a running tool call's own output, which is now
            its own event, and appears only on events recorded before that
            change.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          delta: 'The test is flaky because '
    MessageStartPayload:
      type: object
      description: >-
        One assistant message is about to be generated: the request is built and
        one call away from the provider. It names the provider and model this
        round will actually be answered by, which is what lets a cost or audit
        consumer attribute the round before its first token rather than after
        the turn.

        A turn has one of these per round — an answer, a tool-calling round, an
        answer again are three rounds and three starts. The summarisation round
        a compaction sends is not a message and produces none.

        One round is not always ONE message, though: see message-end. Count
        committed messages by their ends, never by their starts.
      required:
        - turnId
        - provider
        - model
      properties:
        turnId:
          type: string
          format: uuid
        provider:
          type: string
          description: The provider this round is being sent to.
        model:
          type: string
          description: The model id this round is being answered by.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          provider: anthropic
          model: claude-sonnet-4-5
    MessageEndPayload:
      type: object
      description: >-
        One assistant message is durably committed, named by the transcript
        entry that holds it. It is a NOTIFICATION of a fact already recorded: a
        consumer cannot revise, replace or reject the message, because it landed
        before this event existed.

        A round that died before committing anything emits no end at all, so a
        start is not guaranteed a partner. The turn's own outcome is
        turn-finished's.

        One round can also commit TWO messages, and then emits two ends under
        its one start: a round whose stream dies mid-answer on a context
        overflow keeps the words it already said, and the retry that follows the
        compaction commits its own answer beside them. Count ends per committed
        message, never per start — every end names the transcript entry it is
        about.
      required:
        - turnId
        - entryId
      properties:
        turnId:
          type: string
          format: uuid
        entryId:
          type: string
          format: uuid
          description: The transcript entry this message committed as.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          entryId: 7c1d3e5f-0a2b-4c6d-8e0f-1a3b5c7d9e2f
    CompactionPayload:
      type: object
      description: >-
        The session's transcript was summarised: what the model is working from
        is no longer a straight append of everything that happened, and this
        entry is where the older part now lives.

        reason says what brought it about. `threshold` is the projection
        refusing to send a round that would not fit — the designed path, no
        request wasted. `overflow` is the provider having refused a request the
        projection passed, which means the estimate was wrong and the round cost
        a round-trip to discover it. `behavioural` is a round that SUCCEEDED and
        still carried a signature of overflow: the provider never said so, and
        the evidence was in what came back. `manual` is a person having asked
        for it by name through `compactLane`, with nothing overflowing at all.
      required:
        - turnId
        - reason
        - entryId
      properties:
        turnId:
          type: string
          format: uuid
          description: >-
            The turn this compaction happened inside. A `manual` compaction runs
            between turns, so there is no turn to name and this carries the
            compaction's own operation id instead — a correlation handle for its
            durable writes, and never the id of any turn.
        reason:
          type: string
          enum:
            - threshold
            - overflow
            - behavioural
            - manual
          description: >-
            `threshold`: the pre-send projection said the next round no longer
            fits. `overflow`: the provider refused the request. `behavioural`:
            the round succeeded and its own shape said the window was already
            full. `manual`: somebody asked for it.
        entryId:
          type: string
          format: uuid
          description: The summary entry that replaced the summarised history.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          reason: threshold
          entryId: 9e2f1a3b-5c7d-4e0f-8a2b-4c6d8e0f1a3b
    ContextPrunedPayload:
      type: object
      description: >-
        Old tool output was taken out of a lane's window: the model stops
        re-reading results the conversation has moved past, and nothing was
        summarised or destroyed to achieve it. The transcript still holds every
        result in full, and a client renders them exactly as before — only what
        the model is re-sent shrinks.

        Its own event type rather than a `compaction` with another reason,
        deliberately: a prune writes no summary entry, so it has nothing to put
        in `CompactionPayload.entryId`, and every consumer counting compactions
        would have counted this one.

        The same event fires whether a person asked through `pruneLane` or the
        host's own threshold pruned before summarising, because a client
        watching a lane needs the same fact either way.

        It carries no token figure. Nobody can say what the shrunken window
        costs until a provider counts it, so a client showing a context reading
        should drop it and wait for the next metered round — the same silence
        the `compaction` event asks for. It also carries no origin. A prune the
        host ran before considering a summary and a prune somebody typed are the
        same change to the same window, and the one person who could act on the
        difference is the one who just typed it. `compaction` carries a reason
        because its four triggers cost different things; a prune costs nothing
        however it started.
      required:
        - count
      properties:
        count:
          type: integer
          minimum: 1
          description: >-
            How many tool results were set aside. Never zero: a prune that found
            nothing eligible writes nothing and announces nothing.
      examples:
        - count: 12
    EntryAppendedPayload:
      type: object
      description: >-
        Stable identity for one transcript entry that committed durably. The
        event envelope identifies its session and lane; turnId is present when
        the entry belongs to a turn.
      required:
        - entryId
        - kind
      properties:
        entryId:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - user-message
            - assistant-message
            - tool-call
            - tool-result
            - summary
            - system
            - intercom
            - monitor-event
        turnId:
          type: string
          format: uuid
      examples:
        - entryId: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
          kind: user-message
          turnId: 9c2b7a10-3e4d-4f6a-8b1c-5d7e9f0a2b3c
    ToolCallStartedPayload:
      type: object
      description: The agent began executing a tool call.
      required:
        - turnId
        - toolCallId
        - tool
      properties:
        turnId:
          type: string
          format: uuid
        toolCallId:
          type: string
        tool:
          type: string
          description: The tool's name.
        input:
          type: object
          description: >-
            A preview of the tool call's input as the model produced it: any
            top-level string value over 512 runes is truncated with a trailing
            ellipsis. The full, untruncated input is durable in the tool-call
            transcript entry this event announces — this field exists for a
            receipt or sidebar preview, not as a second copy of arbitrarily
            large arguments on the broadcast event stream.
          additionalProperties: true
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          toolCallId: call_01
          tool: shell
          input:
            command: go test ./components/ledger/...
    ToolCallUpdatePayload:
      type: object
      description: >-
        One chunk of a RUNNING tool call's own output — a shell command's bytes
        as they arrive — named for the call it belongs under. A client renders
        it beneath that call's line, never as the assistant speaking; a chunk
        for a call the client never saw start has no line to sit under and is
        dropped rather than shown.

        The bytes are the tool's, not the model's: they are unsanitized on the
        wire and a client that draws them to a terminal owns that boundary.
      required:
        - turnId
        - toolCallId
        - delta
      properties:
        turnId:
          type: string
          format: uuid
        toolCallId:
          type: string
          description: The running call whose output this is.
        delta:
          type: string
          description: The next chunk the tool wrote.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          toolCallId: call_01
          delta: "ok  \tgithub.com/LerianStudio/narya/internal/wire\t0.412s\n"
    ToolCallFinishedPayload:
      type: object
      description: >-
        A tool call finished, with its outcome.


        IT IS ALSO WHAT RETIRES A QUESTION. `ask_user` is a tool, so a call that
        put questions to the person (question-asked, carrying this same
        toolCallId) finishes when they are answered, when its turn is
        interrupted, and when a starting host closes what a dead one left
        pending. A client showing the prompt drops it here, on the call it
        already joined on, and no question-decided event exists because it would
        say nothing this one does not.
      required:
        - turnId
        - toolCallId
        - tool
        - status
      properties:
        turnId:
          type: string
          format: uuid
        toolCallId:
          type: string
        tool:
          type: string
        status:
          type: string
          enum:
            - ok
            - error
            - denied
            - cancelled
        durationMs:
          type: integer
          minimum: 0
        failure:
          type: string
          description: >-
            Why the call failed, in the words the model was given, bounded by
            the host. Present only for a status other than `ok`, and absent when
            the tool said nothing beyond failing. It exists so a client can show
            the reason at the moment the call fails: only a streaming tool sends
            progress chunks, so every other failed call settled a red line with
            nothing under it, and the reason was reachable only by reopening the
            session's durable record.
        fileTouches:
          type: array
          description: The rows this result produced (reads and successful mutations only).
          items:
            $ref: '#/components/schemas/FileTouch'
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          toolCallId: call_01
          tool: shell
          status: ok
          durationMs: 42000
    ToolCallReceiptPayload:
      type: object
      description: >-
        One tool call a model-authored program made from inside itself. It is
        the display channel of that call's three-channel result and nothing
        else: the model text and the structured value stayed inside the program
        and never re-enter model context. parentToolCallId is the run_program
        call the receipt belongs to, which is what a client collapses the line
        under.
      required:
        - turnId
        - parentToolCallId
        - tool
        - status
      properties:
        turnId:
          type: string
          format: uuid
        parentToolCallId:
          type: string
          description: The program's own tool-call id.
        tool:
          type: string
          description: The tool the program called.
        status:
          type: string
          enum:
            - ok
            - error
        title:
          type: string
          description: >-
            The one line a client renders — verb, object and outcome. Bounded by
            the host, because a receipt is one line and a tool's title can carry
            its caller's own argument.
        fileTouches:
          type: array
          description: >-
            The file rows this call produced, so a touched-files panel is TOLD
            what was touched rather than inferring it from a tool name. At most
            128 rows; fileTouchesTruncated says when there were more.
          items:
            $ref: '#/components/schemas/FileTouch'
        fileTouchesTruncated:
          type: boolean
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          parentToolCallId: call_07
          tool: edit
          status: ok
          title: Edit internal/wire/sse.go
          fileTouches:
            - path: /Users/dev/repos/midaz/internal/wire/sse.go
              op: edit
    PermissionAskedPayload:
      type: object
      description: A sensitive action is waiting on a decision.
      required:
        - permissionId
        - action
        - resource
      properties:
        permissionId:
          type: string
          format: uuid
        action:
          type: string
        resource:
          type: string
        detail:
          type: string
          description: >-
            Human-readable presentation, including the rule a remembered allow
            would grant.
        toolCallId:
          type: string
          description: >-
            The tool call this ask is blocking, when one raised it — the id
            tool-call-started already announced. It is the JOIN that lets a
            client show what is being decided rather than only its name: the
            call's own input is already on that earlier event, so a reader asked
            to approve an edit can be shown the patch instead of a path. Absent
            when no tool call raised the ask, and absent on the pending listing
            a reattaching client reads, so a client renders the ask from action,
            resource and detail alone whenever it cannot resolve the call.
      examples:
        - permissionId: 7e5d3c1b-9a8f-4e6d-b2c4-0f1a3b5c7d9e
          action: shell
          resource: git push origin main
          detail: 'Allow running: git push origin main'
          toolCallId: call_01
    PermissionDecidedPayload:
      type: object
      description: >-
        A permission ask was settled — answered by a human, resolved by policy,
        or closed unanswered. A client drops the prompt on any of the three: the
        question can no longer be answered.
      required:
        - permissionId
        - decision
        - decidedBy
      properties:
        permissionId:
          type: string
          format: uuid
        decision:
          type: string
          description: >-
            `expired` is the ask nobody decided — the turn was interrupted, the
            tool call's own deadline passed, or the question could not be
            delivered to anybody. It is not a refusal: the action did not run,
            and no answer was given about it. Why it closed is on the audit row,
            which carries a reason; this stream carries only the fact, so a
            client can retire the prompt. The same word the decisions listing
            already filters by (`GET /permissions/decisions?decision=expired`),
            for the same state.
          enum:
            - allow
            - deny
            - expired
        decidedBy:
          type: string
          enum:
            - user
            - policy
            - hook
      examples:
        - permissionId: 7e5d3c1b-9a8f-4e6d-b2c4-0f1a3b5c7d9e
          decision: deny
          decidedBy: user
    LaneStartedPayload:
      type: object
      description: A lane opened. The envelope's laneId identifies it.
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - main
            - side
            - subagent
            - workflow
        agent:
          type: string
          description: >-
            The agent recipe running in the lane, for subagent and workflow
            lanes.
      examples:
        - kind: subagent
          agent: reviewer
    LaneFinishedPayload:
      type: object
      description: A lane finished or closed.
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - completed
            - failed
            - cancelled
      examples:
        - status: completed
    TurnFinishedPayload:
      type: object
      description: >-
        A turn ended, with why, what it consumed, and — when it died — what
        killed it. usage is the LAST supplier round's figures rather than a sum
        across the turn's rounds (a turn that uses tools resends the whole
        growing conversation on every round, so those counts read as "how full
        is the context now"); costUsd beside it is the whole turn's money. error
        is the same NRY envelope every operation returns, so a client renders a
        failed turn through the code path it already has for a failed request.
        It is optional because a turn that completed has no cause to report, and
        it is the ONLY account of the cause any client gets: without it a failed
        turn reaches every watcher as stopReason "error" and nothing else, while
        the real error stays in the host process's own stderr — which, for a
        detached daemon, nobody reads.
      required:
        - turnId
        - stopReason
      properties:
        turnId:
          type: string
          format: uuid
        stopReason:
          type: string
          description: >-
            max-tokens is the honest "truncated" outcome: the supplier stopped
            on its own output ceiling, not because the model reached a natural
            end of turn.

            content-filter is its sibling, and the reason this value exists at
            all: the provider's own safety system ended the turn — an openai
            family's finish_reason content_filter, an anthropic family's
            stop_reason refusal. Without it both fold into "completed", so a
            censored or half-censored answer reaches every client as a turn the
            model finished. Like max-tokens it is NOT a failure: whatever text
            arrived before the filter fired is real output, so it carries no
            error envelope and a one-shot still exits 0 with that text on stdout
            — the incompleteness is stated, not turned into a fault nobody can
            act on.

            split is the third non-failure: the turn's own work did not fit the
            model even after the conversation behind it was summarised, so it
            was cut here and a successor turn was opened on the compacted
            window, carrying a note that says so. The work continues — a client
            that sees two turns where a person sent one message reads this as
            the reason, and the turn that carries it has no error envelope
            because nothing failed. It is bounded at one split per chain: a turn
            that still does not fit after a split ends as error instead. budget
            is the fourth, and it is a bound being honoured rather than anything
            going wrong: the turn was admitted under a spend ceiling — a
            scheduled fire's, today the only caller that sets one — and it ended
            on the first supplier round that took the turn's running cost to
            that number. Whatever text arrived is real output, so it carries no
            error envelope, and the tool calls the last round was still asking
            for were not run. Overshoot is bounded by that one round, because a
            round's price is known only once it has ended, and a bounded turn
            may not delegate: nothing sums another lane's spend into the turn
            that asked for it.

            A budget turn reporting a costUsd spent its ceiling. A budget turn
            reporting none stopped for the opposite reason — no round of it
            could be priced at all, and a ceiling that cannot be measured is not
            a ceiling, so the work stops rather than running on unmetered.
          enum:
            - completed
            - interrupted
            - error
            - max-tokens
            - content-filter
            - split
            - budget
        usage:
          $ref: '#/components/schemas/TokenUsage'
        costUsd:
          type: number
          minimum: 0
          description: >-
            What the WHOLE turn cost, in US dollars — every supplier round it
            ran, summed, not just the last one (usage.costUsd). A turn that
            calls tools runs several rounds and each is metered and priced
            separately, so on a ten-round turn the last round's price is a
            fraction of the turn's. Absent unless every round could be priced: a
            sum over only the priced rounds is smaller than what the turn
            actually cost, and a readout that quietly understates money is worse
            than one that says it does not know.
        elapsedMs:
          type: integer
          minimum: 0
          description: >-
            How long the host had this turn in hand, in milliseconds, read off
            its own monotonic clock: from the instant it took the turn up — the
            instant turn-started announces — to this event. The measurement is
            the HOST's, which is the whole point of carrying it. A watcher
            timing the gap between two deliveries times its own queue as well,
            and its stopwatch restarts on every turn-started, so a turn that was
            split reads as nothing but its tail. Absent only on an event
            replayed from a journal written before this field existed.
        chainElapsedMs:
          type: integer
          minimum: 0
          description: >-
            How long the whole unit of work took, in milliseconds: from the
            start of the turn that was SPLIT to this turn's end. Present ONLY on
            a turn that continued a split, so a client can say what one message
            really cost in time without mislabelling elapsedMs, which stays
            honest about the turn it is printed beside. A message queued behind
            a turn is a new unit of work rather than a continuation, and carries
            none.
        contextWindow:
          type: integer
          minimum: 1
          description: >-
            The context window, in tokens, of the model THIS turn ran on — the
            denominator usage is a fraction of. It rides here rather than on
            usage because usage is also a session total across many turns and
            possibly many models, where a single window would have no owner.

            It exists because a client cannot resolve the pair itself: a turn
            run under an agent recipe that overrides provider and model, or on a
            delegated lane, does not run on the session's model, and a client
            dividing that turn's tokens by the session model's window reports a
            percentage of the wrong number. The host knows the effective pair at
            the instant the turn ends, so it states the window and the client
            divides what it received.

            Absent when the catalogue states no window for the model that ran,
            and on any event replayed from a journal written before this field
            existed — a client that finds it absent falls back to whatever
            denominator it used before, which is what it does today.
        error:
          $ref: '#/components/schemas/Error'
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          stopReason: completed
          usage:
            inputTokens: 15000
            outputTokens: 900
            costUsd: 0.12
          costUsd: 0.47
          elapsedMs: 8400
        - turnId: 9c1d3e5f-0a2b-4c4d-8e6f-1a3b5c7d9e0f
          stopReason: error
          error:
            code: NRY-0018
            title: No usable model
            message: >-
              No model is selected for this session. Sign in with `narya auth
              login <provider>` — narya then runs on that provider's default
              model — or set default_model in
              /home/dana/.config/narya/config.toml, then send your message
              again. `narya providers` lists what this machine can use.
          elapsedMs: 240
        - turnId: 2f0a4b6c-8d1e-4f3a-9b5c-7d9e1f3a5b7c
          stopReason: completed
          costUsd: 0.31
          elapsedMs: 51200
          chainElapsedMs: 184000
    IntercomMessagePayload:
      type: object
      description: >-
        An inter-session message arrived — a one-way message, a question, or the
        answer to an earlier ask (askId present).
      required:
        - from
        - content
        - kind
      properties:
        from:
          type: string
          description: The sending session's addressable name.
        content:
          type: string
        kind:
          type: string
          enum:
            - message
            - ask
            - answer
        askId:
          type: string
          format: uuid
          description: Correlates a question with its answer.
      examples:
        - from: midaz-research
          content: Yes, the fee engine rounds half-even.
          kind: answer
          askId: 8a6b4c2d-0e1f-4a3b-9c5d-7e9f1a3b5c7d
    WorkflowProgressPayload:
      type: object
      description: A workflow run advanced — the run or one of its steps changed state.
      required:
        - runId
        - status
      properties:
        runId:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - running
            - completed
            - failed
            - stopped
          description: The run's overall status after this update.
        step:
          $ref: '#/components/schemas/WorkflowStep'
          description: The step that changed, when the update concerns one step.
        completedSteps:
          type: integer
          minimum: 0
        totalSteps:
          type: integer
          minimum: 0
      examples:
        - runId: 2d4e6f8a-0b2c-4d6e-8f0a-2b4c6d8e0f2a
          status: running
          step:
            name: run-tests
            status: completed
            output: 412 tests passed
          completedSteps: 1
    LadderChangedPayload:
      type: object
      description: >-
        An installed package's files changed on disk and the host republished
        them, so what a person can type has moved. A client re-reads
        listCommands and listSkills when this arrives and shows the notices; it
        is never sent for a write that changed nothing anybody can type.
      required:
        - notices
      properties:
        notices:
          type: array
          description: >-
            What changed, in the words a person reads — "packages: /review
            updated" — alongside anything that went wrong doing it. Empty is
            possible for a client that only wants the re-read signal.
          items:
            type: string
    CheckpointCreatedPayload:
      type: object
      description: >-
        A restore point exists: everything the working tree held at this step
        can be put back, per file or whole. The tree named here lives in narya's
        shadow repository beside the checkout and never in the person's own git
        history, so nothing in `git log` knows about it. Checkpoints are never
        swept — not on a timer, not at startup, not because a threshold was
        crossed — because a rewind to state from months ago has to work.
      required:
        - turnId
        - treeHash
        - kind
      properties:
        turnId:
          type: string
          format: uuid
          description: The turn whose step this restore point closes.
        entryId:
          type: string
          format: uuid
          description: >-
            The transcript entry the checkpoint stands at. Absent for one that
            stands at no moment — a session's baseline, taken before anything
            was said.
        treeHash:
          type: string
          description: >-
            The git tree object in the shadow repository. Two consecutive
            checkpoints carrying the same hash is how a step that changed no
            file says so.
          maxLength: 64
        kind:
          type: string
          enum:
            - step
            - pre-rewind
            - manual
          description: >-
            Why the checkpoint exists. step is an ordinary step boundary;
            pre-rewind is the working tree as it was immediately before a rewind
            restored over it, which is what makes a rewind undoable; manual is
            one somebody asked for.
    ExtensionEventPayload:
      type: object
      description: >-
        An event an extension published onto the stream. The envelope's type is
        always `extension`; which extension spoke and what it said live here.
        This is how a host extension feeds its client half (or any subscriber)
        without the core contract knowing its vocabulary.
      required:
        - extension
        - name
      properties:
        extension:
          type: string
          description: Namespaced name of the publishing extension.
          pattern: ^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$
        name:
          type: string
          description: The extension's own event name, unique within it.
          maxLength: 128
        payload:
          type: object
          description: Free-form payload — the extension's own contract.
          additionalProperties: true
      examples:
        - extension: lerian/ring-review
          name: review-progress
          payload:
            findingsSoFar: 4
            filesRemaining: 11
    MonitorEventPayload:
      type: object
      description: >-
        Something a monitor said, ready to draw. text is the whole sentence,
        attributed to the monitor, in the words a person reads — the same form
        the transcript read serves, and a client prints it without asking for
        anything else. The model reads its own copy of the same event off the
        transcript, framed for a model rather than for a reader. The matching
        entry-appended event carries the durable entry this is the body of; a
        client that renders this one must not draw that one again.
      required:
        - monitor
        - notice
        - text
        - running
      properties:
        monitor:
          type: string
        notice:
          type: string
          enum:
            - started
            - output
            - throttled
            - exited
            - stopped
        text:
          type: string
        exit:
          type: integer
          description: The process's exit code, on the notice that reports it.
        running:
          type: array
          description: >-
            Every monitor this session is running after this event, by name. It
            rides each event so a status line and a session panel stay true
            without a read of their own.
          items:
            type: string
      examples:
        - monitor: tests
          notice: output
          text: |-
            monitor "tests":
            FAIL ledger_test.go:12
          running:
            - tests
    MonitorListPayload:
      type: object
      description: >-
        Which monitors a session is running, and nothing else. It carries no
        words, so no client draws it — it exists because the LIST must not wait
        for a turn to end while a monitor's WORDS must. A monitor's words go
        into the transcript, whose tail belongs to whatever turn is running, so
        a monitor-event is held until that turn ends; the running list is not
        part of the conversation and is announced the moment it changes. Sent on
        every change: a watcher starting, one being stopped, and one whose
        process ended by itself — the last of which nothing else reports until
        the turn is over.
      required:
        - running
      properties:
        running:
          type: array
          description: Every monitor this session is running, by name, in start order.
          items:
            type: string
      examples:
        - running:
            - tests
    QueuedInputWithdrawnPayload:
      type: object
      description: >-
        A message that was waiting behind a running turn is no longer waiting,
        because somebody took it back (`DELETE
        /v1/sessions/{sessionId}/inputs/{entryId}`).

        It is the other half of an announcement the stream has always carried
        one side of. A message ARRIVING in the queue is written with its
        entry-appended event in one transaction, so every attached window learns
        of it; before this event a message LEAVING the queue was told only to
        the window that asked, and a second window on the same session went on
        drawing it — and offering to drop it again — until something unrelated
        made it re-read the session.

        Written in the same transaction as the withdrawal itself, so a client is
        never told a message left a queue it is still in. A withdrawal that was
        refused — nothing was waiting under that id — emits nothing.

        The entry stays in the transcript with the reason it was set aside,
        which is what a client reads to draw it: this event names which message
        left and nothing more.
      required:
        - entryId
      properties:
        entryId:
          type: string
          format: uuid
          description: >-
            The transcript entry whose queued message was withdrawn — the same
            id PendingInputReference carries, so a window holding several
            waiting messages knows which one to remove.
      examples:
        - entryId: 7c1e8b3a-2d4f-4a6b-8c9d-1e2f3a4b5c6d
    ScheduleFiredPayload:
      type: object
      description: >-
        The host's clock fired a schedule and the session it owed now exists,
        already running its turn. Both ids are here so a client can walk
        straight into the work — from the schedule that asked for it, to the
        session doing it — rather than polling a listing to notice that
        something started while nobody was looking.
      required:
        - scheduleId
        - sessionId
      properties:
        scheduleId:
          type: string
          format: uuid
        sessionId:
          type: string
          format: uuid
    TaskListPayload:
      type: object
      description: >-
        The agent's task list for this session, whole, as it now stands.

        In `monitor-list`'s words and for its reason: the engine decides the
        list and the client draws it, so every change carries the entire list
        rather than a delta nobody can apply to a list they may not have. A
        client that missed one frame is corrected by the next, and one that
        attached late reads the same list off the session.

        Sent on every write, including the write that empties it — an agent that
        finishes and clears its list is telling the person something, and an
        empty array says it.
      required:
        - tasks
      properties:
        tasks:
          type: array
          description: Every task this session has, in the order the agent wrote them.
          items:
            $ref: '#/components/schemas/Task'
      examples:
        - tasks:
            - content: Reproduce the flaky ledger test
              status: completed
            - content: Fix the race in the balance reader
              status: in_progress
    QuestionAskedPayload:
      type: object
      description: >-
        The agent is asking the person to choose, and its turn is blocked until
        somebody does.

        It is NOT a permission ask and does not reuse one: `action`, `resource`,
        `status`, `decision` and `remember` are all either meaningless or lying
        about a question, and a client would have to branch inside the
        permission box on whether the ask it is drawing is really a permission.
        The engine shares one table and one desk with the permission road, where
        sharing is cheap; the contract says what the thing is.

        THERE IS NO `question-decided` EVENT. The call that raised the question
        is a tool call, so its closure — answered, interrupted, or closed by a
        starting host after this one died — rides `tool-call-finished` for the
        call named by `toolCallId`, and a client showing the prompt retires it
        there.
      required:
        - questionId
        - questions
      properties:
        questionId:
          type: string
          format: uuid
          description: >-
            The question's id, which is what an answer is posted against (`POST
            /v1/questions/{questionId}/answer`).
        toolCallId:
          type: string
          description: >-
            The tool call this question is blocking — the id tool-call-started
            already announced, and the same JOIN key PermissionAskedPayload
            documents. It is how a client retires the prompt when the call
            finishes, and how it shows the question beside the work that raised
            it. Absent when no tool call raised it, which is not the ordinary
            road.
        questions:
          type: array
          description: >-
            The questions, in the order they were asked and with their options
            already ordered. An answer is matched to its question by position.
          items:
            $ref: '#/components/schemas/Question'
      examples:
        - questionId: 4c2e9a71-5b3d-4e8f-9a1c-6d0b2f4e8a3c
          toolCallId: call_01
          questions:
            - header: Storage
              question: Where should the export land?
              options:
                - value: A new table
                  recommended: true
                - value: A file on disk
                  preview: One CSV per run under ./exports.
                - value: Something else
                  other: true
    SessionMovedPayload:
      type: object
      description: >-
        The session is working in another directory (`moveSession`): from the
        next turn onwards its tools are confined to the new root, and every
        surface that names the directory a session is on is now naming the wrong
        one until it folds this in.

        It is emitted only when the path actually MOVED, so re-homing a session
        to the directory it already stands in is silent. Nothing infers it
        either: a session is never observed to have moved, it is moved by a
        call.

        What it does not carry, because the response to that call does: the
        session's restore points were dropped by the move. A client drawing a
        "go back to how the files were" surface re-reads it after this event
        rather than reasoning from the two paths.
      required:
        - repository
        - previousRepository
      properties:
        repository:
          type: string
          description: Absolute path of the directory the session works in now.
        previousRepository:
          type: string
          description: >-
            The directory it left. Always present, unlike
            `SessionPostureChangedPayload.previousPosture`: a session cannot
            exist without a directory, so there is no "never stated" case here.
      examples:
        - repository: /srv/code/api-feat-settlement
          previousRepository: /srv/code/api
    CompactionStartedPayload:
      type: object
      description: >-
        A compaction has BEGUN: the host has decided the transcript must be
        summarised and is about to spend a model round doing it. Nothing is
        summarised yet and nothing is committed — this is the moment the
        decision was taken and the money is about to be spent.

        Its own event type rather than a `compaction` with an absent `entryId`,
        deliberately and for the reason `ContextPrunedPayload` states about
        itself: every consumer counting compactions would have counted this one.

        HOW IT ENDS, stated plainly because a client that guessed would leave a
        word on screen forever: a compaction that commits announces `compaction`
        for the same turn, and a compaction that FAILS announces nothing at all.
        So whatever state a client opens here is cleared on that `compaction` OR
        on `turn-finished` for the same turn, whichever comes first — the three
        reasons the host starts a compaction for all live inside a turn, and a
        turn always finishes. No timer, nothing guessed.

        It is emitted for `manual` too, where the client that asked for the
        compaction is already showing the state and this changes nothing on its
        screen. One funnel, one event: the OTHER window on the same session has
        no other way to learn that a round is running.

        Session-and-lane scoped, exactly like `compaction`.
      required:
        - turnId
        - reason
      properties:
        turnId:
          type: string
          format: uuid
          description: >-
            The turn this compaction is running inside. A `manual` compaction
            runs between turns, so there is no turn to name and this carries the
            compaction's own operation id instead — the same value
            `CompactionPayload.turnId` carries, so a start and its commit
            correlate on one field.
        reason:
          type: string
          enum:
            - threshold
            - overflow
            - behavioural
            - manual
          description: >-
            Why it started, in `CompactionPayload.reason`'s own four words and
            with its meanings: `threshold` the pre-send projection refusing a
            round that would not fit, `overflow` the provider having refused one
            the projection passed, `behavioural` a round that succeeded while
            showing a signature of overflow, `manual` somebody having asked.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          reason: threshold
    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 repository the session works in.
        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. Two 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
            named model the catalogue no longer carries, for which the provider
            default was substituted. 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. 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
            as `attended` — 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.
        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
      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
          usage:
            inputTokens: 152000
            outputTokens: 8400
            costUsd: 1.87
          createdAt: '2026-08-08T12:00:00.000Z'
          updatedAt: '2026-08-08T12:34:56.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, which is what every
        session starts as. 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.

        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.
    FileTouch:
      type: object
      description: One file a tool call read or changed.
      required:
        - path
        - op
      properties:
        path:
          type: string
          maxLength: 4096
        op:
          type: string
          enum:
            - read
            - write
            - edit
            - delete
      examples:
        - path: /Users/dev/repos/midaz/internal/wire/sse.go
          op: edit
    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
    WorkflowStep:
      type: object
      description: >-
        One step of a workflow run. A failed step never silently disappears; a
        stop marks running steps stopped and unstarted steps skipped.
      required:
        - name
        - status
      properties:
        name:
          type: string
          maxLength: 256
        status:
          type: string
          enum:
            - pending
            - running
            - completed
            - failed
            - stopped
            - skipped
        output:
          type: string
          description: The step's result or partial result, as the script reported it.
        startedAt:
          type: string
          format: date-time
        finishedAt:
          type: string
          format: date-time
      examples:
        - name: run-tests
          status: completed
          output: 412 tests passed
          startedAt: '2026-08-08T13:00:00.000Z'
          finishedAt: '2026-08-08T13:04:00.000Z'
    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
    Question:
      type: object
      description: >-
        One question the agent is putting to the person. Up to four ride in one
        call, and the answer names which option index was chosen rather than
        repeating its label, so the model can tell a pick from a typed sentence.
      required:
        - question
        - options
      properties:
        header:
          type: string
          description: A short chip naming what this question is about.
        question:
          type: string
          description: The question itself.
        multiple:
          type: boolean
          description: >-
            More than one option may be chosen. Absent or false is a single
            choice, and a client draws checkboxes only when this says so.
        options:
          type: array
          description: >-
            The options in the order they must be drawn — recommended first,
            free-text row last, both placed by the engine. Two to four options
            the model wrote, plus that row.
          items:
            $ref: '#/components/schemas/QuestionOption'
    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. Presentation metadata only:
        nothing in the host behaves differently because of its value.
    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'
    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
    QuestionOption:
      type: object
      description: >-
        One answer a person may pick.

        `recommended` is a FIELD rather than a convention: the alternative is
        the model typing "(recommended)" into a label, which puts narya's own
        vocabulary inside text the model authored and leaves a client parsing
        for it.
      required:
        - value
      properties:
        value:
          type: string
          description: The option as the person reads it.
        recommended:
          type: boolean
          description: >-
            The agent's own preference. The engine has already moved the
            recommended option to the front of the list, so a client drawing the
            options in the order it received them draws them right — and a
            client must never reorder, because the indices it sends back are the
            engine's.
        preview:
          type: string
          description: >-
            What this option would mean, at length, for the side-by-side pane a
            client draws beside the option list. Absent when the option speaks
            for itself.
        other:
          type: boolean
          description: >-
            The free-text row, which the ENGINE adds to every question and the
            model never writes. A client draws it as the row that takes typing;
            an answer naming its index is an answer somebody typed, and the
            typed text rides the same answer's `text`.
  responses:
    BadRequest:
      description: Malformed request — invalid parameter, cursor, or JSON body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: >-
        The resource's current state rejects the request — archived session,
        busy lane, slot conflict, failed build, already-final state.
      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'

````