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. Set DORKOS_RELAY_ENABLED=false to turn it off.
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 the Telegram adapter:
- Create a bot with Telegram's @BotFather and copy its token.
- Add a Telegram adapter in DorkOS with that token.
- 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 console session is already wired up. Send a session 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.{agentId} for one specific agent (yes, that includes relay.agent.dorkbot, should you ever want to message the mascot directly). You rarely need to write these yourself. DorkOS and its adapters generate them for you; 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 an adapter. 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.{agentId}: messages addressed to a specific agentrelay.human.console.{clientId}: messages destined for a human's consolerelay.human.telegram.{chatId}: messages routed to a Telegram chatrelay.system.tasks.{scheduleId}: system messages from the Task scheduler
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.*matchesrelay.agent.backend, notrelay.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.
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 sessions. When a message arrives on
relay.agent.*, this adapter creates or resumes a Claude session 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 viasendMessageDraft. - 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 typing indicators.
- 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 is shipped, but we haven't verified every agent-to-agent path end to end yet, so test your specific setup before relying on it in production.
Storage
Relay uses two storage systems:
- Maildir: a filesystem-based message store. Each endpoint gets a directory with
new/,cur/, andfailed/subdirectories. Messages are delivered as atomic file writes, making the store resilient to crashes. - SQLite:
~/.dork/relay/index.dbprovides 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
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.
Integration with sessions
When Relay is enabled, session messaging routes through Relay instead of calling the agent SDK directly. A POST to /api/sessions/:id/messages publishes to relay.agent.{sessionId}. The Claude Code adapter 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 session 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.