Skip to content

Message streaming over WebSocket for large payloads #170

@vrknetha

Description

@vrknetha

Problem

Currently, messages are delivered as single frames over the WebSocket connection. This works for small payloads but breaks down with:

  • Large file transfers (documents, images, model weights)
  • Embedding vectors
  • Batch data payloads
  • Any payload approaching the frame size limit (relayMaxFrameBytes)

Messages that exceed the frame limit are rejected. There's no way to send large data between agents.

Proposal

Implement chunked streaming over the existing WebSocket connection. No new infrastructure needed — the WebSocket is already there, bidirectional, and persistent.

Frame Protocol

Add a streaming frame type alongside existing deliver and heartbeat:

stream_start  → { streamId, requestId, totalBytes, totalChunks, metadata }
stream_chunk  → { streamId, chunkIndex, data (base64 or binary), checksum }
stream_end    → { streamId, finalChecksum }
stream_abort  → { streamId, reason }
stream_ack    → { streamId, chunkIndex }  (flow control)

Flow

Sender's connector → Sender's proxy DO → WebSocket → Recipient's connector

1. Sender sends stream_start (metadata: size, type, sender DID)
2. Proxy DO stores stream metadata, forwards to recipient's WebSocket
3. Sender sends stream_chunk (N chunks, flow-controlled by stream_ack)
4. Recipient's connector reassembles chunks into the inbox
5. Sender sends stream_end with final checksum
6. Recipient verifies checksum → enqueues complete message for replay to OpenClaw
7. Receipt sent back on completion

Flow Control

  • Recipient sends stream_ack per chunk (or per N chunks as a window)
  • Sender pauses if no ack received within timeout
  • Prevents fast sender from overwhelming slow recipient
  • DO can enforce per-stream memory limits

Failure Handling

  • stream_abort from either side cancels the stream
  • If WebSocket drops mid-stream: recipient has partial chunks in temp storage
  • On reconnect: sender can resume from last ack'd chunk (if stream hasn't expired)
  • Stream TTL: incomplete streams are cleaned up after configurable timeout
  • Checksum mismatch on stream_end: recipient sends stream_abort, sender can retry

DO Considerations

  • DO doesn't need to buffer the entire payload — it forwards chunks as they arrive
  • If recipient is offline: chunks queue in DO storage (same pattern as regular messages)
  • Per-stream memory limit on DO to prevent abuse
  • Concurrent stream limit per agent pair

Why Not HTTP Multipart?

The WebSocket is already established, authenticated, and bidirectional. HTTP would mean:

  • New auth flow per upload
  • No flow control (stream_ack)
  • Can't resume on disconnect
  • Separate infrastructure from the message path

Backward Compatibility

  • Old connectors that don't understand stream_* frames ignore them (existing frame parsing skips unknown types)
  • Protocol version negotiation (#future) would handle this cleanly
  • Fallback: reject large messages with clear error suggesting connector upgrade

Implementation Phases

  1. Phase 1: Basic chunking — split large messages into chunks, reassemble on receipt. No flow control, no resume.
  2. Phase 2: Flow control — stream_ack window, backpressure
  3. Phase 3: Resume on disconnect — checkpoint from last ack'd chunk

Related

  • Frame size limit: relayMaxFrameBytes in proxy config
  • DO queue: RELAY_QUEUE_MAX_MESSAGES_PER_AGENT — streaming chunks should count as one logical message, not N

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions