2026 03 21 Vitto Unified Channel Completion Design
Documentation for Plans
Vitto Unified Channel Completion — Design Spec
Session: S250 Date: 2026-03-21 Status: Draft Related: WAHA Personal WhatsApp Spec (standalone, paused until SIM acquired)
Problem Statement
Vitto has an impressive amount of channel infrastructure built across S117-S218 — webhook endpoints, channel gateway, group orchestration, whistle, proactive dispatch, rich adapters, cross-channel bridge. But almost none of it works end-to-end. The pieces exist in isolation.
What works today (verified):
- WhatsApp inbound → Vitto processes → replies via WhatsApp (full loop, production-quality)
- Slack inbound → Vitto processes → replies via Slack (full loop)
- Telegram inbound → Vitto processes → replies via Telegram (text only)
- Discord inbound → Vitto processes → replies via Discord (embeds only)
- Email inbound → Vitto processes → replies via Email (basic)
- Channel messages bridge to web UI via
persistToChannelSession()→bridgeMessageToAiApi()(S217) - Rich message adapters: WhatsApp (complete), Teams (partial), Web (complete)
What's broken (verified):
manage_grouptool → always fails.tool-registry.service.ts:503setsgroupOrchestration: undefined. Vitto literally cannot create groups, post to groups, or manage participants. The tool exists, the service exists, the injection is missing.send_messagetool → always fails.tool-registry.service.ts:504setschannelGateway: undefined. Vitto cannot proactively send messages via any channel through the tool system.No frontend UI for groups. No create group dialog, no group list, no group chat view, no whistle notification UI. The backend has REST endpoints (
POST /engine/groups, etc.) but nothing calls them.Telegram: no inline keyboards or callback handling. Vitto sends text-only replies. No buttons, no interactive menus, no callback_query processing.
Discord: no button/modal support. Only embeds. No component interactions.
Slack: no Block Kit interactive components. Text + basic formatting only. No button actions, no modals.
No channel adapters for Slack, Discord, Telegram. Only WhatsApp, Web, and Teams have registered adapters. ProactiveDispatcher falls back to plain text for other channels.
WhatsApp Cloud API credentials not configured.
WhatsAppCloudApiServiceexists and calls Meta Graph API v21.0, but env vars (WHATSAPP_CLOUD_PHONE_NUMBER_ID, etc.) are empty. This blocks group creation on WhatsApp.Telegram group creation impossible via Bot API. Bot API cannot create groups — only forum topics in existing supergroups.
TelegramGroupService.createGroup()correctly returns an error.
Solution
Wire the existing pieces together and close the gaps. This is NOT a rewrite — it's a completion job. The architecture from S117-S218 is sound. The services are real, tested, and well-structured. What's missing is:
- Two lines of dependency injection
- Channel adapters for 3 platforms
- Frontend UI for group management
- Interactive message callback handlers
- Credential configuration
Layers
Layer 1: Wire the Tools (Critical — hours, not days)
Problem: Two undefined values in tool-registry.service.ts disable all messaging tools.
Fix:
// APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts
// 1. Add imports
import { GroupOrchestrationService } from '../connectivity/group-orchestration.service';
import { ChannelGatewayService } from '../connectivity/channel-gateway.service';
// 2. Add to constructor (both @Optional since they come from ConnectivityModule)
constructor(
// ... existing injections ...
@Optional() private readonly groupOrchestrationService?: GroupOrchestrationService,
@Optional() private readonly channelGatewayService?: ChannelGatewayService,
) {}
// 3. Fix the context (lines 503-504):
// BEFORE:
groupOrchestration: undefined,
channelGateway: undefined,
// AFTER:
groupOrchestration: this.groupOrchestrationService
? {
createGroup: (opts) => this.groupOrchestrationService!.createGroup(opts),
postUpdate: (groupId, message, opts) => this.groupOrchestrationService!.postUpdate(groupId, message, opts),
addParticipant: (groupId, participant) => this.groupOrchestrationService!.addParticipant(groupId, participant),
removeParticipant: (groupId, externalId) => this.groupOrchestrationService!.removeParticipant(groupId, externalId),
pinMessage: (groupId, messageId) => this.groupOrchestrationService!.pinMessage(groupId, messageId),
archiveGroup: (groupId) => this.groupOrchestrationService!.archiveGroup(groupId),
listGroups: (spaceId, filter) => this.groupOrchestrationService!.listGroups(spaceId, filter),
}
: undefined,
channelGateway: this.channelGatewayService
? {
sendResponse: (response) => this.channelGatewayService!.sendResponse(response),
}
: undefined,
Module wiring: ConnectivityModule already exports GroupOrchestrationService. Verify EngineModule imports ConnectivityModule (it does — confirmed in engine.module.ts). The @Optional() decorator ensures no crash if the import chain has issues — the tool just returns the "not available" error gracefully, same as today but with a working path.
Verification: After this fix, ask Vitto "create a Slack group called test-channel in space X" — the manage_group tool should fire and call GroupOrchestrationService.createGroup().
Layer 2: Channel Adapters (2-3 days)
Problem: Only WhatsApp, Web, and Teams have registered adapters. Slack, Discord, and Telegram get plain text fallbacks.
Pattern to follow: WhatsAppChannelAdapter at APIs/backend-vitto-api/src/app/engine/adapters/whatsapp-channel.adapter.ts.
Each adapter implements:
export interface ChannelAdapter {
readonly channelType: string;
supports(message: RichMessage): boolean;
render(message: RichMessage, context: ChannelAdapterContext): Promise<ChannelAdapterResult>;
}
2a. Slack Adapter
File: APIs/backend-vitto-api/src/app/engine/adapters/slack-channel.adapter.ts
Slack uses Block Kit for rich messages. Maps:
quick_reply→actionsblock withbuttonelementslist_picker→sectionblocks withaccessory: static_selectcard→section+image+actionscarousel→ multiplesectionblocks (Slack has no native carousel)cta→actionsblock withbuttonelements (URL type for links)
Critical: Slack interactive components require a Request URL configured in the Slack App settings. When a user clicks a button, Slack POSTs to this URL. Currently no handler exists.
New handler needed: POST /engine/integrations/slack/interactions in vitto.controller.ts
@Public()
@Post('integrations/slack/interactions')
async slackInteraction(@Body() body: { payload: string }) {
const payload = JSON.parse(body.payload);
// payload.type === 'block_actions' for button clicks
// payload.actions[0].value contains the action data
// Route back to engine as a follow-up task
await this.engine.submitTask({
summary: payload.actions[0].value,
type: 'channel-message',
priority: 'normal',
context: {
channelType: 'slack',
channelId: payload.channel.id,
userId: payload.user.id,
replyTo: 'slack',
replyChannelId: payload.channel.id,
replyMetadata: { threadTs: payload.message?.ts },
},
});
}
2b. Telegram Adapter
File: APIs/backend-vitto-api/src/app/engine/adapters/telegram-channel.adapter.ts
Telegram uses inline keyboards and reply keyboards. Maps:
quick_reply→reply_markup: { inline_keyboard: [[{ text, callback_data }]] }list_picker→ multiple rows of inline keyboard buttonscard→sendPhotowith caption + inline keyboardcarousel→sendMediaGroup(album) + separate keyboard messagecta→ inline keyboard withurlbuttons
Callback handler needed: Extend existing handleTelegramWebhook() in ChannelGatewayService:
// In handleTelegramWebhook()
if (body.callback_query) {
// User pressed an inline keyboard button
const query = body.callback_query;
// Acknowledge the callback (removes loading spinner)
await this.telegramGroup.answerCallbackQuery(query.id);
return {
message: {
channelId: String(query.message.chat.id),
userId: String(query.from.id),
text: query.data, // The callback_data from the button
metadata: { callbackQueryId: query.id, messageId: query.message.message_id },
},
};
}
TelegramGroupService extension needed: Add answerCallbackQuery(queryId) and sendMessageWithKeyboard(chatId, text, keyboard) methods.
2c. Discord Adapter
File: APIs/backend-vitto-api/src/app/engine/adapters/discord-channel.adapter.ts
Discord uses components (buttons, select menus). Maps:
quick_reply→components: [{ type: 1, components: [{ type: 2, style: 1, label, custom_id }] }]list_picker→components: [{ type: 1, components: [{ type: 3, options: [...], custom_id }] }](string select menu)card→ embed with componentscta→ button withstyle: 5(link button, has URL)
Interaction handler: Discord interactions already route through POST /engine/integrations/discord/webhook. The handleDiscordEvent() method needs to handle type: 3 (MESSAGE_COMPONENT) in addition to the existing type: 1 (PING).
2d. Adapter Registration
// In engine.module.ts, update CHANNEL_ADAPTERS factory:
{
provide: 'CHANNEL_ADAPTERS',
useFactory: (wa, web, teams, slack, telegram, discord) =>
[wa, web, teams, slack, telegram, discord],
inject: [
WhatsAppChannelAdapter,
WebChatChannelAdapter,
TeamsChannelAdapter,
SlackChannelAdapter, // NEW
TelegramChannelAdapter, // NEW
DiscordChannelAdapter, // NEW
],
}
Layer 3: Group Management Frontend (3-5 days)
Problem: Backend has full group CRUD (REST endpoints + LLM tools). No UI exists.
3a. Group Management Page
Location: libs/libs-structural/vitto-hub/ or libs/libs-ai/vitto-chat/
Scope (deliberate YAGNI — minimum viable):
- Group list — shows active groups for current space, with channel icon, participant count, last activity
- Create group dialog — pick channel (WhatsApp/Telegram/Slack), name, add participants from space members
- Group detail view — participants, message history (read from GroupConversation + linked ChatSession)
- Post to group — text input, sends via
POST /engine/groups/:id/message
API calls (all existing endpoints in vitto.controller.ts):
GET /engine/groups?spaceId=X— listPOST /engine/groups— createPOST /engine/groups/:id/message— postPOST /engine/groups/:id/participants— addDELETE /engine/groups/:id/participants/:externalId— remove
Critical design note: The group chat view should show messages from ALL channels in a single timeline — the whole point of the cross-channel bridge. Messages tagged with their origin channel (WhatsApp badge, Slack badge, etc.). This is already how message-bubble.component.ts works — it renders channelBadge based on the channel field.
3b. Whistle Notification UI
Scope:
- Toast/notification when someone whistles you into a conversation
- Accept/decline actions
- Shows who whistled, which conversation, and why
Integration: ChatGateway.emitWhistle() already emits via Socket.IO to user:{userId} room. Frontend needs to listen for this event and display a notification.
3c. Channel Setup Wizard
Purpose: Guide users through connecting their first external channel (Slack bot token, Telegram bot token, etc.).
Scope:
- Step 1: Pick channel type
- Step 2: Enter credentials (bot token, webhook URL, etc.)
- Step 3: Verify connection (send test message)
- Step 4: Configure notification preferences
Deferred: This is useful but not blocking. Channels can be configured via env vars first, UI later.
Layer 4: Credential & Configuration Gaps
4a. WhatsApp Cloud API
Status: WhatsAppCloudApiService is implemented and calls Meta Graph API v21.0. Env vars are empty.
To activate:
WHATSAPP_CLOUD_PHONE_NUMBER_ID=<from Meta Business Manager>
WHATSAPP_CLOUD_BUSINESS_ACCOUNT_ID=<your WABA ID>
WHATSAPP_CLOUD_ACCESS_TOKEN=<generate in Business Manager>
Risk: This is a separate Meta Business API account from Twilio. Twilio wraps the Business API; Cloud API is direct. Both can coexist on the same WABA if configured correctly. Verify with Meta documentation before activating.
Impact: Without this, GroupOrchestrationService.createGroup() for WhatsApp channel will fail with auth errors.
4b. Telegram Bot
Status: TelegramGroupService works for messaging. Group creation is impossible via Bot API (known limitation).
Workaround already implemented: Uses forum topics in existing supergroups instead of creating new groups. This is acceptable for now.
To activate: Set TELEGRAM_BOT_TOKEN in env. Register webhook via:
curl -X POST https://api.telegram.org/bot<TOKEN>/setWebhook \
-d "url=https://<domain>/vitto-api/engine/integrations/telegram/webhook"
4c. Slack App
Status: Webhook handler exists. SlackChannelManagerService has full CRUD for channels.
To activate: Create a Slack App in the Slack API dashboard. Set:
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...
Configure Event Subscriptions URL: https://<domain>/vitto-api/engine/integrations/slack/events
Configure Interactivity URL: https://<domain>/vitto-api/engine/integrations/slack/interactions (NEW — needed for button callbacks)
4d. Discord Bot
Status: Webhook handler exists. Basic embed formatting works.
To activate: Create a Discord Application. Set:
DISCORD_BOT_TOKEN=...
DISCORD_APPLICATION_ID=...
Configure Interactions Endpoint URL: https://<domain>/vitto-api/engine/integrations/discord/webhook
End-to-End Flow Verification
After all layers are complete, these flows must work:
Flow 1: Vitto creates a Slack group and posts to it
User (web chat): "Create a Slack channel called #deploy-alerts and post the latest deploy status"
→ Vitto agent loop → manage_group tool (action: create, channelType: slack)
→ GroupOrchestrationService.createGroup() → SlackChannelManagerService.createChannel()
→ Slack API creates #deploy-alerts
→ manage_group tool (action: post, message: "Deploy v2.3.1 completed...")
→ GroupOrchestrationService.postUpdate() → Slack API sends message
→ Vitto responds: "Created #deploy-alerts and posted the deploy status."
Flow 2: WhatsApp user messages Vitto, sees reply + quick actions
User (WhatsApp): "What's the status of my deploy?"
→ Twilio webhook → vitto.controller.ts → handleTwilioWhatsAppInbound()
→ dedup check → engine.submitTask(type: 'channel-message', replyTo: 'whatsapp')
→ ChannelProcessingService.processChannelMessage() → agent loop
→ routeTaskResponse() → channelGateway.sendResponse(channelType: 'whatsapp')
→ TwilioService.sendWhatsAppMessage()
→ WhatsApp adapter → sendWhatsAppQuickReply() with ["View logs", "Redeploy", "Details"]
→ User sees response + 3 quick reply buttons
→ User taps "View logs" → new webhook → engine.submitTask()
→ Same flow, Vitto replies with logs
Flow 3: Telegram user presses inline button
User (Telegram): "Show me my tasks"
→ Telegram webhook → handleTelegramWebhook() → engine.submitTask()
→ Agent loop → response with TelegramAdapter → inline keyboard buttons
→ User presses "Mark as done" button
→ Telegram callback_query webhook → handleTelegramWebhook()
→ answerCallbackQuery() (removes spinner)
→ engine.submitTask(summary: "mark_task_done:task_123")
→ Agent loop → marks task done → replies "Done!"
Flow 4: Group chat appears in web UI
User (Slack #deploy-alerts): "Is the rollback ready?"
→ Slack webhook → slackEvents() → engine.submitTask()
→ processChannelMessage() → agent loop → response
→ routeTaskResponse() → sendResponse(channelType: 'slack') ✓
→ persistToChannelSession() → bridgeMessageToAiApi() → ai-api
→ ChatSession updated → Socket.IO broadcast to web clients
→ Web UI shows message with Slack badge in conversation timeline
Flow 5: Whistle user into conversation
User A (web chat): "Can you get @bob to help with this?"
→ Agent loop → whistle_user tool
→ WhistleOrchestratorService.whistle()
→ Check space membership ✓
→ Check whistle preferences ✓
→ Upgrade session in ai-api (add Bob as participant)
→ ProactiveDispatcher → notify Bob via preferred channel
→ Bob joins conversation, sees full/summary history based on historyAccess
Files to Create
| File | Purpose |
|---|---|
APIs/backend-vitto-api/src/app/engine/adapters/slack-channel.adapter.ts |
Slack Block Kit adapter |
APIs/backend-vitto-api/src/app/engine/adapters/telegram-channel.adapter.ts |
Telegram inline keyboard adapter |
APIs/backend-vitto-api/src/app/engine/adapters/discord-channel.adapter.ts |
Discord components adapter |
| Frontend group list component (TBD location) | Group management UI |
| Frontend create group dialog (TBD location) | Create group form |
| Frontend whistle notification (TBD location) | Whistle toast/modal |
Files to Modify
| File | Change | Layer |
|---|---|---|
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts |
Inject GroupOrchestrationService + ChannelGatewayService, fix lines 503-504 |
1 |
APIs/backend-vitto-api/src/app/engine/engine.module.ts |
Register 3 new adapters in CHANNEL_ADAPTERS factory |
2 |
APIs/backend-vitto-api/src/app/connectivity/channel-gateway.service.ts |
Handle callback_query in handleTelegramWebhook() |
2 |
APIs/backend-vitto-api/src/app/engine/vitto.controller.ts |
Add POST /engine/integrations/slack/interactions endpoint |
2 |
libs/libs-api/messaging-core/src/lib/services/telegram-group.service.ts |
Add answerCallbackQuery(), sendMessageWithKeyboard() |
2 |
deployment/shared/.env.example |
Document all channel env vars | 4 |
Risk Assessment
| Risk | Severity | Confidence | Mitigation |
|---|---|---|---|
| Layer 1 injection breaks existing tools | Low | Certain it won't — @Optional() + existing pattern |
Verify all tools still register after change |
| Slack interaction URL not configured | Medium | Certain — buttons won't work without it | Document in setup checklist, fail gracefully |
| WhatsApp Cloud API credential mismatch with Twilio WABA | Medium | Likely — need to verify they can coexist | Test on staging before production |
| Telegram Bot API rate limits on group operations | Low | Speculative — unlikely at our scale | TelegramGroupService already has rate limiter |
| Frontend scope creep on group UI | Medium | Likely | Strict YAGNI — list, create, post. Nothing else in v1. |
What's Genuinely Solid (No Changes Needed)
- ChannelGatewayService —
sendResponse()routes correctly to all 5 channels. Well-structured switch, proper error handling, DLQ fallback. - Channel webhook endpoints — all 5 exist in
vitto.controller.ts, properly dedup-guarded (WhatsApp), signature-verified (WhatsApp, Slack), and fire-and-forget to engine. - ChannelProcessingService — the fast conversational path (
processChannelMessage()) handles intent classification, agent loop, rich message composition, response routing, memory persistence, and quality gates. This is production-quality code. - Cross-channel bridge —
persistToChannelSession()→bridgeMessageToAiApi()correctly bridges external channel messages to ai-api for web UI visibility. Already wired and fire-and-forget. - GroupOrchestrationService — real API calls to WhatsApp Cloud, Telegram Bot, Slack Web API. CRUD + persistence. Properly structured.
- WhistleOrchestratorService — permission checks, session upgrade, proactive delivery. Well-designed.
- ProactiveDispatcher — multi-channel dispatch with safety guards. Solid.
Implementation Priority
Layer 1 (hours): Fix 2 lines → unlock manage_group + send_message tools
↓
Layer 2 (2-3d): Channel adapters + interaction handlers
↓
Layer 3 (3-5d): Frontend group UI + whistle notifications
↓
Layer 4 (config): Activate credentials per channel
Layer 1 should be done immediately — it's the highest leverage change in this entire spec. Two lines of code unlock the entire group and messaging tool system that's been dormant since S174.