Messaging
Chat platform integration
Messaging Guide
AIWG messaging integration connects your project to Slack, Discord, and Telegram for real-time notifications, interactive commands, and 2-way AI chat. The messaging subsystem runs within the daemon process.
Overview
The messaging system provides:
- Event notifications — Receive alerts for agent loop completions, security issues, build failures, and HITL gates
- Interactive commands — Query status, approve gates, and manage workflows from chat
- 2-way AI chat — Ask questions about your project directly from messaging platforms
- Multi-platform — Same event stream delivered to all connected platforms simultaneously
Quick Start
1. Set environment variables
Each platform requires a bot token via environment variable:
# Slack
export AIWG_SLACK_TOKEN="xoxb-your-bot-token"
export AIWG_SLACK_CHANNEL="#aiwg-notifications"
# Discord
export AIWG_DISCORD_TOKEN="your-discord-bot-token"
export AIWG_DISCORD_CHANNEL="channel-id"
# Telegram
export AIWG_TELEGRAM_TOKEN="123456:ABCdefGHIjklMNOpqrsTUVwxyz"
export AIWG_TELEGRAM_CHAT_ID="-1001234567890"
2. Start the daemon
aiwg daemon start
The messaging hub auto-discovers enabled adapters from environment variables. No configuration file changes needed.
3. Verify
The daemon log confirms adapter loading:
[messaging] Hub started with 2 adapter(s): slack, telegram
[messaging] 2-way AI chat enabled
Platform Setup
Slack
1. Create a Slack app at https://api.slack.com/apps 2. Add Bot Token Scopes: `chat:write`, `commands`, `app_mentions:read` 3. Install to workspace and copy the Bot User OAuth Token 4. Set environment variables:
export AIWG_SLACK_TOKEN="xoxb-..."
export AIWG_SLACK_CHANNEL="#aiwg-notifications" # Default channel for notifications
Discord
1. Create a Discord application at https://discord.com/developers/applications 2. Add a Bot under the application 3. Enable Message Content Intent in Bot settings 4. Generate an invite URL with `Send Messages` and `Read Message History` permissions 5. Invite the bot to your server 6. Set environment variables:
export AIWG_DISCORD_TOKEN="your-bot-token"
export AIWG_DISCORD_CHANNEL_ID="channel-id" # Right-click channel → Copy ID
To get a channel ID: enable Developer Mode in Discord (Settings → Advanced), then right-click any channel → Copy Channel ID.
Multi-channel configuration
Route different event types to separate Discord channels using `config.rooms` in your daemon configuration:
{
"messaging": {
"discord": {
"botToken": "your-bot-token",
"rooms": [
{
"channel_id": "1234567890",
"label": "dev-notifications",
"is_default": true,
"purpose": "interactive"
},
{
"channel_id": "9876543210",
"label": "security-alerts",
"is_default": true,
"purpose": "notifications"
},
{
"channel_id": "1122334455",
"label": "debug-logs",
"is_default": false,
"purpose": "logs"
}
]
}
}
}
| Field | Description |
|---|---|
| `channel_id` | Discord channel ID (also accepts `channelId`) |
| `label` | Human-readable name for this channel |
| `is_default` | When `true`, the channel receives all broadcast messages (e.g., agent loop completions, health alerts). When `false`, the channel only receives messages sent to it explicitly. Also accepts `isDefault`. |
| `purpose` | Informational. `interactive` (commands), `notifications` (one-way events), `logs` (verbose output) |
Rooms with `is_default: true` receive `broadcastToRooms()` messages. Multiple default rooms can exist — all receive the same broadcast. Rooms with `is_default: false` only receive messages sent via `sendToRoom(message, channelId)`.
Single-channel fallback: If `rooms` is omitted, the adapter uses `AIWG_DISCORD_CHANNEL_ID` or `defaultChannelId` as the sole channel with `is_default: true`.
Telegram
1. Message @BotFather on Telegram 2. Send `/newbot` and follow the prompts 3. Copy the bot token 4. Add the bot to your group/channel 5. Get the chat ID (send a message, then check `https://api.telegram.org/bot<token>/getUpdates`) 6. Set environment variables:
export AIWG_TELEGRAM_TOKEN="123456:ABCdef..."
export AIWG_TELEGRAM_CHAT_ID="-1001234567890"
Multi-room configuration
Route different event types to separate Telegram chats or groups using `config.rooms`:
{
"messaging": {
"telegram": {
"botToken": "123456:ABCdef...",
"rooms": [
{
"chat_id": "-1001234567890",
"label": "dev-team",
"is_default": true,
"purpose": "interactive"
},
{
"chat_id": "-1009876543210",
"label": "security-channel",
"is_default": true,
"purpose": "notifications"
}
]
}
}
}
| Field | Description |
|---|---|
| `chat_id` | Telegram chat/group/channel ID (also accepts `chatId`) |
| `label` | Human-readable name for this chat |
| `is_default` | When `true`, receives broadcast messages. Also accepts `isDefault`. |
| `purpose` | Informational. `interactive`, `notifications`, or `logs` |
Single-room fallback: If `rooms` is omitted, the adapter uses `AIWG_TELEGRAM_CHAT_ID` or `defaultChatId` as the sole room with `is_default: true`.
Commands
All platforms support the same command set. Prefix commands with `/` in chat:
| Command | Permission | Description |
|---|---|---|
| `/help` | read | List available commands |
| `/status` | read | Show project status and daemon health |
| `/ralph-status` | read | Show active agent loop status |
| `/health` | read | Run health check on all subsystems |
| `/ask <question>` | read | Ask the AI a question about your project |
| `/approve <gate-id>` | write | Approve a pending HITL gate |
| `/reject <gate-id> [reason]` | write | Reject a pending HITL gate with optional reason |
Permissions
Commands have two permission levels:
- read — Available to everyone in the channel
- write — Requires explicit permission grant
Grant write permissions via the `writeUsers` option:
const hub = await createMessagingHub({
writeUsers: ['U12345', '[email protected]'],
});
Or in daemon configuration, set `AIWG_WRITE_USERS` environment variable (comma-separated user IDs).
Command Examples
Check project status:
You: /status
Bot: Project: my-project
Phase: Construction
Daemon uptime: 3600s
Health: healthy
Approve a HITL gate:
You: /approve gate-e2c-001
Bot: Gate gate-e2c-001 approved
Ask a question:
You: /ask what is our test coverage?
Bot: Based on the project configuration, the test suite runs via
`npx vitest run` and currently has approximately 2,619 tests...
2-Way AI Chat
Beyond slash commands, you can send free-text messages to the bot for AI-powered responses. The bot uses `claude -p` with your project's full context (CLAUDE.md, codebase, etc.).
How It Works
1. Send a message to the bot (DM or mention in a channel) 2. The messaging hub forwards it to the ChatHandler 3. ChatHandler spawns a `claude -p` process with conversation context 4. The AI response is sent back to the originating platform
Multi-Turn Conversations
The chat system maintains conversation history per chat channel. Each conversation tracks up to 10 message pairs (configurable), so the AI remembers context from recent messages:
You: What testing framework do we use?
Bot: The project uses Vitest for testing...
You: How do I add a new test file?
Bot: Based on the existing test structure, create a new file in
test/unit/ following the pattern...
Configuration
Chat behavior is configured via `createMessagingHub()` options or environment variables:
| Setting | Default | Description |
|---|---|---|
| `maxConcurrent` | 3 | Maximum simultaneous AI processes |
| `maxContextMessages` | 10 | Conversation history depth (message pairs) |
| `timeoutMs` | 120000 | AI response timeout (milliseconds) |
| `maxResponseLength` | 4000 | Maximum response characters |
Concurrency Limits
To prevent resource exhaustion, the chat system limits concurrent AI processes:
- Global limit: Maximum 3 simultaneous `claude -p` processes (configurable)
- Per-chat dedup: Only one AI process per chat at a time
- Busy response: When limits are reached, users receive a "please wait" message
Disabling Chat
To run messaging with notifications and commands only (no AI chat):
const hub = await createMessagingHub({
chatHandler: false,
});
Event Topics
The messaging system forwards events from the internal event bus to all connected platforms. Events are formatted into human-readable messages with severity indicators.
Event Categories
| Category | Topics | Description |
|---|---|---|
| Al | `ralph.started`, `ralph.iteration`, `ralph.completed`, `ralph.failed`, `ralph.aborted` | Agent loop lifecycle events |
| HITL Gates | `gate.pending`, `gate.approved`, `gate.rejected`, `gate.timeout` | Human-in-the-loop gate events |
| Security | `security.critical`, `security.warning`, `security.scan_done` | Security scan results |
| Health | `health.check`, `health.degraded`, `health.recovered` | System health transitions |
| Build | `build.failed`, `build.passed` | Build/test results |
| Daemon | `daemon.started`, `daemon.stopping` | Daemon lifecycle |
| Chat | `chat.message`, `chat.response`, `chat.error` | Chat session events |
Event Severity
Events carry a severity level that affects formatting:
| Severity | Display | Use Case |
|---|---|---|
| `info` | Normal text | Status updates, completions |
| `warning` | Highlighted | Degraded health, gate timeouts |
| `critical` | Alert/urgent | Security issues, failures |
Publishing Custom Events
From your own integrations, publish events to the messaging hub:
hub.publish({
topic: 'custom.event',
source: 'my-integration',
severity: 'info',
summary: 'Deployment completed successfully',
details: { version: '1.2.3', environment: 'staging' },
timestamp: new Date().toISOString(),
});
Custom Adapters
To add support for a new messaging platform, extend `BaseAdapter`:
import { BaseAdapter } from '../adapters/base.mjs';
export class MyPlatformAdapter extends BaseAdapter {
constructor(config) {
super('myplatform');
this.config = config;
}
async initialize() {
// Connect to platform API
this._setConnected();
}
async send(message, channel) {
// Format and send message to platform
this._recordSend();
return { messageId: '...', channelId: channel, success: true };
}
async update(messageId, message) {
// Update an existing message
}
async shutdown() {
this._setDisconnected();
}
}
Required Methods
| Method | Description |
|---|---|
| `initialize()` | Connect to platform, set up event listeners |
| `send(message, channel)` | Send a formatted message |
| `update(messageId, message)` | Update an existing message |
| `shutdown()` | Disconnect and clean up |
Protected Methods (from BaseAdapter)
| Method | Description |
|---|---|
| `_setConnected()` | Mark adapter as connected |
| `_setDisconnected()` | Mark adapter as disconnected |
| `_recordSend()` | Increment sent message counter |
| `_recordReceive()` | Increment received message counter |
| `_recordError(error)` | Record an error |
| `_dispatchCommand(command, args, context)` | Forward command to registered handlers |
| `_dispatchMessage(text, context)` | Forward free-text message to handlers |
Registering Inbound Handlers
For platforms that support inbound messages, call the dispatch methods from your adapter's event listeners:
// In your adapter's initialize():
this.client.on('message', (msg) => {
if (msg.text.startsWith('/')) {
const [command, ...args] = msg.text.slice(1).split(' ');
this._dispatchCommand(command, args, {
chatId: msg.chatId,
from: msg.author,
});
} else {
this._dispatchMessage(msg.text, {
chatId: msg.chatId,
from: msg.author,
});
}
});
Architecture
┌─────────────────────────────────────────────────┐
│ Daemon Process │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ EventBus │───→│ MessageFormatter │ │
│ └──────────┘ └────────┬─────────┘ │
│ ↑ ↓ │
│ Events from Formatted messages │
│ Al, Health, ┌─────────┬─────────┐ │
│ Security, etc. ↓ ↓ ↓ │
│ ┌───────┐ ┌───────┐ ┌────────┐ │
│ │ Slack │ │Discord│ │Telegram│ │
│ └───┬───┘ └───┬───┘ └────┬───┘ │
│ ↓ ↓ ↓ │
│ Inbound: ┌──────────────────────────────┐ │
│ /commands │ CommandRouter │ │
│ free text │ ChatHandler │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Troubleshooting
No adapters loaded
Verify environment variables are set:
echo $AIWG_SLACK_TOKEN
echo $AIWG_DISCORD_TOKEN
echo $AIWG_TELEGRAM_TOKEN
At least one token must be set for messaging to activate.
Bot not responding to commands
- Verify the bot has appropriate permissions in the channel
- Check daemon logs: `tail -f .aiwg/daemon/daemon.log`
- Ensure commands start with `/` (e.g., `/status`, not `status`)
AI chat responses are slow
Each response spawns a `claude -p` process with full project context. For large projects, this can take 30-60 seconds. To speed up:
- Reduce project context (optimize CLAUDE.md)
- Increase `maxConcurrent` if you have resources
- Use `/ask` for quick questions (routes through command system)
Messages truncated
Response length is capped at `maxResponseLength` (default 4000 chars) to respect platform limits. Truncated responses end with `[...truncated]`. Adjust the limit in chat handler configuration.
Rate limiting
If a platform rate-limits the bot, errors appear in daemon logs. The adapters handle rate limits gracefully with backoff, but sustained high-volume notifications may require message batching configuration.
Cross-References
- Daemon Guide — Daemon setup and management
- Al Guide — Agent loops with messaging notifications
- `.aiwg/architecture/adrs/ADR-messaging-bot-mode.md` — Architecture decision
- `.aiwg/architecture/adrs/ADR-2way-chat.md` — Chat architecture
- `tools/messaging/README.md` — Developer documentation