> ## 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. The data field of each frame is one JSON Event envelope, discriminated by its type property.
A FINISHED TURN'S EVENTS ARE FORGOTTEN. About thirty seconds after a turn ends its events are deleted, and a start of the host deletes each finished turn's events older than thirty seconds, however recently that turn ended; the transcript is the record of the turn. A replay from a Last-Event-ID below anything this stream has forgotten begins with one replay-gap frame with no SSE id line, so it moves no cursor, followed by the events that remain. A client that receives it re-reads the transcript (listSessionEntries) from what it already holds and resumes the stream from that page's lastEventId.
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: it replays the events that remain past it, opening with a replay-gap frame when the stream forgot some of them. The remedy is to resume with no Last-Event-ID, never to retry the same cursor.



## OpenAPI

````yaml /pt/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. On your own machine,
    one long-lived host serves this API over a Unix socket in your Narya home.
    The terminal client and the one-shot command that Lerian ships drive this
    API, and a client you write drives the same one.


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


    Submitting a message returns 202 with a turn id. Everything the turn
    produces streams over GET /v1/events as server-sent events with a typed
    envelope, and a stream resumes from a Last-Event-ID header.


    One error envelope: code, title and message. Codes are NRY- followed by four
    digits. Cursors are opaque.
servers: []
security:
  - bearerAuth: []
tags:
  - name: host
    description: The host process itself — version, uptime, mode, store.
  - name: sessions
    description: Durable conversation containers. Archive, never destroy.
  - name: messages
    description: Submitting work into a session and interrupting it.
  - name: events
    description: The server-sent event stream every client consumes.
  - name: lanes
    description: Parallel tracks inside a session — main, subagent, side.
  - name: agents
    description: Named recipes — instructions, tools, model, policy. Read-only in v1.
  - name: ladder
    description: >-
      What a person can type: the skills and command files in force for one
      repository, and expanding one into text. Five origins merged, nearest
      winning a name, the repository's own rungs gated on trust.
  - name: permissions
    description: Pending permission asks, decisions, and the decision audit.
  - name: intercom
    description: Sessions on one machine finding and messaging each other.
  - name: packages
    description: The one thing a user installs — resources, Go code, or both.
  - name: extensions
    description: >-
      Host-side extensions and the operations each exposes over the wire. This
      is the generic lane a host extension uses to serve its own client half (a
      TUI component, a web panel) or any API-only consumer, without adding
      routes to this contract.
  - name: workflows
    description: Deterministic multi-agent orchestration runs.
  - name: providers
    description: Model suppliers, their auth state, and the model catalogue.
  - name: monitors
    description: >-
      Long-running watchers a session keeps beside its conversation — a test
      runner in watch mode, a build, a log being followed. Started by the model
      or by the person, always listed, always killable.
  - name: records
    description: The queryable local record of everything that happened.
  - name: environments
    description: >-
      Where a session's code lives and its commands run — this machine, or a
      container narya operates. A session that names none runs here.
  - name: schedules
    description: >-
      Work the host's own clock starts with nobody present — a repository, a
      prompt, a rule and what one fire may spend. Cancel, never destroy.
  - name: sharing
    description: >-
      Publishing a session from a developer's own home to the organisation's,
      and what is held back before a byte leaves the machine.
  - name: refinements
    description: >-
      What this owner has taught narya and allowed it to keep — distilled facts,
      and the skills, agents and commands the model wrote for itself. Propose,
      read, consent, roll back. Nothing here fires until a person answers.
  - name: platform
    description: >-
      Calls Lerian's control plane made to a home it hosts, as this home
      recorded them.
paths:
  /v1/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. The data field of each frame is one JSON
        Event envelope, discriminated by its type property.

        A FINISHED TURN'S EVENTS ARE FORGOTTEN. About thirty seconds after a
        turn ends its events are deleted, and a start of the host deletes each
        finished turn's events older than thirty seconds, however recently that
        turn ended; the transcript is the record of the turn. A replay from a
        Last-Event-ID below anything this stream has forgotten begins with one
        replay-gap frame with no SSE id line, so it moves no cursor, followed by
        the events that remain. A client that receives it re-reads the
        transcript (listSessionEntries) from what it already holds and resumes
        the stream from that page's lastEventId.

        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: it replays the events that remain past it, opening with a
        replay-gap frame when the stream forgot some of them. 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. One this home holds no session
            by, or another member's product session, answers 404.
          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'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - bearerAuth: []
        - extensionSignature: []
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/DelegationsChangedEvent'
        - $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'
        - $ref: '#/components/schemas/ShareUpdatedEvent'
        - $ref: '#/components/schemas/RefinementProposedEvent'
        - $ref: '#/components/schemas/CompactionFailedEvent'
        - $ref: '#/components/schemas/ShareReceivedEvent'
        - $ref: '#/components/schemas/ReplayGapEvent'
        - $ref: '#/components/schemas/ModelSilenceEvent'
      discriminator:
        propertyName: type
        mapping:
          session-created: '#/components/schemas/SessionCreatedEvent'
          session-status-changed: '#/components/schemas/SessionStatusChangedEvent'
          session-posture-changed: '#/components/schemas/SessionPostureChangedEvent'
          model-switch: '#/components/schemas/ModelSwitchEvent'
          turn-started: '#/components/schemas/TurnStartedEvent'
          message-delta: '#/components/schemas/MessageDeltaEvent'
          message-start: '#/components/schemas/MessageStartEvent'
          message-end: '#/components/schemas/MessageEndEvent'
          compaction: '#/components/schemas/CompactionEvent'
          context-pruned: '#/components/schemas/ContextPrunedEvent'
          entry-appended: '#/components/schemas/EntryAppendedEvent'
          tool-call-started: '#/components/schemas/ToolCallStartedEvent'
          tool-call-update: '#/components/schemas/ToolCallUpdateEvent'
          tool-call-finished: '#/components/schemas/ToolCallFinishedEvent'
          tool-call-receipt: '#/components/schemas/ToolCallReceiptEvent'
          permission-asked: '#/components/schemas/PermissionAskedEvent'
          permission-decided: '#/components/schemas/PermissionDecidedEvent'
          lane-started: '#/components/schemas/LaneStartedEvent'
          lane-finished: '#/components/schemas/LaneFinishedEvent'
          delegations-changed: '#/components/schemas/DelegationsChangedEvent'
          turn-finished: '#/components/schemas/TurnFinishedEvent'
          intercom-message: '#/components/schemas/IntercomMessageEvent'
          workflow-progress: '#/components/schemas/WorkflowProgressEvent'
          ladder-changed: '#/components/schemas/LadderChangedEvent'
          checkpoint-created: '#/components/schemas/CheckpointCreatedEvent'
          extension: '#/components/schemas/ExtensionEvent'
          monitor-event: '#/components/schemas/MonitorEventEvent'
          monitor-list: '#/components/schemas/MonitorListEvent'
          queued-input-withdrawn: '#/components/schemas/QueuedInputWithdrawnEvent'
          schedule-fired: '#/components/schemas/ScheduleFiredEvent'
          task-list: '#/components/schemas/TaskListEvent'
          question-asked: '#/components/schemas/QuestionAskedEvent'
          session-moved: '#/components/schemas/SessionMovedEvent'
          compaction-started: '#/components/schemas/CompactionStartedEvent'
          share-updated: '#/components/schemas/ShareUpdatedEvent'
          refinement-proposed: '#/components/schemas/RefinementProposedEvent'
          compaction-failed: '#/components/schemas/CompactionFailedEvent'
          share-received: '#/components/schemas/ShareReceivedEvent'
          replay-gap: '#/components/schemas/ReplayGapEvent'
          model-silence: '#/components/schemas/ModelSilenceEvent'
    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/acme/ledger/internal/api\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
    DelegationsChangedEvent:
      description: Envelope for delegations-changed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: delegations-changed
            payload:
              $ref: '#/components/schemas/DelegationsChangedPayload'
      examples:
        - id: '1011'
          type: delegations-changed
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:31:00.000Z'
          payload:
            running: 0
    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: []
            waking: 0
    QueuedInputWithdrawnEvent:
      description: Envelope for queued-input-withdrawn.
      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
    ShareUpdatedEvent:
      description: Envelope for share-updated. Session-scoped.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: share-updated
            payload:
              $ref: '#/components/schemas/ShareUpdatedPayload'
      examples:
        - id: '1210'
          type: share-updated
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-09-08T09:20:00.000Z'
          payload:
            shareId: K7QX2ZM4YB6PWNRJ5TFHCDA3EV
            throughSeq: 42
    RefinementProposedEvent:
      description: Envelope for refinement-proposed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: refinement-proposed
            payload:
              $ref: '#/components/schemas/RefinementProposedPayload'
      examples:
        - id: '1402'
          type: refinement-proposed
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-09-07T02:14:00.000Z'
          payload:
            roundId: 2f7c1b93-4a5e-4d6f-8b0c-1e3a5c7d9f2b
            sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
            itemCount: 3
    CompactionFailedEvent:
      description: Envelope for compaction-failed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: compaction-failed
            payload:
              $ref: '#/components/schemas/CompactionFailedPayload'
      examples:
        - id: '1103'
          type: compaction-failed
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:01:57.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            reason: manual
    ShareReceivedEvent:
      description: >-
        Envelope for share-received: a delivery landed at THIS home and the copy
        of a shared session grew.

        IT BELONGS TO NO SESSION HERE, which is what separates it from
        share-updated. The conversation it names was published from another
        machine and no session of that id exists at this home, so the share id
        travels in the payload and the envelope carries no sessionId — making it
        a machine-wide event on the wire.

        GATED ANYWAY, AND BY THE SHARE. Having no session means the session gate
        cannot answer for it, so the host asks the share's own audience about
        every frame it writes, exactly as the read road does: a member no live
        share admits never receives it, on the machine-wide stream or on a
        narrowed one that asked for host events.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: share-received
            payload:
              $ref: '#/components/schemas/ShareReceivedPayload'
      examples:
        - id: '1211'
          type: share-received
          timestamp: '2026-09-08T09:21:00.000Z'
          payload:
            shareId: K7QX2ZM4YB6PWNRJ5TFHCDA3EV
            throughSeq: 42
    ReplayGapEvent:
      description: >-
        Envelope for replay-gap: some events this replay would have carried were
        forgotten, because their turn finished. It is written as a frame with no
        id, so it moves no Last-Event-ID; its envelope id is the cursor the
        replay started from, and its sessionId is the session the stream is
        narrowed to, absent on the machine-wide stream. A client whose last
        transcript read's lastEventId is below the payload's forgottenThrough
        re-reads the transcript from what it already holds and resumes the
        stream from that read's lastEventId; one whose read reached it already
        holds what was forgotten.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - payload
          properties:
            type:
              const: replay-gap
            payload:
              $ref: '#/components/schemas/ReplayGapPayload'
      examples:
        - id: '1211'
          type: replay-gap
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          timestamp: '2026-10-17T09:21:00.000Z'
          payload:
            forgottenThrough: '1290'
    ModelSilenceEvent:
      description: Envelope for model-silence.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - sessionId
            - payload
          properties:
            type:
              const: model-silence
            payload:
              $ref: '#/components/schemas/ModelSilencePayload'
      examples:
        - id: '1007'
          type: model-silence
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          laneId: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          timestamp: '2026-08-08T12:02:01.000Z'
          payload:
            turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
            silentMinutes: 1
            attempt: 1
            attempts: 3
    Error:
      type: object
      description: >-
        The single error envelope every operation returns. Codes are NRY-
        followed by four digits and are catalogued in the top-level
        x-error-catalog extension.
      required:
        - code
        - title
        - message
      properties:
        code:
          type: string
          pattern: ^NRY-[0-9]{4}$
          description: Machine-readable error code from the NRY catalogue.
        title:
          type: string
          maxLength: 256
          description: Short human-readable summary of the error class.
        message:
          type: string
          maxLength: 4096
          description: Specific, actionable description of what went wrong.
        fields:
          type: object
          description: Per-field validation problems.
          additionalProperties:
            type: string
      examples:
        - code: NRY-0002
          title: Session not found
          message: >-
            No session with id 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d exists on
            this host.
    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. `toolCallId` and channel `display` stay declared because older
        recorded events carry them and replay forever; 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
        notice:
          type: string
          enum:
            - turn-failed
          description: >-
            Present on the system note a failed turn leaves with its cause. The
            turn's turn-finished carries the same failure, so a window attached
            to the stream draws it from that event and does not read this entry;
            a transcript read shows it like any other note.
      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/acme/ledger/internal/api\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
          maxLength: 4096
        detail:
          type: string
          maxLength: 4096
          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
        decidedBySubject:
          type: string
          maxLength: 256
          description: >-
            WHICH person answered, when a person did. Any member may decide a
            permission request raised on a conversation that is not marked
            private, so the owner of a run released while they were away learns
            from this event that it happened AND who did it — without which the
            announcement says only that their own question is gone.


            Absent when no person decided (a rule, a hook, or an expiry) and on
            any host that did not record who. Never read as the owner.
      examples:
        - permissionId: 7e5d3c1b-9a8f-4e6d-b2c4-0f1a3b5c7d9e
          decision: deny
          decidedBy: user
          decidedBySubject: user_01HZX3Q8N4KDPYV2A7C9M5T6RB
    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
    DelegationsChangedPayload:
      type: object
      description: >-
        One of the session's delegations started or ended, and how many are
        still under way. A delegation is under way from the call that starts it
        until its report is delivered and the turn that report wakes has
        finished, so zero means nothing more will happen in the session until
        somebody sends it something. The count is the announcing host process's
        own: a delegation still under way when its shutdown begins is never
        counted out, and a host started after it counts from zero. The
        envelope's laneId is the delegate's lane. `narya -p` exits only once its
        own turn has finished and this reads zero.
      required:
        - running
      properties:
        running:
          type: integer
          minimum: 0
        unsettled:
          type: boolean
          description: >-
            True on a delegation's end whose report did not settle cleanly; the
            host's log says how. `narya -p` does not exit 0 past one.
      examples:
        - running: 1
    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. One bound is the turn's own spend ceiling — a scheduled
            fire's, today the only caller that sets one — reached by the first
            supplier round that took the turn's running cost to that number; a
            turn bounded that way may not delegate, because nothing sums another
            lane's spend into the turn that asked for it. The other is the
            person's daily spend ceiling, operator configuration unset by
            default: what narya metered for the person who acted — whoever
            started the turn, not the session's owner — this UTC day reached it,
            or includes a round nobody could price. A colleague's message that
            steers a turn already running joins it and its rounds stay the
            starter's, so a colleague whose day is spent can lengthen that turn
            until it ends. The daily ceiling can stop a turn before its first
            round, resets at 00:00 UTC and refuses no delegation. 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 past a turn's own ceiling is that one round, because a
            round's price is known only once it has ended, plus up to two
            unbilled dropped rounds the turn continued past, which sit outside
            the total. A round cut before the supplier reports its usage — an
            interrupt or a dropped stream — is billed by the supplier but never
            counted in the person's day: on a supplier that reports usage only
            at the end of a round that is any round cut mid-answer, on Anthropic
            only the cut round's output. Overshoot past the daily ceiling is
            therefore one round per lane of that person in flight when the day
            filled — other sessions, delegates, refinement, warms and scheduled
            fires each finish the round they had started — plus every round cut
            before its usage arrived.

            A budget turn reporting a costUsd spent a ceiling. A budget turn
            reporting none either ran no round at all, or stopped because no
            round of it could be priced — 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 narya knows no window for the model that ran (the
            catalogue states none and no refusal has taught one), 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. Sign in with `narya auth login <provider>`
              to use its default model, or set default_model in
              ~/.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. A question and its answer both carry the ask's
        askId.
      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: >-
            The ask a question opens or an answer resolves, the same id on both,
            so a watcher can pair them. Absent on a one-way message.
      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 —
        or, as `withdrawn`, left this reader's view.
      required:
        - runId
        - status
      properties:
        runId:
          type: string
          format: uuid
        sessionId:
          type: string
          format: uuid
          description: >-
            The run's conversation, absent once it is gone and on `withdrawn`.
            The stream delivers this event only to a caller who may read that
            conversation; a run whose conversation is gone reaches the holder of
            `sessions:read-private` alone.
        status:
          type: string
          enum:
            - pending
            - running
            - completed
            - failed
            - stopped
            - withdrawn
          description: >-
            The run's overall status after this update. `withdrawn` is not a
            status the run reaches: a stream that showed this run to a reader
            who may no longer read its conversation (it was made private
            mid-run) sends that reader this once, carrying `runId` alone, and
            nothing more about the run. A client takes the run off its live line
            and says nothing else.
        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
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          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, standing at the moment that timeline left off,
            which is what makes a rewind undoable — rewinding the code of the
            abandoned session at that entry puts the tree back byte for byte;
            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
        - waking
      properties:
        running:
          type: array
          description: Every monitor this session is running, by name, in start order.
          items:
            type: string
        waking:
          type: integer
          minimum: 0
          description: >-
            How many watchers were started with wake on and have not yet had
            their last words land, plus the turns they woke that have not ended:
            each can start a turn on this conversation. `narya -p` exits only
            once this reads zero.
      examples:
        - running:
            - tests
          waking: 1
    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 entry-appended: a message ARRIVING in the queue
        is announced to every attached window, and so is one LEAVING it, so a
        second window on the same session stops drawing it.

        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. On a product session the call finishes as it asks and its turn
        ends, so the prompt retires instead when answerQuestion answers it, when
        the person sends a message on its lane, or when listQuestions no longer
        lists it.
      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: >-
            The directory the session works in now: an absolute path on a local
            home, the path relative to its workspace on a hosted home.
            `previousRepository` is spelled the same way.
        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 one that does NOT commit, for any reason,
        announces `compaction-failed` for the same operation. So whatever state
        a client opens here is cleared on that `compaction`, on
        `compaction-failed`, or on `turn-finished` for the same turn, whichever
        comes first. 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
    ShareUpdatedPayload:
      type: object
      description: >-
        A share carried more of its session to the organisation's home, so
        whoever is watching re-reads the transcript.

        IT CARRIES NO CONVERSATION. The words are read back through the
        transcript; an event carrying them would be a second copy of a published
        conversation on a durable stream that replays forever.
      required:
        - shareId
        - throughSeq
      properties:
        shareId:
          type: string
          minLength: 26
          maxLength: 26
          pattern: ^[A-Z2-7]{26}$
          description: The share whose copy grew.
        throughSeq:
          type: integer
          format: int64
          minimum: 0
          description: >-
            The highest transcript position the organisation's home has
            confirmed it holds — the same number a recipient's own read of the
            transcript stops at, so a watcher can tell "there is more" from "I
            already have this" without a request.
    RefinementProposedPayload:
      type: object
      description: >-
        A round of proposed refinements is standing, waiting on a person.

        NOTHING IS BLOCKED ON IT, which is what separates it from a question:
        the loop that proposed it has already moved on, no tool call waits, no
        timeout closes it, and a restart leaves it alone. It exists so a client
        already attached draws the affordance without polling — a round proposed
        by last night's unattended fire is answered the next morning either way.

        There is no `refinement-answered` beside it. A round is answered through
        `answerRefinementRound`, by the client that was showing it.
      required:
        - roundId
        - itemCount
      properties:
        roundId:
          type: string
          format: uuid
          description: >-
            The round's id, which is what an answer is posted against (`POST
            /v1/refinements/rounds/{roundId}/answer`).
        sessionId:
          type: string
          format: uuid
          description: >-
            The conversation the round was learned from. Absent on a round
            proposed over the contract.
        itemCount:
          type: integer
          format: int32
          minimum: 1
          description: How many edits the round is asking about.
      examples:
        - roundId: 2f7c1b93-4a5e-4d6f-8b0c-1e3a5c7d9f2b
          sessionId: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          itemCount: 3
    CompactionFailedPayload:
      type: object
      description: >-
        A compaction ENDED without committing anything: the round its
        `compaction-started` announced is over, no summary entry was written,
        and the conversation is exactly what it was before.

        WHAT IT COVERS is every ending of every reason that is not a commit — a
        summarisation round the provider refused, a summary the store would not
        write, and a window with nothing eligible to summarise, which is a
        compaction that announces its start and then finds no material. A
        `manual` round runs BETWEEN turns and has no `turn-finished` to close
        it; a round inside a turn is often followed by one that answers
        uncompacted, and this ends the word before that answer arrives.

        It carries no cause. What went wrong reaches the person who asked, in
        the answer to their own call; every other window is being told one
        thing, which is that the round is over.

        Session-and-lane scoped, exactly like `compaction-started`.
      required:
        - turnId
        - reason
      properties:
        turnId:
          type: string
          format: uuid
          description: >-
            The compaction this ends, named by the same id its
            `compaction-started` carried. A `manual` compaction runs between
            turns, so this is the compaction's own operation id and never a
            turn's — one field pairs a start with its ending, exactly as it
            pairs a start with its commit.
        reason:
          type: string
          enum:
            - threshold
            - overflow
            - behavioural
            - manual
          description: >-
            Why the compaction had started, in `CompactionStartedPayload`'s own
            four words, so a consumer counting rounds by reason can settle this
            one without having held the start.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          reason: manual
    ShareReceivedPayload:
      type: object
      description: >-
        The copy of a shared session at this home reaches a further position, so
        whoever may read it re-reads the entries.

        IT CARRIES NO CONVERSATION, for ShareUpdatedPayload's reason: the words
        come back through the share's entries, which is the one road that
        applies the audience rule and writes the disclosure record. An event
        carrying them would be a second copy of a published conversation on a
        durable stream that replays forever, recorded in nobody's audit.
      required:
        - shareId
        - throughSeq
      properties:
        shareId:
          type: string
          minLength: 26
          maxLength: 26
          pattern: ^[A-Z2-7]{26}$
          description: >-
            The share whose copy grew — what a reader asks this home about,
            there being no session here to ask about.
        throughSeq:
          type: integer
          format: int64
          minimum: 0
          description: >-
            How far into the conversation this home now reaches, in the
            publishing home's own numbering. A batch offering entries this home
            already held announces the same position again, which a watcher
            reads as "nothing new" rather than as a second copy.
    ReplayGapPayload:
      type: object
      description: >-
        How far the stream forgot. A position in the event sequence, as public
        as the cursor; it names no session, turn, type or count.
      required:
        - forgottenThrough
      properties:
        forgottenThrough:
          type: string
          description: The highest event id the stream has forgotten.
      additionalProperties: false
    ModelSilencePayload:
      type: object
      description: >-
        The model of a running round has produced no piece of the answer (text,
        visible thinking, or a tool call's arguments) for silentMinutes whole
        minutes, counted from the moment the round's request was sent or from
        its last piece. It repeats once per minute of continued silence, so
        silence is never mute; no bound changes with it (the round still ends
        where it would).

        A notice with silentMinutes 0 closes the silence the earlier ones
        reported: a piece of the answer arrived, or the round ended. A round
        that was never silent for a minute emits none at all.

        Session-and-lane scoped, and a turn's own event, forgotten with it.
      required:
        - turnId
        - silentMinutes
        - attempt
        - attempts
      properties:
        turnId:
          type: string
          format: uuid
        silentMinutes:
          type: integer
          minimum: 0
          description: >-
            Whole minutes this round has gone without a piece of the answer; 0
            ends the silence.
        attempt:
          type: integer
          minimum: 1
          description: >-
            Which try of its round chain this round is: a stream that dropped or
            was cut is sent again, and the retry is attempt 2.
        attempts:
          type: integer
          minimum: 1
          description: >-
            The most tries the chain may take: 3, with or without a spend
            ceiling.
      examples:
        - turnId: 4d8e2f6a-0b1c-4d3e-9f5a-7b9c1d3e5f0a
          silentMinutes: 2
          attempt: 2
          attempts: 3
    Session:
      type: object
      description: >-
        A durable conversation container. Survives the process that created it;
        archiving hides it from active lists but its transcript remains
        queryable and exportable forever.
      required:
        - id
        - repository
        - status
        - clientKind
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: The session's id.
        title:
          type: string
          maxLength: 512
          description: Human-readable title.
        repository:
          type: string
          maxLength: 4096
          description: >-
            Absolute path of the directory the session works in; on a hosted
            home, the path relative to its workspace, which is all a client
            there is shown.

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

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

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

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

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

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

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

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

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

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


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


            ABSENT WHEN THERE IS NOTHING TO SAY, and absent rather than
            null-shaped: a conversation nobody is being asked about carries no
            field, not an empty object. It is also absent when the caller may
            not see the prompt itself — on a conversation somebody marked
            private, a reader who is not its owner and does not hold the
            read-private permission sees no attention even though the request
            behind it exists, because what this field may say is bounded by the
            same door the ask is behind.
      examples:
        - id: 6b9f6d2e-1c3a-4f5b-9d7e-2a8c4e6f0b1d
          title: Fix flaky ledger test
          repository: /Users/dev/repos/midaz
          status: running
          provider: anthropic
          model: claude-sonnet-4-5
          clientKind: inline
          owner: user_01HZX3Q8N4KDPYV2A7C9M5T6RB
          usage:
            inputTokens: 152000
            outputTokens: 8400
            costUsd: 1.87
          createdAt: '2026-08-08T12:00:00.000Z'
          updatedAt: '2026-08-08T12:34:56.000Z'
    Posture:
      type: string
      enum:
        - attended
        - detached
      x-enum-varnames:
        - PostureAttended
        - PostureDetached
      description: >-
        Whether somebody is there to answer a permission prompt raised in a
        session. attended — a prompt is shown and waited on. detached — nobody
        is watching, so a prompt is refused and recorded with its reason rather
        than blocking the turn forever on a question nothing can answer. A
        session that recorded neither is attended when a terminal client
        (clientKind inline or tui) opened it, or it is a product session, and
        detached otherwise.

        Orthogonal to clientKind, and deliberately not a value of it: clientKind
        is stamped once at creation and never rewritten, whereas a session
        BECOMES detached when somebody says so. It is a recorded fact with
        exactly one transition (setSessionPosture) and is never inferred from
        the presence or absence of a connection — a client that crashed, a lid
        that closed and a pipe that dropped all leave it exactly as it was.
    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. It also answers who attends a
        session that recorded no posture: inline and tui have a person behind
        them, so a permission prompt is asked; oneshot, api and unknown have
        nobody, so a prompt is refused with its reason. In a repository nobody
        has trusted, an api or unknown session's turn is refused naming the
        trust gesture; a oneshot session runs there under the operator's rules
        without the repository's own files, whoever created it.
    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
    SessionEnvironment:
      type: object
      description: >-
        A session's binding to an environment, as a client draws it beside the
        session's identity. The id and the state, and deliberately nothing else:
        everything a person acts on is here, which keeps a page of twenty-five
        sessions from carrying twenty-five setup transcripts.
      required:
        - id
        - state
      properties:
        id:
          $ref: '#/components/schemas/EnvironmentId'
        state:
          $ref: '#/components/schemas/EnvironmentState'
    Visibility:
      type: string
      enum:
        - organisation
        - private
      x-enum-varnames:
        - VisibilityOrganisation
        - VisibilityPrivate
      description: >-
        Who inside this organisation may read a session. organisation — every
        member may, which is the default and what every session is until
        somebody marks it. private — its owner, plus a member holding
        `sessions:read-private`, plus whoever a share of it names.

        Two values and no third. There is no "unset": the absence of a mark IS
        the organisation's, so "nobody said" and "the team may read it" are one
        fact rather than two, and nothing in this product has a use for a
        visibility somebody would have to define.
    SessionAttention:
      type: object
      description: One prompt standing on a conversation, waiting for a person.
      required:
        - kind
        - askId
        - since
      properties:
        kind:
          type: string
          enum:
            - permission
            - question
          description: >-
            Which of the two things a person answers is holding this
            conversation: a permission request for an action the agent wants to
            take, or a question the agent put to the user.


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


            Where several prompts stand on one conversation this is the OLDEST,
            for the same reason: the question an overview of sessions asks is
            how long somebody has been waiting, not when the most recent thing
            appeared.
    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`.
    EnvironmentId:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9][A-Za-z0-9._-]*$
      description: >-
        An environment's identifier, chosen by whoever creates it and unique on
        this host. It travels as one path segment, so its shape is bounded to
        what a path segment carries unchanged, exactly as a monitor's name is: a
        "/" would address no route and a "?" or "#" would end the segment early
        and address something else. One beginning `conversation-` is a hosted
        home's own, and a create naming it is 422.
    EnvironmentState:
      type: string
      enum:
        - ready
        - degraded
        - gone
        - unusable
      x-enum-varnames:
        - EnvironmentStateReady
        - EnvironmentStateDegraded
        - EnvironmentStateGone
        - EnvironmentStateUnusable
      description: >-
        Whether an environment is answering, and — when it is not — whether
        waiting is the right move. The distinction is the whole point: a session
        waiting on an unreachable environment looks identical to a slow one
        otherwise, and what the person does next depends entirely on which it
        is. ready — answering; work proceeds. degraded — not answering right now
        and expected back: a restart or a dropped connection, which the host
        retries so the next call may simply succeed, or a container stopped
        outside narya, which the next turn starts. gone — destroyed, its name
        taken by another environment since, or the runtime no longer has it. A
        hosted home also removes a container nothing has used for 15 minutes and
        keeps its disks, the workspace and its restore points, and so does a
        person's own home for a conversation's own `conversation-<sessionId>`;
        it reads gone until a turn needs it. A session bound to it is then given
        a new container on its next turn, over the disks the old one left when
        those are still there, and is told which; on a person's own home a named
        environment that is gone needs a new one. unusable — its setup did not
        succeed; `setup` says what happened.
  responses:
    BadRequest:
      description: Malformed request — invalid parameter, cursor, or JSON body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: >-
        Authentication required — a request carrying no valid identity token
        (NRY-0011).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        The caller is authenticated and is not allowed this operation (NRY-0028
        or NRY-0030).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: The addressed resource does not exist on this host.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    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'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Enforced on every transport, with no exempt operation. A person's
        request must carry a JWT issued by the configured identity provider,
        which the host verifies itself against that issuer's key set: signature,
        issuer, expiry, and the person and organisation it names. Requests
        without a valid one receive 401 NRY-0011 — except a request with no
        token on a laptop's socket from the OS user the kernel names as the
        host's own, which is that machine's user.
    extensionSignature:
      type: apiKey
      in: header
      name: Narya-Signature
      description: >-
        THE SECOND KIND OF CALLER, and the only operation it reaches is
        streamEvents. A registered extension holds no person's token: it signs
        its request under the secret this home minted for it when the owner
        registered it.

        FOUR HEADERS TRAVEL TOGETHER and this scheme names only the first,
        because OpenAPI's apiKey has room for one. `Narya-Signature` carries the
        algorithm name, a comma, and the MAC in lower-case hex;
        `Narya-Timestamp` the seconds since the epoch the call was signed at;
        `Narya-Request-Id` a value unique per call, which is the nonce a
        receiver refuses a second time; and `Narya-Extension` the publisher/name
        of the registration, which says which secret to verify under and is
        deliberately outside the signed bytes.

        WHAT IS SIGNED is HMAC-SHA256 over five parts, each terminated by a
        newline, in this order: the method upper-cased, the path including its
        query string, the timestamp, the request id, and the lower-case hex
        SHA-256 of the body (of the empty string when there is none). Nothing
        else.

        HOW IT IS REFUSED. A signed call that names any operation other than
        streamEvents is 403 NRY-0028. A signed call to streamEvents that this
        host cannot resolve to a registered, signature-verified extension is 401
        NRY-0011, with one sentence for every cause so a refusal cannot be used
        to ask whether a registration exists. A verified call whose registration
        is disabled is 403 NRY-0028. The acceptance window is sixty seconds, and
        a request id already accepted inside it is refused as a replay.

````

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