DorkOS
Concepts

Relay

Built-in messaging between agents, humans, and external platforms like Telegram

Relay

Relay is the messaging layer built into DorkOS. It routes and delivers messages between agents, humans, and external platforms like Telegram. Relay runs by default. Turn it off with the Agent messaging switch in Settings › Advanced › Tools (it takes effect when DorkOS restarts), or set DORKOS_RELAY_ENABLED=false, which overrules the switch while it is set.

Looking to actually connect Telegram or Slack to your agents? They are rows on the Connections page, no code required. This page explains how Relay works underneath it.

What Relay does

When you have agents working across different projects, they need a way to reach you, and sometimes to reach each other. An agent finishing a task should be able to message you on Telegram. Relay makes that possible.

Agents publish messages to named addresses. Relay routes each one to the right place, whether that is your phone, your browser console, or another agent. Agents never need to know each other's internals; they just agree on where a message should go.

See it work

The easiest way to see Relay in action is Telegram:

  1. Create a bot with Telegram's @BotFather and copy its token.
  2. Open Connections, select Connect on Telegram, and paste the token.
  3. Message the bot. Your message shows up on the relay.human.telegram.{chatId} address, and any agent listening there can reply.

You don't need Telegram to see Relay working, either: your own chats are already wired up. Send a chat a message and watch the reply stream back through Relay under the hood, no setup required.

No screenshot yet: this is a good place for one showing a message arriving in a Telegram chat.

Addresses, in plain terms

Every message travels to a named address, like relay.human.telegram.{chatId} for one specific Telegram chat, or relay.agent.{namespace}.{agentId} for one specific agent. You rarely need to write these yourself. DorkOS generates them for you, and each agent's own address is shown as its relaySubject; the pattern is here mostly so log lines make sense when you're troubleshooting.

If you want to know how these addresses are matched, wildcarded, stored, and secured, the reference section below covers all of it.


Reference

Here's how Relay works under the hood. Useful if you're debugging delivery, tuning limits, or building your own bridge into Relay. This section assumes a developer's background.

Subjects and endpoints

Relay organizes communication around subjects: hierarchical, dot-separated names that describe where a message goes. Subjects follow a three-tier hierarchy: relay.{audience}.{identifier}.

Common patterns:

  • relay.agent.{namespace}.{agentId}: messages addressed to a specific agent (its relaySubject)
  • relay.human.console.{clientId}: messages destined for a human's console
  • relay.human.telegram.{chatId}: messages routed to a Telegram chat
  • relay.system.tasks.{scheduleId}: system messages from your scheduled tasks

Endpoints

An endpoint is a registered destination on the bus. Registering an endpoint for a subject creates a Maildir directory structure on disk to receive messages. Maildir is a mailbox format: each message becomes its own file, so writes are atomic and crash-safe. Messages arrive as JSON files in the endpoint's new/ directory and are indexed in SQLite.

Endpoints are durable. Even if no subscriber is actively listening, messages are delivered to the endpoint's Maildir and held for later pickup.

Wildcards

Relay supports two wildcard tokens:

  • * matches exactly one segment (relay.agent.* matches relay.agent.backend, not relay.agent.backend.tasks)
  • > matches one or more trailing segments (relay.agent.> matches both)

The envelope

Every message is wrapped in an envelope that carries routing metadata alongside the payload:

  • id: a ULID (an ID format that also records creation order, so messages sort chronologically without a separate timestamp field)
  • subject: the target subject for delivery
  • from: the sender's subject identifier
  • replyTo: optional subject for response routing
  • budget: resource constraints that prevent runaway message chains
  • payload: the actual message content (any JSON-serializable value)

Budget enforcement

Every envelope carries a budget that constrains how far a message can travel:

  • hopCount / maxHops: how many times the message has been forwarded. Default maximum: 5 hops. Prevents infinite loops where agent A sends to agent B, which sends back to agent A.
  • ttl: a Unix timestamp after which the message expires. Default: 1 hour.
  • callBudgetRemaining: API calls an agent may make when processing this message.

The budget also carries an ancestor chain: the sender identifier for every hop the message has taken. If a message would be delivered to a sender that already appears in that chain, Relay rejects it as a cycle.

An expired message never runs. A message can run out of time after it is sent — while it waits for a busy agent, or sits behind another turn — and when that happens it is turned away and the sender is told, rather than answered later as if it had just arrived. Nothing gets a fresh clock because it arrived late.

Budget limits are enforced per delivery, not per publish. A message published to three endpoints consumes one hop for each delivery.

Access control

Relay enforces sender-to-subject rules that determine who can publish to what. Rules are {from, to, action, priority} tuples supporting subject wildcards. The first matching rule wins. If no rules match, the default is allow: you start open and add deny rules to restrict specific paths.

Access rules live in access-rules.json inside the Relay data directory and are watched for changes, so edits take effect without a restart.

Adapters

Relay connects to external platforms through adapters: plugins that bridge external channels into the Relay subject hierarchy. DorkOS ships with four built-in adapters:

  • Claude Code adapter: routes messages to Claude Code chats. When a message arrives on relay.agent.*, this adapter creates or resumes a Claude Code chat and passes the message as a prompt. Response chunks flow back to the sender's reply-to subject.
  • Telegram adapter: connects a Telegram bot to the bus via the grammy library. Inbound messages publish to relay.human.telegram.{chatId}. Outbound messages on matching subjects are delivered to the chat. Supports real-time streaming via sendMessageDraft.
  • Slack adapter: bridges Slack workspaces into Relay using Socket Mode (Slack's WebSocket-based connection, which avoids exposing a public webhook URL). Supports streaming responses, threading, and a working indicator driven by the turn itself.
  • Webhook adapter: a generic HTTP webhook bridge with HMAC-SHA256 signature verification (a cryptographic check that confirms a webhook payload wasn't tampered with) for both directions.

You can add custom adapters from npm packages or local file paths. Plugin adapters implement the same RelayAdapter interface as built-in adapters. Multi-agent coordination through Relay carries end-to-end test coverage and Rooms (where several agents talk in one place) are generally available; test your specific setup before relying on it for anything time-sensitive.

Storage

Relay uses two storage systems:

  • Maildir: a filesystem-based message store. Each endpoint gets a directory with new/, cur/, and failed/ subdirectories. Messages are delivered as atomic file writes, making the store resilient to crashes.
  • SQLite: ~/.dork/relay/index.db provides fast querying by subject, sender, status, and time range. It runs in WAL mode (write-ahead logging, a SQLite mode that lets one writer and multiple readers work at the same time). If the index becomes corrupted, it can be rebuilt from Maildir.

Tracing

Each published message gets a trace ID. Each delivery to an endpoint creates a span recording timing, budget consumption, and error details. The trace store lives in ~/.dork/relay/index.db alongside the message index and provides:

  • Per-message trace lookup (every delivery attempt for a given message)
  • Aggregate delivery metrics (success/failure counts, latency percentiles)
  • Budget rejection breakdowns

A message's trace is kept for eight days: the seven days an unread message can wait in an inbox, plus the day its dead letter is kept. After that the message is gone, so its trace is too. Cleanup runs every five minutes, so a dead letter can outlast its trace by up to that long. A connection's event log (connected, disconnected, errors) keeps its newest 500 events, however old they are.

Reliability

Three mechanisms protect endpoints under load:

  • Rate limiting: per-sender sliding window. Default: 100 messages per 60 seconds.
  • Circuit breakers: after 5 consecutive delivery failures, an endpoint's circuit opens and rejects further deliveries for 30 seconds before retrying.
  • Backpressure: when an endpoint's mailbox reaches capacity (default: 1000 messages), new deliveries are rejected.

All reliability settings can be tuned in ~/.dork/relay/config.json. Changes are hot-reloaded.

How chats use Relay

When Relay is enabled, chat messaging routes through Relay instead of calling the agent SDK directly. A POST to /api/sessions/:id/messages publishes to relay.agent.{sessionId}. DorkOS's Claude Code side picks it up and routes it to the SDK. Response chunks flow back through relay.human.console.{clientId} and into the client's SSE (server-sent events) stream as relay_message events.

Every chat message is tracked in the Relay index with full delivery tracing. The same access control and budget rules apply here as everywhere else in Relay.

Next steps