DorkOS
Guides

Relay Messaging

Enable Relay and send messages between agents, humans, and external platforms

Relay Messaging

Relay is DorkOS's built-in messaging layer. It routes messages between agents, humans, and external platforms like Telegram using a subject-based publish/subscribe model. Every message is persisted to disk, indexed in SQLite, and tracked end-to-end.

This guide covers enabling Relay, sending your first message, configuring adapters, and integrating with Tasks.

What Relay Does

Agents publish messages to named subjects. Relay handles routing those messages to the right recipients. Agents don't need to know about each other — they only need to agree on subject naming.

Messages flow through subject validation, access control, rate limiting, budget enforcement, and per-endpoint circuit breakers. Failed deliveries land in a dead letter queue. The full delivery path is traced so you can see exactly what happened to any message.

When Relay is enabled, it takes over the standard session messaging path. Instead of POST /api/sessions/:id/messages calling the Agent SDK directly, the message is published to relay.agent.{sessionId} and the Claude Code adapter picks it up. Every session message benefits from Relay's tracing, budget enforcement, and reliability.

Enabling Relay

Relay is enabled by default on fresh installs. When DorkOS starts, you should see:

DorkOS server listening on port 4242
Relay message bus enabled

Relay creates its data directory at ~/.dork/relay/ on first start.

To verify, check the config endpoint:

curl http://localhost:4242/api/config

The response includes relay.enabled: true when the subsystem is active.

To disable Relay, set DORKOS_RELAY_ENABLED=false in your .env file. All Relay routes will return 404 and session messaging will use the direct Agent SDK call. The Maildir files and SQLite index persist between restarts — you can toggle Relay without losing data.

Sending Messages

Using the REST API

Publish a message with a POST to the messages endpoint:

curl -X POST http://localhost:4242/api/relay/messages \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "relay.agent.backend",
    "payload": { "task": "Run the test suite" },
    "from": "relay.human.console.my-client"
  }'

The response includes the message ID and delivery results:

{
  "id": "01HX...",
  "delivered": 1,
  "deadLettered": 0
}

Using MCP Tools

Agents running inside DorkOS have built-in MCP tools for Relay operations — no HTTP calls required:

relay_send                  Send a message to a Relay subject
relay_inbox                 Read inbox messages for an endpoint
relay_list_endpoints        List all registered endpoints
relay_register_endpoint     Register a new endpoint
relay_unregister_endpoint   Unregister an endpoint (use to clean up dispatch inboxes)
relay_send_and_wait                 Blocking request/reply — sends a message and waits for the response
relay_send_async              Fire-and-poll — dispatches a message, returns immediately with an inbox to poll
relay_get_trace             Get the full delivery trace for a message
relay_get_metrics           Get aggregate delivery metrics
relay_list_adapters         List all adapters with status
relay_enable_adapter        Enable a disabled adapter
relay_disable_adapter       Disable a running adapter
relay_reload_adapters       Hot-reload adapter configuration from disk

relay_send_and_wait (added in v0.8.0) handles request/reply in a single call. It registers an ephemeral inbox, publishes the message with that inbox as replyTo, and blocks until the agent replies. Use this for synchronous patterns where you need the response before proceeding.

relay_send_async returns immediately with a dispatch inbox subject. The agent runs asynchronously; poll relay_inbox for progress events and the final result. Use this for long-running tasks where blocking is not appropriate. Call relay_unregister_endpoint to clean up when you receive done: true.

Subject Naming

Subjects use a hierarchical dot-separated format: relay.{audience}.{identifier}.

Subject PatternPurpose
relay.agent.{agentId}Messages addressed to a specific agent
relay.human.console.{clientId}Messages destined for a human's browser console
relay.human.telegram.{chatId}Messages routed to a Telegram chat
relay.system.tasks.{scheduleId}System messages from the Task scheduler

Wildcards work for subscriptions: * matches exactly one segment, > matches one or more trailing segments. Subscribing to relay.agent.> receives messages addressed to any agent.

Budget Constraints

Every message carries a budget that prevents runaway chains:

curl -X POST http://localhost:4242/api/relay/messages \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "relay.agent.backend",
    "payload": { "task": "Refactor the auth module" },
    "from": "relay.human.console.my-client",
    "budget": {
      "maxHops": 3,
      "ttlMs": 300000,
      "callBudgetRemaining": 50
    }
  }'

If you omit budget fields, Relay applies defaults: 5 maximum hops, 1-hour TTL, no call budget limit. For automated agent-to-agent workflows, set explicit budgets to prevent unexpected resource consumption.

Built-in Adapters

Adapters bridge external platforms into the Relay subject hierarchy. DorkOS ships with five built-in adapters, configured through ~/.dork/relay/adapters.json.

Claude Code Adapter

The Claude Code adapter is enabled by default when Relay starts. It subscribes to relay.agent.* subjects and routes incoming messages to Claude Agent SDK sessions. Response chunks flow back through Relay to the sender's replyTo subject.

{
  "adapters": [
    {
      "id": "claude-code",
      "type": "claude-code",
      "builtin": true,
      "enabled": true,
      "config": {
        "maxConcurrent": 3,
        "defaultTimeoutMs": 300000
      }
    }
  ]
}

maxConcurrent limits parallel agent sessions. defaultTimeoutMs sets the maximum time a session can run before termination.

Telegram Adapter

The Telegram adapter connects a Telegram bot to the Relay bus using grammy. Inbound messages from Telegram are published to relay.human.telegram.{chatId}, and outbound messages on matching subjects are delivered back to the chat. Supports real-time streaming via Telegram's sendMessageDraft API.

{
  "id": "telegram",
  "type": "telegram",
  "builtin": true,
  "enabled": true,
  "config": {
    "token": "123456:ABC-DEF..."
  }
}

Telegram Chat SDK Adapter

An experimental alternative Telegram adapter backed by the Chat SDK. Uses the same BotFather token but integrates via Chat SDK's adapter abstraction instead of grammy directly. Streaming uses post+edit at ~500ms intervals (vs the native adapter's 200ms sendMessageDraft).

{
  "id": "telegram-chatsdk",
  "type": "telegram-chatsdk",
  "builtin": true,
  "enabled": true,
  "config": {
    "token": "123456:ABC-DEF...",
    "mode": "polling"
  }
}

Slack Adapter

The Slack adapter bridges Slack workspaces into Relay using Socket Mode — no public URL required. Supports streaming responses, threading, typing indicators via emoji reactions, and fine-grained access control.

{
  "id": "slack",
  "type": "slack",
  "builtin": true,
  "enabled": true,
  "config": {
    "botToken": "xoxb-...",
    "appToken": "xapp-...",
    "signingSecret": "abc123...",
    "streaming": true,
    "typingIndicator": "reaction",
    "respondMode": "thread-aware",
    "dmPolicy": "open"
  }
}

respondMode controls when the bot responds in channels: thread-aware (default -- responds to @mentions and thread replies where the bot is already participating), mention-only (only @mentions), or always (every message). dmPolicy controls DM access: open (default) or allowlist (restrict to user IDs in dmAllowlist). Per-channel overrides are available via channelOverrides.

Webhook Adapter

The webhook adapter provides a generic HTTP bridge with HMAC-SHA256 signature verification — both inbound (external services posting to DorkOS) and outbound (Relay messages forwarded as HTTP requests).

{
  "id": "my-webhook",
  "type": "webhook",
  "builtin": true,
  "enabled": true,
  "config": {
    "url": "https://example.com/hook",
    "secret": "your-hmac-secret",
    "subjectPrefix": "relay.webhook.incoming"
  }
}

Inbound webhooks are received at POST /api/relay/webhooks/{adapterId}. The adapter verifies the HMAC signature, extracts the payload, and publishes it to the configured subject prefix. Secret rotation is supported with a 24-hour transition window.

Hot Reload

Adapter configuration is watched via chokidar. Edit ~/.dork/relay/adapters.json and DorkOS automatically reconciles the running adapters — stopping removed or disabled adapters and starting newly enabled ones. No server restart required.

Trigger a reload explicitly:

curl -X POST http://localhost:4242/api/relay/adapters/reload

Message Tracing

Every message published through Relay gets a trace that tracks delivery end-to-end. Retrieve the trace for a specific message:

curl http://localhost:4242/api/relay/messages/{messageId}/trace
{
  "traceId": "01HXABC123",
  "spans": [
    {
      "id": "01HXDEF456",
      "messageId": "01HXABC123",
      "traceId": "01HXABC123",
      "subject": "relay.agent.backend",
      "status": "delivered",
      "sentAt": "2025-02-26T12:00:00.000Z",
      "deliveredAt": "2025-02-26T12:00:00.050Z",
      "processedAt": "2025-02-26T12:00:00.200Z",
      "errorMessage": null,
      "metadata": null
    }
  ]
}

Span statuses: sent (published, delivery in progress), delivered (processed successfully), failed (handler errored), timeout (rejected by budget, access control, or TTL expiry).

For deeper observability, see the Relay Observability guide.

Relay and Tasks Integration

When both Relay and Tasks are enabled, scheduled jobs are dispatched through Relay. Tasks publishes to relay.system.tasks.{scheduleId}, and the Claude Code adapter starts the agent session.

Scheduled runs then get Relay's full delivery pipeline — budget enforcement, tracing, dead letter handling, and access control.

export DORKOS_RELAY_ENABLED=true
export DORKOS_TASKS_ENABLED=true
dorkos

Subscribe to relay.system.tasks.* via the SSE stream to observe all Tasks dispatch events.

Configuration Reference

Prop

Type

Data Directory

PathPurpose
~/.dork/relay/index.dbSQLite index database (messages, traces; WAL mode)
~/.dork/relay/endpoints/Maildir message store, one directory per endpoint
~/.dork/relay/adapters.jsonAdapter configuration (hot-reloaded)
~/.dork/relay/config.jsonReliability settings (rate limits, circuit breakers, backpressure)
~/.dork/relay/access-rules.jsonAccess control rules (hot-reloaded)

Reliability Settings

The config.json file controls rate limiting, circuit breakers, and backpressure. Changes are hot-reloaded.

Prop

Type

REST API Endpoints

MethodPathDescription
POST/api/relay/messagesPublish a message to a subject
GET/api/relay/messagesList messages with filters and cursor pagination
GET/api/relay/messages/:idGet a single message by ID
GET/api/relay/messages/:id/traceGet the full delivery trace for a message
GET/api/relay/endpointsList registered endpoints
POST/api/relay/endpointsRegister a new endpoint
DELETE/api/relay/endpoints/:subjectUnregister an endpoint
GET/api/relay/endpoints/:subject/inboxRead inbox for an endpoint
GET/api/relay/dead-lettersList dead letter messages
GET/api/relay/metricsRelay system metrics
GET/api/relay/trace/metricsAggregate delivery metrics
GET/api/relay/streamSSE event stream (supports ?subject= filter)
GET/api/relay/adaptersList adapters with status
GET/api/relay/adapters/:idGet a single adapter's status
POST/api/relay/adapters/reloadHot-reload adapter configuration
POST/api/relay/adapters/:id/enableEnable an adapter
POST/api/relay/adapters/:id/disableDisable an adapter
POST/api/relay/webhooks/:adapterIdInbound webhook receiver

Next Steps