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"
        }
      ]
    }
  }
}
FieldDescription
`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"
        }
      ]
    }
  }
}
FieldDescription
`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:

CommandPermissionDescription
`/help`readList available commands
`/status`readShow project status and daemon health
`/ralph-status`readShow active agent loop status
`/health`readRun health check on all subsystems
`/ask <question>`readAsk the AI a question about your project
`/approve <gate-id>`writeApprove a pending HITL gate
`/reject <gate-id> [reason]`writeReject 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:

SettingDefaultDescription
`maxConcurrent`3Maximum simultaneous AI processes
`maxContextMessages`10Conversation history depth (message pairs)
`timeoutMs`120000AI response timeout (milliseconds)
`maxResponseLength`4000Maximum 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

CategoryTopicsDescription
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:

SeverityDisplayUse Case
`info`Normal textStatus updates, completions
`warning`HighlightedDegraded health, gate timeouts
`critical`Alert/urgentSecurity 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

MethodDescription
`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)

MethodDescription
`_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