$schema: https://json-schema.org/draft/2020-12/schema
schemaVersion: 1
title: UatuCode streaming protocol contract
channels:
  live:
    transport: sse
    operationId: hubStreamLive
    controlOperationId: hubUpdateLiveSubscriptions
    path: /api/hub/live
    mediaType: text/event-stream
    authentication: hubSession
    events:
      - { name: hello, dataSchema: LiveHello, replayId: none }
      - { name: live, dataSchema: LiveEnvelope, replayId: none }
    topics:
      document:
        domain: workspace
        key: LiveDocumentKey
        dataSchema: DocumentUpdate
        cursor: hub
        description: A joining or behind subscriber receives a workspace snapshot; caught-up subscribers receive ordered file/repository patches. Apply patches only to their matching epoch and previousRevision; otherwise resubscribe for a snapshot. Discovery does not imply a live edit.
      inventory:
        domain: workspace
        dataSchema: ConversationInventoryEvent
        cursor: hub
        description: >-
          The workspace's conversation inventory changed. Invalidations may
          collapse into one event carrying the latest payload, including its
          optional agent catalog revision map. Shared and lingering upstreams
          retain that payload for joining or reconnecting pages. Read the
          inventory after `ready` and again on every data event; refresh an
          agent's catalog when its revision changes.
      conversation:
        domain: workspace
        key: LiveConversationKey
        dataSchema: ChatEvent
        resyncDataSchema: ChatResyncEvent
        cursor: workspace
        description: >-
          One conversation's ordered events. Resuming from a retained cursor
          replays later events in sequence before live ones, and replay is
          byte-bounded per conversation. `conversation.configuration`
          publishes a newly effective model, mode, or variant after prompt
          acceptance or an agent-side change. `conversation.updated` publishes
          summary changes such as a persisted rename. `conversation.queue`
          restates the workspace-held message queue after a message is held,
          removed, or delivered, so every client shows the same queue at the
          same point in the stream. All three are ordered, replayable,
          idempotent projections of conversation state. A resync never
          arrives as a data event. It arrives as a `resync` signal carrying
          the workspace's resync event.
      activity:
        domain: hub
        dataSchema: WorkspaceActivity
        cursor: hub
        subscription: activity query parameter
        description: >-
          One summary per workspace the caller may access when the stream
          opens, then one whenever a workspace's facts change. The envelope's
          `ws` names the workspace described, which need not be the stream's
          own. A workspace whose child does not answer reports not running.
          The summary carries no content, titles, paths, or per-conversation
          detail.
      worktrees:
        domain: hub
        dataSchema: WorktreeInventoryEvent
        cursor: none
        description: >-
          The worktree inventory of the stream's workspace's repository may
          have changed: a Uatu worktree operation committed, or a Hub
          reconciliation observed a changed inventory. Read the inventory
          again on every data event. There is no replay cursor. Every
          subscription and every reconnect receives one fresh invalidation,
          so changes missed while disconnected are recovered by the read that
          follows. The event carries no paths, branches, identities,
          credentials, or conversation content, and never changes the active
          workspace or its selections. The topic does not depend on the
          workspace's session running.
    signals:
      ready: >-
        The subscription is attached to a live upstream, and the Hub has
        written any replay owed to it.
      resync: >-
        The presented cursor is not replayable, or the workspace said so. The
        subscription stays detached until the client takes a fresh snapshot
        and adds it again with the snapshot's cursor. It says nothing about
        other subscriptions. For the conversation topic, `data` carries the
        workspace's resync event.
      unavailable: >-
        The upstream failed or the workspace child exited. The Hub retries
        while subscribers remain, and on recovery sends `ready` plus fresh
        data where the topic needs it.
    bounds: { maxSubscriptions: 16, maxKeyUtf8Bytes: 4096, maxCursorUtf8Bytes: 1024, maxWorkspaceIdUtf8Bytes: 256, keepaliveSeconds: 15 }
    lifecycle:
      initialEvent: hello
      open: >-
        The response writes an `: open` comment frame at once, so headers and
        first bytes arrive before any event exists. Exactly one `hello` event
        follows, carrying the stream id.
      subscriptions: >-
        The `subs` query parameter sets the initial subscriptions. Later
        changes go to hubUpdateLiveSubscriptions with the stream id from
        `hello`, sent with the same Hub session. Subscribing never opens
        another connection. `subs` rides the query string, and proxies
        commonly refuse a request line past about 8 KB, so a client whose set
        would exceed that presents part of it in `subs` and adds the rest
        after `hello`, from the same cursors.
      cursors: >-
        Cursors are opaque and scoped to one subscription. A client advances
        a subscription's cursor only on `data` events. Signals carry the
        current cursor unchanged, possibly empty. Subscriptions resume and
        resync independently.
      replay: >-
        To reconnect, open a new stream that presents in `subs` the last
        cursor applied for every subscription it resumes. The Hub replays each
        subscription from its cursor in order before live events, or sends it
        a `resync` signal when the cursor is not replayable. The SSE `id`
        field and `Last-Event-ID` are not used.
      keepalive: >-
        A `: keepalive` comment frame every 15 seconds while idle. It carries
        no envelope and advances no cursor.
      reconnection: >-
        After a transport error the client reconnects the one stream with
        bounded backoff, presenting every retained cursor.
      cancellation: >-
        Client disconnect or stream cancellation releases the stream's
        subscriptions. The Hub releases an upstream within a bounded period
        after its last subscriber leaves.
      completion: >-
        Open until client cancellation, Hub shutdown, or transport failure.
        Upstream failures and workspace exits arrive as signals and never end
        the stream.
      errors: HTTP 400, 401, 403, and 404 occur before the stream starts. There is no in-band error event.
  cloneJobEvents:
    transport: sse
    operationId: hubStreamCloneJobEvents
    path: /api/hub/clone-jobs/{jobId}/events
    mediaType: text/event-stream
    authentication: hubSession
    events:
      - { name: output, dataSchema: CloneOutput, replayId: integer }
      - { name: phase, dataSchema: ClonePhase, replayId: integer }
      - { name: result, dataSchema: CloneResult, replayId: integer, terminal: true }
    lifecycle:
      replay: Send Last-Event-ID; retained events whose integer id is greater are replayed in order.
      replayLimit: Output retention is byte-bounded; phase and result events remain replayable while the job is retained.
      reconnection: A result event closes the stream. Reconnecting within the job retention window replays retained events — including the terminal result — and then closes again.
      errors: Unknown or non-owned jobs return HTTP 404 before stream start; no in-band error event exists.
schemas:
  DocumentUpdate:
    description: Snapshot or incremental document patch, scoped to the subscription context.
    $ref: ./openapi.yaml#/components/schemas/DocumentUpdate
  WorkspaceState:
    description: Same schema as openapi.yaml#/components/schemas/WorkspaceState.
    $ref: ./openapi.yaml#/components/schemas/WorkspaceState
  ChatEvent: { $ref: ./openapi.yaml#/components/schemas/ChatEvent }
  ChatResyncEvent: { $ref: ./openapi.yaml#/components/schemas/ChatResyncEvent }
  ConversationInventoryEvent: { $ref: ./openapi.yaml#/components/schemas/ConversationInventoryEvent }
  LiveHello:
    type: object
    required: [streamId]
    properties:
      streamId: { type: string, minLength: 1, description: Unguessable id that binds subscription changes to this stream. }
    additionalProperties: false
  LiveEnvelope:
    type: object
    required: [ws, topic, cursor, event]
    properties:
      ws: { type: string, minLength: 1, description: "The workspace the event belongs to. For the activity topic, the workspace the summary describes, which may be any workspace the caller may access." }
      topic: { enum: [document, inventory, conversation, activity, worktrees] }
      key: { type: string, description: "The subscription key. The watch context for document and the conversation id for conversation. Absent for inventory, activity, and worktrees." }
      cursor: { type: string, description: "The subscription's cursor. It advances only on data events." }
      event: { $ref: '#/schemas/LiveEvent' }
    additionalProperties: false
  LiveEvent:
    oneOf:
      - { type: object, required: [kind, data], properties: { kind: { const: data }, data: { description: "The topic's payload. Its schema is the topic's dataSchema in channels.live.topics." } }, additionalProperties: false }
      - { type: object, required: [kind], properties: { kind: { const: ready } }, additionalProperties: false }
      - { type: object, required: [kind], properties: { kind: { const: resync }, data: { description: "Present for the conversation topic when the workspace sent a resync event. Its schema is topics.conversation.resyncDataSchema." } }, additionalProperties: false }
      - { type: object, required: [kind], properties: { kind: { const: unavailable } }, additionalProperties: false }
  WorktreeInventoryEvent:
    type: object
    required: [type]
    properties:
      type: { const: worktree.inventory }
    additionalProperties: false
    description: A content-free invalidation of the worktree inventory. Read the inventory again.
  WorkspaceActivity:
    type: object
    required: [running, working, awaiting, finished]
    properties:
      running: { type: boolean, description: The workspace's session is running and its child answers. }
      working: { type: boolean, description: An agent conversation is working, whether a turn is in flight or the agent still holds live background work. Always false when not running. }
      awaiting: { type: boolean, description: A permission request or question awaits the user. Always false when not running. }
      finished: { type: boolean, description: "Held per caller: the Hub observed the workspace working, it has since gone quiet (not working, nothing awaiting), and the caller has not acknowledged viewing it since (POST /s/{workspaceId}/api/activity-viewed). Cleared by that acknowledgement, by the session stopping, or by work starting again. A workspace first seen quiet is not finished. Always false when not running." }
    additionalProperties: false
  CloneOutput:
    type: object
    required: [output]
    properties: { output: { type: string } }
    additionalProperties: false
  ClonePhase:
    type: object
    required: [phase]
    properties: { phase: { enum: [cloning, registering, starting] } }
    additionalProperties: false
  CloneResult:
    oneOf:
      - { type: object, required: [status, workspaceId, target, running], properties: { status: { const: succeeded }, workspaceId: { type: string }, target: { type: string }, running: { type: boolean, description: "false = the registered workspace finished stopped (the default); true = an explicitly requested start succeeded." } }, additionalProperties: false }
      - { type: object, required: [status, target, error], properties: { status: { enum: [clone-failed, register-failed, start-failed, cleanup-failed] }, target: { type: string }, error: { type: string }, workspaceId: { type: string, description: "Present on start-failed when the configuration committed and the stopped workspace was preserved." } }, additionalProperties: false }
      - { type: object, required: [status, target], properties: { status: { enum: [cancelled, timed-out] }, target: { type: string }, reason: { enum: [inactivity, lifetime] } }, additionalProperties: false }
