DorkOSDorkOS
Concepts

Relay

How DorkOS routes messages between agents, humans, and external platforms using a subject-based message bus

Relay

Relay is the inter-agent message bus built into DorkOS. It handles routing, delivery, and reliability for messages flowing between agents, humans, and external platforms like Telegram. Relay is an opt-in subsystem controlled by the DORKOS_RELAY_ENABLED environment variable.

Why a Message Bus?

When you have multiple AI agents working on different parts of a project, they need a way to communicate. An agent fixing backend code might need to tell the frontend agent about an API change. A scheduled task might need to notify you on Telegram when it finishes.

Relay provides this communication layer. Rather than agents calling each other directly, they publish messages to named subjects, and Relay handles routing those messages to the right recipients. This decoupling means agents do not need to know about each other's internals — they only need to agree on subject naming conventions.

Subjects and Endpoints

Relay organizes communication around subjects — hierarchical dot-separated names that describe where a message should go. Subjects follow a NATS-style naming convention with wildcard support.

Subject Hierarchy

Subjects use a three-tier hierarchy: relay.{audience}.{identifier}. The first segment is always relay, the second indicates the audience type, and the third and beyond identify the specific target.

Common subject patterns include:

  • relay.agent.{agentId} — Messages addressed to a specific agent
  • relay.human.console.{clientId} — Messages destined for a human's console
  • relay.human.telegram.{chatId} — Messages routed to a Telegram chat
  • relay.system.pulse.{scheduleId} — System messages from the Pulse scheduler

Endpoints

An endpoint is a registered destination on the bus. When you register an endpoint for a subject, Relay creates a Maildir directory structure on disk to receive messages. Each endpoint has a unique hash derived from its subject, and messages are delivered as JSON files into its new/ directory.

Endpoints are the durable side of Relay. Even if no subscriber is actively listening, messages are still delivered to the endpoint's Maildir and indexed in SQLite. Subscribers can pick them up later.

Pattern Matching

Relay supports two wildcard tokens for flexible routing:

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

Subscriptions use these wildcards to listen to broad categories of messages without registering separate endpoints for each one.

The Envelope

Every message flowing through Relay is wrapped in an envelope that carries metadata alongside the payload. The envelope includes:

  • id — A ULID that uniquely identifies the message and provides natural time-ordering
  • subject — The target subject for delivery
  • from — The sender's subject identifier
  • replyTo — An optional subject for response routing
  • budget — Resource constraints that prevent runaway message chains
  • createdAt — ISO 8601 timestamp
  • payload — The actual message content (any JSON-serializable value)

The envelope is what gets written to disk, indexed in SQLite, and passed to subscription handlers. The separation of routing metadata (envelope) from content (payload) means Relay can make delivery decisions without understanding the message content.

Budget Enforcement

One of Relay's most important safety features is its budget system. Every envelope carries a budget that constrains how far the message can travel and how many resources it can consume.

Budget Fields

The budget tracks three limits:

  • hopCount / maxHops — How many times the message has been forwarded versus the maximum allowed. Default maximum is 5 hops. This prevents infinite message loops where agent A sends to agent B, which sends back to agent A.
  • ttl — A Unix timestamp (milliseconds) after which the message expires. Default is 1 hour. Expired messages are rejected to the dead letter queue rather than delivered.
  • callBudgetRemaining — The number of API calls an agent is allowed to make when processing this message. This prevents a single message from triggering an expensive chain of Claude API calls.

Ancestor Chain

The budget also carries an ancestor chain — a list of sender identifiers for every hop the message has taken. Relay uses this to detect cycles: if a message would be delivered to a sender that already appears in the ancestor chain, it is rejected. This is a second line of defense against message loops, complementing the hop count limit.

Budget limits are enforced per-delivery, not per-publish. A message published to three endpoints consumes one hop for each delivery, and each endpoint receives its own copy of the budget with an incremented hop count.

Delivery Pipeline

When a message is published, it passes through a multi-stage pipeline before reaching its destinations.

Pipeline Stages

  1. Subject validation — The target subject is checked for valid format (dot-separated segments, no empty segments, no leading/trailing dots).
  2. Access control — Relay checks whether the sender is allowed to publish to the target subject. Access rules are priority-ordered and support wildcards.
  3. Rate limiting — A per-sender sliding window rate limit is applied before fan-out. By default, each sender can publish 100 messages per 60-second window.
  4. Envelope construction — A ULID is generated, the budget is populated with defaults and any caller overrides, and the full envelope is assembled.
  5. Endpoint matching — All registered endpoints whose subject matches the target are identified.
  6. Per-endpoint delivery — For each matching endpoint, three additional checks run in sequence: backpressure (is the endpoint's mailbox full?), circuit breaker (has this endpoint been failing?), and budget enforcement (has the message exceeded its hop/TTL/call limits?).
  7. Maildir write — The envelope is written as a JSON file into the endpoint's new/ directory, then indexed in SQLite.
  8. Subscriber dispatch — Matching subscription handlers are invoked synchronously. On success, the message moves from new/ to completed. On failure, it moves to failed/.

Dead Letter Queue

Messages that cannot be delivered end up in the dead letter queue (DLQ). This happens when no endpoints match the subject, when budget limits are exceeded, or when Maildir delivery fails. Dead letters are stored on disk and indexed for later inspection, providing visibility into delivery failures without losing messages.

Reliability

Relay includes three reliability mechanisms that protect endpoints from being overwhelmed and ensure graceful degradation under load.

Rate Limiting

Per-sender sliding window rate limiting prevents any single sender from flooding the bus. The default configuration allows 100 messages per 60-second window, with optional per-sender overrides for high-throughput endpoints.

Circuit Breakers

Each endpoint has its own circuit breaker that tracks consecutive delivery failures. After 5 consecutive failures (configurable), the circuit opens and further deliveries to that endpoint are rejected immediately. After a 30-second cooldown, the circuit moves to half-open state and allows a probe message through. If the probe succeeds, the circuit closes and normal delivery resumes.

Backpressure

When an endpoint's mailbox accumulates too many unprocessed messages, backpressure kicks in. At 80% capacity (configurable), Relay emits a warning signal. At 100% capacity (default: 1000 messages), new deliveries are rejected outright. This prevents slow consumers from causing memory issues or disk exhaustion.

All three reliability settings can be tuned via a config.json file in the Relay data directory (~/.dork/relay/config.json). Changes are hot-reloaded — you do not need to restart the server.

Access Control

Relay enforces sender-to-subject access rules that determine who can publish to what. Rules are defined as {from, to, action, priority} tuples, where from and to are subject patterns supporting wildcards.

Rules are evaluated in priority order (highest priority first). The first matching rule wins. If no rules match, the default policy is to allow the message. This makes access control additive — you start open and add deny rules to restrict specific paths.

Access rules are persisted in access-rules.json inside the Relay data directory and are watched for changes via a file watcher, so you can edit them externally and have them take effect without a restart.

Adapters

Relay connects to external platforms through adapters — plugins that bridge external communication channels into the Relay subject hierarchy. Adapters handle both inbound messages (external platform to Relay) and outbound delivery (Relay to external platform).

Built-in Adapters

DorkOS ships with three built-in adapters:

  • Claude Code adapter — Routes messages to Claude Agent SDK sessions. When a message arrives on a relay.agent.* subject, this adapter creates or resumes a Claude session and passes the message content as a prompt. Response chunks flow back through Relay to the sender's reply-to subject.
  • Telegram adapter — Connects a Telegram bot to the Relay bus. Inbound Telegram messages are published to relay.human.telegram.{chatId}, and outbound messages on matching subjects are delivered back to the Telegram chat.
  • Webhook adapter — A generic HTTP webhook bridge with HMAC-SHA256 signature verification for both inbound and outbound directions. Supports secret rotation with a 24-hour transition window.

Plugin Adapters

You can extend Relay with custom adapters loaded from npm packages or local file paths. Plugin adapters implement the same RelayAdapter interface as built-in adapters and are managed through the adapter configuration file.

Storage

Relay uses two storage systems working together:

  • Maildir — A filesystem-based message store inspired by the traditional Maildir email format. Each endpoint gets a directory with new/, cur/, and failed/ subdirectories. Messages are delivered as atomic file writes, making the store resilient to crashes. The Maildir is the authoritative source of message data.
  • SQLite — The consolidated DorkOS database (~/.dork/dork.db) managed by Drizzle ORM provides fast querying by subject, sender, status, and time range. SQLite uses WAL mode for concurrent read/write access. The relay_index and relay_traces tables live alongside other DorkOS tables. If the index becomes corrupted, it can be rebuilt from the Maildir files.

This dual-storage design means you get both durability (Maildir survives crashes) and performance (SQLite enables efficient queries and cursor-based pagination).

Tracing

Relay includes a tracing system for tracking message delivery end-to-end. Each published message gets a trace ID, and each delivery to an endpoint creates a span. Spans record timing information (sent, delivered, processed timestamps), budget consumption, and error details.

The trace store lives in the consolidated DorkOS database (~/.dork/dork.db) alongside the message index and provides:

  • Per-message trace lookup (see every delivery attempt for a given message)
  • Aggregate delivery metrics (total messages, success/failure counts, latency percentiles)
  • Budget rejection breakdowns (how many messages were rejected for hop limits, TTL expiry, cycle detection, or budget exhaustion)

Integration with Sessions

When Relay is enabled, the standard DorkOS session messaging path changes. Instead of POST /api/sessions/:id/messages directly calling the Agent SDK, the message is published to relay.agent.{sessionId}. The Claude Code adapter picks it up and routes it to the Agent SDK. Response chunks flow back through relay.human.console.{clientId} and into the client's SSE stream as relay_message events.

This indirection means every session message is tracked in the Relay index with full delivery tracing, and the same message routing rules (access control, budgets, rate limits) apply to interactive sessions as they do to automated agent-to-agent communication.

Next Steps