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

What's broken (verified):

  1. manage_group tool → always fails. tool-registry.service.ts:503 sets groupOrchestration: undefined. Vitto literally cannot create groups, post to groups, or manage participants. The tool exists, the service exists, the injection is missing.

  2. send_message tool → always fails. tool-registry.service.ts:504 sets channelGateway: undefined. Vitto cannot proactively send messages via any channel through the tool system.

  3. 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.

  4. Telegram: no inline keyboards or callback handling. Vitto sends text-only replies. No buttons, no interactive menus, no callback_query processing.

  5. Discord: no button/modal support. Only embeds. No component interactions.

  6. Slack: no Block Kit interactive components. Text + basic formatting only. No button actions, no modals.

  7. No channel adapters for Slack, Discord, Telegram. Only WhatsApp, Web, and Teams have registered adapters. ProactiveDispatcher falls back to plain text for other channels.

  8. WhatsApp Cloud API credentials not configured. WhatsAppCloudApiService exists and calls Meta Graph API v21.0, but env vars (WHATSAPP_CLOUD_PHONE_NUMBER_ID, etc.) are empty. This blocks group creation on WhatsApp.

  9. 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:


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:

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:

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:

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

API calls (all existing endpoints in vitto.controller.ts):

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:

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:

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)


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.