Relay is a lightweight Telegram app for running long-lived ACP coding agents from DMs, groups, and topic chats. Point it at a project, connect your bot, and keep agent work moving without leaving Telegram.
It runs as one app with local SQLite state: no Redis, Postgres, object storage, queues, public webhook, or published port required. Use Codex, OpenCode, Copilot, Gemini, Claude Code, or any ACP-compatible command, with durable history, memory, MCP tools, and optional git workspace isolation.
npm install -g -y @normahq/relay
relay init
relay start| Feature | What it means |
|---|---|
| No backing services | Relay stores local state in SQLite and does not require Redis, Postgres, queues, or object storage. |
| No webhook required | Polling mode is the default, so local quickstarts do not need a public URL or published port. |
| Any ACP agent | Use built-in providers for codex, opencode, copilot, gemini, and claude, or wire any ACP-compatible command with generic_acp. |
| Telegram control plane | One owner, optional collaborators, direct-message sessions, topic sessions with /topic <name>, and public-chat mention/reply routing. |
| Git workspaces | Each session can get its own git worktree, with relay.workspace.import and relay.workspace.export MCP tools for safe branch flow. |
| Durable sessions | SQLite persistence is on by default, so conversation history survives restarts until /reset or explicit /close. |
| Memory system | MEMORY.md stores facts, /memory shows them, and relay.memory.* MCP tools let agents remember user-approved facts. |
| MCP support | Add stdio, HTTP, or SSE MCP servers globally, per provider, or for every Relay session. |
| Docker Compose runtime | Run Relay in a container while using the current directory, .env, .git, and .config/relay from the host. |
- Pick an ACP provider.
- Connect a Telegram bot token.
- Chat, create topics, and let Relay persist session state, memory, and workspaces.
Relay runs one provider runtime per process and maps Telegram chats/topics to separate agent sessions. That keeps the bot simple to operate while preserving session boundaries.
You need:
- a Telegram bot token from BotFather
- at least one provider CLI for host installs:
codex,opencode,copilot,gemini, orclaude - Node.js/npm, unless you use the Docker Compose flow
Install Relay:
npm install -g -y @normahq/relayInitialize Relay in your project:
relay initrelay init detects provider CLIs, validates the Telegram token, writes
.config/relay/config.yaml, initializes .config/relay/relay.db, and prints
the next commands. By default, the Telegram token is stored in .env.
Start Relay:
relay startAuthenticate in Telegram with the printed auth URL, or send the printed command directly to your bot:
/start owner=<owner_token>
After owner auth, send a normal direct message to use the owner session. Create a named topic session when you want an isolated workspace and conversation:
/topic <name>
Relay ships a root Dockerfile and compose.yaml
for local Docker Compose runtime.
This path is designed for real project work. The current directory is mounted as
/workspace, so Relay sees your host checkout, .git, .env,
.config/relay/config.yaml, and .config/relay/relay.db.
docker compose build relay
docker compose run --rm relay init
docker compose up -d relayProvider credentials are not baked into the image. Authenticate with provider
environment variables or provider login commands run through Compose.
relay-home persists provider CLI home config across container recreates.
Polling mode is the default and does not require publishing a port. Webhook
setup and image details are documented in docs/relay.md.
Relay has built-in provider types for common CLIs and a generic ACP adapter for anything else that speaks ACP.
runtime:
providers:
my-agent:
type: generic_acp
generic_acp:
cmd: ["my-acp-agent", "--stdio"]
model: "my-model"
relay:
provider: my-agentBuilt-in provider types:
codex_acpopencode_acpcopilot_acpgemini_acpclaude_code_acpgeneric_acppool
/topic <name>: create a named topic session./reset: clear conversation history for the current session./close: reset history, then close the current topic or restart the owner session on the next message./cancel: cancel in-flight work and drop queued turns for the current session./memory: print current${relay.state_dir}/MEMORY.mdcontents when memory is enabled./start owner=<owner_token>: authenticate the owner in direct messages./start invite=<invite_token>: onboard a collaborator in direct messages./user add|list|remove: manage collaborators; owner only.
Relay loads .config/relay/config.yaml, then applies RELAY_* environment
overrides. If .env exists in the working directory, Relay loads it before
config resolution.
Minimal shape:
runtime:
providers:
<provider_id>:
# generic_acp | gemini_acp | codex_acp | opencode_acp | copilot_acp | claude_code_acp | pool
type: <provider_type>
mcp_servers: {}
relay:
provider: <provider_id>
telegram:
token: ""
formatting_mode: "markdownv2"
plan_updates: true
webhook:
enabled: false
listen_addr: "0.0.0.0:8080"
path: "/telegram/webhook"
url: ""
logger:
level: "info"
pretty: true
working_dir: ""
state_dir: ".config/relay"
sessions:
persistence: "sqlite"
memory:
enabled: true
workspace:
mode: "auto"
base_branch: ""
mcp_servers: []
global_instruction: ""Common settings:
relay.provider: provider ID selected duringrelay init.relay.telegram.token: Telegram bot token, usually supplied by.envasRELAY_TELEGRAM_TOKEN.relay.sessions.persistence:sqliteby default; keeps ADK conversation history across restarts until/resetor explicit/close.relay.memory.enabled:trueby default; controls${relay.state_dir}/MEMORY.md,/memory, andrelay.memory.*MCP tools.${relay.state_dir}/SOUL.md: optional operator instructions read at session start/restore when the file exists.relay.workspace.mode:autoby default; uses git worktrees when Relay runs in a git repository.relay.mcp_servers: extra MCP server IDs added to every Relay-started session.
MCP servers can be attached to providers or injected into every Relay session.
Relay also includes a built-in relay MCP server for memory and workspace
tools.
runtime:
mcp_servers:
local-tools:
type: stdio
cmd: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
remote-tools:
type: http
url: https://mcp.example.com/mcp
providers:
codex:
type: codex_acp
mcp_servers:
- local-tools
relay:
provider: codex
mcp_servers:
- remote-toolsEffective MCP IDs are built-in relay + provider mcp_servers + relay.mcp_servers.
Do not define runtime.mcp_servers.relay; Relay owns that bundled server.
telegram token is required: runrelay init, setRELAY_TELEGRAM_TOKENin.env, or setrelay.telegram.tokenin config.no supported agent CLI detected: install or expose one ofcodex,opencode,copilot,gemini, orclaude.relay.provider is required: rerunrelay initor setrelay.providerto a configured provider ID.- Session history should not survive restarts: set
relay.sessions.persistence=memoryorRELAY_SESSIONS_PERSISTENCE=memory. - Memory facts are not visible in an active session: memory is snapshotted when a session starts or restores; use
/resetor/closeto recreate the provider session. - Workspace import/export issues: check
relay.workspace.mode,relay.workspace.base_branch, and that Relay is running in the expected git checkout. - Progress updates are too noisy: set
relay.telegram.plan_updates=false.
- Technical specification:
docs/relay.md - Release notes:
docs/release-notes.md - Telegram formatting guide:
docs/telegram-formatting.md - Contributing guide:
CONTRIBUTING.md - Agent workflow/policies:
AGENTS.md
- GitHub Releases: https://github.com/normahq/relay/releases
- npm package: https://www.npmjs.com/package/@normahq/relay