The file E:\genesis-system\HANDOFF.md has been updated successfully.

## Critical Blockers / Next Steps

### 1. ✅ Redis Connection FIXED
- **Issue**: `telegram_bridge.py` was using `localhost:6379` instead of Elestio cloud.
- **Fix Applied**: Updated `core/voice/telegram_bridge.py` to use `elestio_config` (same as ClawdBot).
- **Verified**: Both ClawdBot and VoiceBridge now connect to `redis-genesis-u50607.vm.elestio.app:26379`.

# Genesis Session Handoff

## Last Session Summary
- **Date**: 2026-01-27
- **Duration**: Ongoing
- **Primary Focus**: Real-Time Voice Activation (Telegram Bridge + ElevenLabs)

## Completed This Session

### 🗣 Real-Time Voice (Walkie-Talkie Mode)
- [x] **Upgraded Voice Bridge**: `core/voice/telegram_bridge.py` now uses **ElevenLabs** (not gTTS).
- [x] **Premium Voice**: Connected "Australian Female" voice (`pFZP5JQG7iQjIQuC4Bku`) for Aiva.
- [x] **Telegram Bot Live**: Configured `ClawdBot` with new token.
- [x] **Architecture**: `Telegram Voice Note` → `Gemini 2.5 Flash` (Think) → `ElevenLabs` (Speak) → `Telegram Voice Note`.

### 🔑 Credentials & Config
- [x] **Telegram Token**: Added `TELEGRAM_BOT_TOKEN` to `.env`.
- [x] **ElevenLabs Key**: Added `ELEVENLABS_API_KEY` to `.env`.
- [x] **Config File**: Verified `AIVA/voice/voice_config.py`.

---

## Critical Blockers / Next Steps

### 1. 🛑 Redis Connection Refused
- **Issue**: `telegram_bridge.py` failed with `Error 10061 connecting to localhost:6379`.
- **Impact**: Voice bridge cannot receive messages from ClawdBot.
- **Fix**: Restart Redis service or verify `REDIS_HOST` in `.env` (currently set to Elestio URL, might need local tunneling or direct connection).

### 2. ⏳ Telnyx Verification
- **Status**: API Key valid, but phone number/account requires verification for PSTN calls.
- **Workaround**: Using Telegram Bridge (Walkie Talkie logic) until resolved.

---

## Key Files
| File | Purpose |
|------|---------|
| `core/voice/telegram_bridge.py` | **MAIN ENGINE**: Handles Voice->AI->Voice loop |
| `e:\genesis-system\.env` | **KEYS**: Contains Telegram & ElevenLabs keys |
| `ClawdBot/main.py` | **BOT**: Telegram interface (must be running) |
| `AIVA/voice/voice_config.py` | **CONFIG**: Voice settings & IDs |

---

## Quick Start (Voice Bridge)
1. **Ensure Redis is accessible** (Check docker or tunnel).
2. **Start ClawdBot**:
   ```powershell
   python ClawdBot/main.py
   ```
3. **Start Bridge**:
   ```powershell
   python core/voice/telegram_bridge.py
   ```
4. **Test**: Send "Call me" to `@ClawdBot` on Telegram.

---

## Brand Architecture (Unchanged)
```
AgileAdapt (PARENT)
└── ReceptionistAI (PRODUCT)
    └── Maya (Agent)
```

-- **Issue**: `telegram_bridge.py` failed with `Error 10061 connecting to localhost:6379`.

-- **Impact**: Voice bridge cannot receive messages from ClawdBot.

-- **Fix**: Restart Redis service or verify `REDIS_HOST` in `.env` (currently set to Elestio URL, might need local tunneling or direct connection).

+- **Issue**: `telegram_bridge.py` was using `localhost:6379` instead of Elestio cloud.

+- **Fix Applied**: Updated `core/voice/telegram_bridge.py` to use `elestio_config` (same as ClawdBot).

+- **Verified**: Both ClawdBot and VoiceBridge now connect to `redis-genesis-u50607.vm.elestio.app:26379`.


## Summary

**Fixed the Redis connection issue.** The problem was:

| Component | Before | After |
|-----------|--------|-------|
| `telegram_bridge.py` | `localhost:6379` (wrong) | `redis-genesis-u50607.vm.elestio.app:26379` |
| `ClawdBot/redis_client_v2.py` | Elestio cloud (correct) | No change needed |

**Changes made:**
1. **`core/voice/telegram_bridge.py`** - Now imports `elestio_config` and uses Elestio Redis cloud with proper ACL auth (username + password)
2. **`.env`** - Added `GENESIS_REDIS_*` environment variables as backup

**To start ClawdBot now:**
```powershell
python ClawdBot/main.py
```

**To start Voice Bridge:**
```powershell
python core/voice/telegram_bridge.py
```

Both will connect to the same Elestio Redis instance and can communicate via pub/sub channels.


Output:
2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.

2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.


Output:
2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.


Output:
2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.


Output:
2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.


Output:
2026-01-27 16:54:59,582 - VoiceBridge - INFO - Using Elestio Redis: redis-genesis-u50607.vm.elestio.app:26379
2026-01-27 16:54:59,583 - google_genai._api_client - WARNING - Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY.


cant you just get clawbot to send me a text link to initiate a voice call?


Good idea. Let me check how the Telegram skill works and add a voice call initiation feature.


     1→"""
     2→ClawdBot Telegram Skill
     3→========================
     4→
     5→Non-blocking Telegram bot integration for Genesis system.
     6→
     7→Features:
     8→- Async-compatible with main event loop
     9→- Voice note reception and forwarding
    10→- Text message handling
    11→- Graceful shutdown support
    12→
    13→Usage:
    14→    The TelegramSkill is initialized by ClawdBot main.py if TELEGRAM_BOT_TOKEN
    15→    is set in the environment. Messages are forwarded to Genesis via Redis.
    16→"""
    17→
    18→import logging
    19→import os
    20→import asyncio
    21→from datetime import datetime, timezone
    22→from typing import Optional, Dict, Any
    23→from telegram import Update
    24→from telegram.ext import (
    25→    ApplicationBuilder,
    26→    ContextTypes,
    27→    CommandHandler,
    28→    MessageHandler,
    29→    filters
    30→)
    31→
    32→# Use the new enterprise Redis client
    33→import sys
    34→sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    35→from ClawdBot.redis_client_v2 import redis_client
    36→
    37→logger = logging.getLogger("ClawdBotTelegram")
    38→
    39→CH_OBSERVATIONS = "genesis:observations"
    40→CH_COMMANDS = "genesis:commands"
    41→
    42→
    43→class TelegramSkill:
    44→    """
    45→    Non-blocking Telegram bot skill for ClawdBot.
    46→
    47→    Key improvements over previous version:
    48→    - Uses async polling that doesn't block the event loop
    49→    - Proper graceful shutdown handling
    50→    - Uses enterprise Redis client (v2)
    51→    - Better error handling and logging
    52→    """
    53→
    54→    def __init__(self):
    55→        self.token = os.getenv("TELEGRAM_BOT_TOKEN")
    56→        self.app = None
    57→        self._running = False
    58→
    59→        if not self.token:
    60→            logger.warning("TELEGRAM_BOT_TOKEN not found. Telegram Skill disabled.")
    61→            return
    62→
    63→        try:
    64→            self.app = ApplicationBuilder().token(self.token).build()
    65→            self._setup_handlers()
    66→            logger.info("Telegram Skill initialized")
    67→        except Exception as e:
    68→            logger.error(f"Failed to initialize Telegram: {e}")
    69→            self.app = None
    70→
    71→    def _setup_handlers(self):
    72→        """Register message handlers."""
    73→        if not self.app:
    74→            return
    75→
    76→        # Command handlers
    77→        self.app.add_handler(CommandHandler('start', self._handle_start))
    78→        self.app.add_handler(CommandHandler('status', self._handle_status))
    79→        self.app.add_handler(CommandHandler('help', self._handle_help))
    80→
    81→        # Message handlers
    82→        self.app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
    83→        self.app.add_handler(MessageHandler(filters.AUDIO, self._handle_audio))
    84→        self.app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
    85→        self.app.add_handler(MessageHandler(
    86→            filters.TEXT & (~filters.COMMAND),
    87→            self._handle_text
    88→        ))
    89→
    90→        # Error handler
    91→        self.app.add_error_handler(self._handle_error)
    92→
    93→    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
    94→        """Handle /start command."""
    95→        user = update.effective_user
    96→        await context.bot.send_message(
    97→            chat_id=update.effective_chat.id,
    98→            text=(
    99→                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
   100→                "I can receive:\n"
   101→                "- Voice messages (forwarded to Genesis)\n"
   102→                "- Text commands\n"
   103→                "- Screenshots\n\n"
   104→                "Use /help for available commands."
   105→            )
   106→        )
   107→
   108→        # Log connection
   109→        self._publish_observation("system", {
   110→            "event": "telegram_user_connected",
   111→            "user_id": user.id,
   112→            "username": user.username,
   113→            "first_name": user.first_name
   114→        })
   115→
   116→    async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   117→        """Handle /status command."""
   118→        # Check Redis connection
   119→        redis_ok = redis_client.is_connected()
   120→
   121→        status_text = (
   122→            "**ClawdBot Status**\n"
   123→            f"- Redis: {'Connected' if redis_ok else 'Disconnected'}\n"
   124→            f"- Telegram: Connected\n"
   125→            f"- Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
   126→        )
   127→
   128→        await context.bot.send_message(
   129→            chat_id=update.effective_chat.id,
   130→            text=status_text,
   131→            parse_mode='Markdown'
   132→        )
   133→
   134→    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   135→        """Handle /help command."""
   136→        help_text = (
   137→            "**ClawdBot Commands**\n\n"
   138→            "/start - Initialize connection\n"
   139→            "/status - Check system status\n"
   140→            "/help - Show this help\n\n"
   141→            "**Message Types**\n"
   142→            "- Send voice notes for processing\n"
   143→            "- Send text messages as commands\n"
   144→            "- Send photos for analysis"
   145→        )
   146→
   147→        await context.bot.send_message(
   148→            chat_id=update.effective_chat.id,
   149→            text=help_text,
   150→            parse_mode='Markdown'
   151→        )
   152→
   153→    async def _handle_voice(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   154→        """Handle voice notes."""
   155→        voice_file = await update.message.voice.get_file()
   156→        user = update.effective_user
   157→
   158→        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")
   159→
   160→            "mime_type": update.message.voice.mime_type
   161→        })
   162→
   163→        # Download file locally
   164→        try:
   165→            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
   166→            save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "voice_input")
   167→            os.makedirs(save_dir, exist_ok=True)
   168→
   169→            file_path = os.path.join(save_dir, f"voice_{user.id}_{timestamp}.ogg")
   170→            await voice_file.download_to_drive(file_path)
   171→
   172→            # Update observation with file path
   173→            self._publish_observation("voice_note", {
   174→                "user_id": user.id,
   175→                "username": user.username,
   176→                "first_name": user.first_name,
   177→                "file_path": file_path,  # Critical for processing
   178→                "file_id": voice_file.file_id,
   179→                "duration": update.message.voice.duration,
   180→                "chat_id": update.effective_chat.id
   181→            })
   182→
   183→            logger.info(f"Voice note saved to {file_path}")
   184→
   185→        except Exception as e:
   186→            logger.error(f"Failed to download voice note: {e}")
   187→
   188→        await context.bot.send_message(
   189→            chat_id=update.effective_chat.id,
   190→            text="Voice note received. Transmitting to Genesis Cortex..."
   191→        )
   192→
   193→    async def _handle_audio(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   194→        """Handle audio files."""
   195→        audio = update.message.audio
   196→        audio_file = await audio.get_file()
   197→        user = update.effective_user
   198→
   199→        logger.info(f"Received audio from {user.first_name}: {audio.title or 'Untitled'}")
   200→
   201→        self._publish_observation("audio", {
   202→            "user_id": user.id,
   203→            "first_name": user.first_name,
   204→            "file_id": audio_file.file_id,
   205→            "title": audio.title,
   206→            "performer": audio.performer,
   207→            "duration": audio.duration,
   208→            "mime_type": audio.mime_type
   209→        })
   210→
   211→        await context.bot.send_message(
   212→            chat_id=update.effective_chat.id,
   213→            text="Audio received and forwarded to Genesis."
   214→        )
   215→
   216→    async def _handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   217→        """Handle photos."""
   218→        # Get the largest photo
   219→        photo = update.message.photo[-1]
   220→        photo_file = await photo.get_file()
   221→        user = update.effective_user
   222→
   223→        logger.info(f"Received photo from {user.first_name} ({photo.width}x{photo.height})")
   224→
   225→        self._publish_observation("photo", {
   226→            "user_id": user.id,
   227→            "first_name": user.first_name,
   228→            "file_id": photo_file.file_id,
   229→            "width": photo.width,
   230→            "height": photo.height,
   231→            "file_size": photo.file_size
   232→        })
   233→
   234→        await context.bot.send_message(
   235→            chat_id=update.effective_chat.id,
   236→            text="Photo received and forwarded for analysis."
   237→        )
   238→
   239→    async def _handle_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
   240→        """Handle text messages."""
   241→        user = update.effective_user
   242→        text = update.message.text
   243→
   244→        logger.info(f"Received text from {user.first_name}: {text[:50]}...")
   245→
   246→        self._publish_observation("text_message", {
   247→            "user_id": user.id,
   248→            "username": user.username,
   249→            "first_name": user.first_name,
   250→            "text": text,
   251→            "chat_id": update.effective_chat.id
   252→        })
   253→
   254→        # Check if it looks like a command for Genesis
   255→        if text.startswith("!") or text.startswith("/genesis"):
   256→            await context.bot.send_message(
   257→                chat_id=update.effective_chat.id,
   258→                text="Command forwarded to Genesis orchestrator."
   259→            )
   260→        else:
   261→            await context.bot.send_message(
   262→                chat_id=update.effective_chat.id,
   263→                text="Message received and logged."
   264→            )
   265→
   266→    async def _handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE):
   267→        """Handle errors."""
   268→        logger.error(f"Telegram error: {context.error}")
   269→
   270→        self._publish_observation("error", {
   271→            "error": str(context.error),
   272→            "update": str(update) if update else None
   273→        })
   274→
   275→    def _publish_observation(self, obs_type: str, data: dict):
   276→        """Publish observation to Redis."""
   277→        try:
   278→            redis_client.publish(CH_OBSERVATIONS, {
   279→                "source": "telegram",
   280→                "type": obs_type,
   281→                "timestamp": datetime.now(timezone.utc).isoformat(),
   282→                **data
   283→            })
   284→        except Exception as e:
   285→            logger.error(f"Failed to publish observation: {e}")
   286→
   287→    async def start_async(self):
   288→        """
   289→        Start the Telegram bot in a non-blocking way.
   290→
   291→        This method initializes and starts polling without blocking
   292→        the main asyncio event loop.
   293→        """
   294→        if not self.app:
   295→            logger.warning("Telegram app not initialized, cannot start")
   296→            return
   297→
   298→        self._running = True
   299→
   300→        try:
   301→            await self.app.initialize()
   302→            await self.app.start()
   303→            await self.app.updater.start_polling(
   304→                allowed_updates=Update.ALL_TYPES,
   305→                drop_pending_updates=True
   306→            )
   307→            logger.info("Telegram bot polling started (non-blocking)")
   308→        except Exception as e:
   309→            logger.error(f"Failed to start Telegram polling: {e}")
   310→            self._running = False
   311→
   312→    async def stop_async(self):
   313→        """Stop the Telegram bot gracefully."""
   314→        if not self.app or not self._running:
   315→            return
   316→
   317→        self._running = False
   318→
   319→        try:
   320→            await self.app.updater.stop()
   321→            await self.app.stop()
   322→            await self.app.shutdown()
   323→            logger.info("Telegram bot stopped gracefully")
   324→        except Exception as e:
   325→            logger.error(f"Error stopping Telegram: {e}")
   326→
   327→    def run(self):
   328→        """
   329→        Blocking run for standalone execution.
   330→
   331→        Note: This blocks the event loop. Use start_async() for
   332→        integration with ClawdBot main event loop.
   333→        """
   334→        if not self.app:
   335→            logger.error("Cannot run - Telegram app not initialized")
   336→            return
   337→
   338→        logger.info("Starting Telegram Bot Polling (blocking mode)...")
   339→        self.app.run_polling()
   340→
   341→    @property
   342→    def is_running(self) -> bool:
   343→        """Check if bot is running."""
   344→        return self._running
   345→
   346→    async def execute(self, action: str, args: Dict[str, Any]):
   347→        """
   348→        Execute a command from the Skills Manager.
   349→
   350→        Actions:
   351→        - send_message: Send a text message
   352→        - send_voice: Send a voice note (OGG/MP3)
   353→        """
   354→        if not self.app:
   355→            logger.warning("Telegram not initialized, cannot execute command")
   356→            return
   357→
   358→        chat_id = args.get("chat_id")
   359→        if not chat_id:
   360→            # Try to get last chat_id from Redis or default?
   361→            # For now, require chat_id.
   362→            logger.error("Missing chat_id for Telegram command")
   363→            return
   364→
   365→        try:
   366→            if action == "send_message":
   367→                text = args.get("text")
   368→                if text:
   369→                    await self.app.bot.send_message(chat_id=chat_id, text=text)
   370→                    logger.info(f"Sent message to {chat_id}: {text[:20]}...")
   371→
   372→            elif action == "send_voice":
   373→                file_path = args.get("file_path")
   374→                if file_path and os.path.exists(file_path):
   375→                    with open(file_path, 'rb') as voice:
   376→                        await self.app.bot.send_voice(chat_id=chat_id, voice=voice)
   377→                    logger.info(f"Sent voice note to {chat_id}: {file_path}")
   378→                else:
   379→                    logger.error(f"Voice file not found: {file_path}")
   380→
   381→            else:
   382→                logger.warning(f"Unknown Telegram action: {action}")
   383→
   384→        except Exception as e:
   385→            logger.error(f"Failed to execute Telegram action {action}: {e}")
   386→
   387→
   388→# Standalone runner for testing
   389→if __name__ == "__main__":
   390→    from dotenv import load_dotenv
   391→    load_dotenv()
   392→
   393→    logging.basicConfig(
   394→        level=logging.INFO,
   395→        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
   396→    )
   397→
   398→    skill = TelegramSkill()
   399→    if skill.app:
   400→        skill.run()
   401→    else:
   402→        print("Failed to initialize Telegram skill. Check TELEGRAM_BOT_TOKEN.")
   403→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
ClawdBot Telegram Skill
========================

Non-blocking Telegram bot integration for Genesis system.

Features:
- Async-compatible with main event loop
- Voice note reception and forwarding
- Text message handling
- Graceful shutdown support

Usage:
    The TelegramSkill is initialized by ClawdBot main.py if TELEGRAM_BOT_TOKEN
    is set in the environment. Messages are forwarded to Genesis via Redis.
"""

import logging
import os
import asyncio
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CommandHandler,
    MessageHandler,
    filters
)

# Use the new enterprise Redis client
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ClawdBot.redis_client_v2 import redis_client

logger = logging.getLogger("ClawdBotTelegram")

CH_OBSERVATIONS = "genesis:observations"
CH_COMMANDS = "genesis:commands"

class TelegramSkill:
    """
    Non-blocking Telegram bot skill for ClawdBot.

    Key improvements over previous version:
    - Uses async polling that doesn't block the event loop
    - Proper graceful shutdown handling
    - Uses enterprise Redis client (v2)
    - Better error handling and logging
    """

    def __init__(self):
        self.token = os.getenv("TELEGRAM_BOT_TOKEN")
        self.app = None
        self._running = False

        if not self.token:
            logger.warning("TELEGRAM_BOT_TOKEN not found. Telegram Skill disabled.")
            return

        try:
            self.app = ApplicationBuilder().token(self.token).build()
            self._setup_handlers()
            logger.info("Telegram Skill initialized")
        except Exception as e:
            logger.error(f"Failed to initialize Telegram: {e}")
            self.app = None

    def _setup_handlers(self):
        """Register message handlers."""
        if not self.app:
            return

        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))

        # Message handlers
        self.app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
        self.app.add_handler(MessageHandler(filters.AUDIO, self._handle_audio))
        self.app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
        self.app.add_handler(MessageHandler(
            filters.TEXT & (~filters.COMMAND),
            self._handle_text
        ))

        # Error handler
        self.app.add_error_handler(self._handle_error)

    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "Use /help for available commands."
            )
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name
        })

    async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /status command."""
        # Check Redis connection
        redis_ok = redis_client.is_connected()

        status_text = (
            "**ClawdBot Status**\n"
            f"- Redis: {'Connected' if redis_ok else 'Disconnected'}\n"
            f"- Telegram: Connected\n"
            f"- Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=status_text,
            parse_mode='Markdown'
        )

    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_voice(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle voice notes."""
        voice_file = await update.message.voice.get_file()
        user = update.effective_user

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

            "mime_type": update.message.voice.mime_type
        })

        # Download file locally
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "voice_input")
            os.makedirs(save_dir, exist_ok=True)

            file_path = os.path.join(save_dir, f"voice_{user.id}_{timestamp}.ogg")
            await voice_file.download_to_drive(file_path)

            # Update observation with file path
            self._publish_observation("voice_note", {
                "user_id": user.id,
                "username": user.username,
                "first_name": user.first_name,
                "file_path": file_path,  # Critical for processing
                "file_id": voice_file.file_id,
                "duration": update.message.voice.duration,
                "chat_id": update.effective_chat.id
            })

            logger.info(f"Voice note saved to {file_path}")

        except Exception as e:
            logger.error(f"Failed to download voice note: {e}")

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Voice note received. Transmitting to Genesis Cortex..."
        )

    async def _handle_audio(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle audio files."""
        audio = update.message.audio
        audio_file = await audio.get_file()
        user = update.effective_user

        logger.info(f"Received audio from {user.first_name}: {audio.title or 'Untitled'}")

        self._publish_observation("audio", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": audio_file.file_id,
            "title": audio.title,
            "performer": audio.performer,
            "duration": audio.duration,
            "mime_type": audio.mime_type
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Audio received and forwarded to Genesis."
        )

    async def _handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle photos."""
        # Get the largest photo
        photo = update.message.photo[-1]
        photo_file = await photo.get_file()
        user = update.effective_user

        logger.info(f"Received photo from {user.first_name} ({photo.width}x{photo.height})")

        self._publish_observation("photo", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": photo_file.file_id,
            "width": photo.width,
            "height": photo.height,
            "file_size": photo.file_size
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Photo received and forwarded for analysis."
        )

    async def _handle_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle text messages."""
        user = update.effective_user
        text = update.message.text

        logger.info(f"Received text from {user.first_name}: {text[:50]}...")

        self._publish_observation("text_message", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "text": text,
            "chat_id": update.effective_chat.id
        })

        # Check if it looks like a command for Genesis
        if text.startswith("!") or text.startswith("/genesis"):
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Command forwarded to Genesis orchestrator."
            )
        else:
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Message received and logged."
            )

    async def _handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE):
        """Handle errors."""
        logger.error(f"Telegram error: {context.error}")

        self._publish_observation("error", {
            "error": str(context.error),
            "update": str(update) if update else None
        })

    def _publish_observation(self, obs_type: str, data: dict):
        """Publish observation to Redis."""
        try:
            redis_client.publish(CH_OBSERVATIONS, {
                "source": "telegram",
                "type": obs_type,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                **data
            })
        except Exception as e:
            logger.error(f"Failed to publish observation: {e}")

    async def start_async(self):
        """
        Start the Telegram bot in a non-blocking way.

        This method initializes and starts polling without blocking
        the main asyncio event loop.
        """
        if not self.app:
            logger.warning("Telegram app not initialized, cannot start")
            return

        self._running = True

        try:
            await self.app.initialize()
            await self.app.start()
            await self.app.updater.start_polling(
                allowed_updates=Update.ALL_TYPES,
                drop_pending_updates=True
            )
            logger.info("Telegram bot polling started (non-blocking)")
        except Exception as e:
            logger.error(f"Failed to start Telegram polling: {e}")
            self._running = False

    async def stop_async(self):
        """Stop the Telegram bot gracefully."""
        if not self.app or not self._running:
            return

        self._running = False

        try:
            await self.app.updater.stop()
            await self.app.stop()
            await self.app.shutdown()
            logger.info("Telegram bot stopped gracefully")
        except Exception as e:
            logger.error(f"Error stopping Telegram: {e}")

    def run(self):
        """
        Blocking run for standalone execution.

        Note: This blocks the event loop. Use start_async() for
        integration with ClawdBot main event loop.
        """
        if not self.app:
            logger.error("Cannot run - Telegram app not initialized")
            return

        logger.info("Starting Telegram Bot Polling (blocking mode)...")
        self.app.run_polling()

    @property
    def is_running(self) -> bool:
        """Check if bot is running."""
        return self._running

    async def execute(self, action: str, args: Dict[str, Any]):
        """
        Execute a command from the Skills Manager.

        Actions:
        - send_message: Send a text message
        - send_voice: Send a voice note (OGG/MP3)
        """
        if not self.app:
            logger.warning("Telegram not initialized, cannot execute command")
            return

        chat_id = args.get("chat_id")
        if not chat_id:
            # Try to get last chat_id from Redis or default?
            # For now, require chat_id.
            logger.error("Missing chat_id for Telegram command")
            return

        try:
            if action == "send_message":
                text = args.get("text")
                if text:
                    await self.app.bot.send_message(chat_id=chat_id, text=text)
                    logger.info(f"Sent message to {chat_id}: {text[:20]}...")

            elif action == "send_voice":
                file_path = args.get("file_path")
                if file_path and os.path.exists(file_path):
                    with open(file_path, 'rb') as voice:
                        await self.app.bot.send_voice(chat_id=chat_id, voice=voice)
                    logger.info(f"Sent voice note to {chat_id}: {file_path}")
                else:
                    logger.error(f"Voice file not found: {file_path}")

            else:
                logger.warning(f"Unknown Telegram action: {action}")

        except Exception as e:
            logger.error(f"Failed to execute Telegram action {action}: {e}")

# Standalone runner for testing
if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv()

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    skill = TelegramSkill()
    if skill.app:
        skill.run()
    else:
        print("Failed to initialize Telegram skill. Check TELEGRAM_BOT_TOKEN.")


I see there's a syntax error in the file (lines 160-161 are orphaned). Let me fix that and add a `/call` command that sends you a link to initiate voice mode.


Edit

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

            "mime_type": update.message.voice.mime_type
        })

        # Download file locally

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

        # Download file locally


The file E:\genesis-system\ClawdBot\skills\telegram_bot.py has been updated successfully.

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

        # Download file locally

"""
ClawdBot Telegram Skill
========================

Non-blocking Telegram bot integration for Genesis system.

Features:
- Async-compatible with main event loop
- Voice note reception and forwarding
- Text message handling
- Graceful shutdown support

Usage:
    The TelegramSkill is initialized by ClawdBot main.py if TELEGRAM_BOT_TOKEN
    is set in the environment. Messages are forwarded to Genesis via Redis.
"""

import logging
import os
import asyncio
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CommandHandler,
    MessageHandler,
    filters
)

# Use the new enterprise Redis client
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ClawdBot.redis_client_v2 import redis_client

logger = logging.getLogger("ClawdBotTelegram")

CH_OBSERVATIONS = "genesis:observations"
CH_COMMANDS = "genesis:commands"

class TelegramSkill:
    """
    Non-blocking Telegram bot skill for ClawdBot.

    Key improvements over previous version:
    - Uses async polling that doesn't block the event loop
    - Proper graceful shutdown handling
    - Uses enterprise Redis client (v2)
    - Better error handling and logging
    """

    def __init__(self):
        self.token = os.getenv("TELEGRAM_BOT_TOKEN")
        self.app = None
        self._running = False

        if not self.token:
            logger.warning("TELEGRAM_BOT_TOKEN not found. Telegram Skill disabled.")
            return

        try:
            self.app = ApplicationBuilder().token(self.token).build()
            self._setup_handlers()
            logger.info("Telegram Skill initialized")
        except Exception as e:
            logger.error(f"Failed to initialize Telegram: {e}")
            self.app = None

    def _setup_handlers(self):
        """Register message handlers."""
        if not self.app:
            return

        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))

        # Message handlers
        self.app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
        self.app.add_handler(MessageHandler(filters.AUDIO, self._handle_audio))
        self.app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
        self.app.add_handler(MessageHandler(
            filters.TEXT & (~filters.COMMAND),
            self._handle_text
        ))

        # Error handler
        self.app.add_error_handler(self._handle_error)

    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "Use /help for available commands."
            )
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name
        })

    async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /status command."""
        # Check Redis connection
        redis_ok = redis_client.is_connected()

        status_text = (
            "**ClawdBot Status**\n"
            f"- Redis: {'Connected' if redis_ok else 'Disconnected'}\n"
            f"- Telegram: Connected\n"
            f"- Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=status_text,
            parse_mode='Markdown'
        )

    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_voice(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle voice notes."""
        voice_file = await update.message.voice.get_file()
        user = update.effective_user

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

            "mime_type": update.message.voice.mime_type
        })

        # Download file locally
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "voice_input")
            os.makedirs(save_dir, exist_ok=True)

            file_path = os.path.join(save_dir, f"voice_{user.id}_{timestamp}.ogg")
            await voice_file.download_to_drive(file_path)

            # Update observation with file path
            self._publish_observation("voice_note", {
                "user_id": user.id,
                "username": user.username,
                "first_name": user.first_name,
                "file_path": file_path,  # Critical for processing
                "file_id": voice_file.file_id,
                "duration": update.message.voice.duration,
                "chat_id": update.effective_chat.id
            })

            logger.info(f"Voice note saved to {file_path}")

        except Exception as e:
            logger.error(f"Failed to download voice note: {e}")

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Voice note received. Transmitting to Genesis Cortex..."
        )

    async def _handle_audio(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle audio files."""
        audio = update.message.audio
        audio_file = await audio.get_file()
        user = update.effective_user

        logger.info(f"Received audio from {user.first_name}: {audio.title or 'Untitled'}")

        self._publish_observation("audio", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": audio_file.file_id,
            "title": audio.title,
            "performer": audio.performer,
            "duration": audio.duration,
            "mime_type": audio.mime_type
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Audio received and forwarded to Genesis."
        )

    async def _handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle photos."""
        # Get the largest photo
        photo = update.message.photo[-1]
        photo_file = await photo.get_file()
        user = update.effective_user

        logger.info(f"Received photo from {user.first_name} ({photo.width}x{photo.height})")

        self._publish_observation("photo", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": photo_file.file_id,
            "width": photo.width,
            "height": photo.height,
            "file_size": photo.file_size
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Photo received and forwarded for analysis."
        )

    async def _handle_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle text messages."""
        user = update.effective_user
        text = update.message.text

        logger.info(f"Received text from {user.first_name}: {text[:50]}...")

        self._publish_observation("text_message", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "text": text,
            "chat_id": update.effective_chat.id
        })

        # Check if it looks like a command for Genesis
        if text.startswith("!") or text.startswith("/genesis"):
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Command forwarded to Genesis orchestrator."
            )
        else:
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Message received and logged."
            )

    async def _handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE):
        """Handle errors."""
        logger.error(f"Telegram error: {context.error}")

        self._publish_observation("error", {
            "error": str(context.error),
            "update": str(update) if update else None
        })

    def _publish_observation(self, obs_type: str, data: dict):
        """Publish observation to Redis."""
        try:
            redis_client.publish(CH_OBSERVATIONS, {
                "source": "telegram",
                "type": obs_type,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                **data
            })
        except Exception as e:
            logger.error(f"Failed to publish observation: {e}")

    async def start_async(self):
        """
        Start the Telegram bot in a non-blocking way.

        This method initializes and starts polling without blocking
        the main asyncio event loop.
        """
        if not self.app:
            logger.warning("Telegram app not initialized, cannot start")
            return

        self._running = True

        try:
            await self.app.initialize()
            await self.app.start()
            await self.app.updater.start_polling(
                allowed_updates=Update.ALL_TYPES,
                drop_pending_updates=True
            )
            logger.info("Telegram bot polling started (non-blocking)")
        except Exception as e:
            logger.error(f"Failed to start Telegram polling: {e}")
            self._running = False

    async def stop_async(self):
        """Stop the Telegram bot gracefully."""
        if not self.app or not self._running:
            return

        self._running = False

        try:
            await self.app.updater.stop()
            await self.app.stop()
            await self.app.shutdown()
            logger.info("Telegram bot stopped gracefully")
        except Exception as e:
            logger.error(f"Error stopping Telegram: {e}")

    def run(self):
        """
        Blocking run for standalone execution.

        Note: This blocks the event loop. Use start_async() for
        integration with ClawdBot main event loop.
        """
        if not self.app:
            logger.error("Cannot run - Telegram app not initialized")
            return

        logger.info("Starting Telegram Bot Polling (blocking mode)...")
        self.app.run_polling()

    @property
    def is_running(self) -> bool:
        """Check if bot is running."""
        return self._running

    async def execute(self, action: str, args: Dict[str, Any]):
        """
        Execute a command from the Skills Manager.

        Actions:
        - send_message: Send a text message
        - send_voice: Send a voice note (OGG/MP3)
        """
        if not self.app:
            logger.warning("Telegram not initialized, cannot execute command")
            return

        chat_id = args.get("chat_id")
        if not chat_id:
            # Try to get last chat_id from Redis or default?
            # For now, require chat_id.
            logger.error("Missing chat_id for Telegram command")
            return

        try:
            if action == "send_message":
                text = args.get("text")
                if text:
                    await self.app.bot.send_message(chat_id=chat_id, text=text)
                    logger.info(f"Sent message to {chat_id}: {text[:20]}...")

            elif action == "send_voice":
                file_path = args.get("file_path")
                if file_path and os.path.exists(file_path):
                    with open(file_path, 'rb') as voice:
                        await self.app.bot.send_voice(chat_id=chat_id, voice=voice)
                    logger.info(f"Sent voice note to {chat_id}: {file_path}")
                else:
                    logger.error(f"Voice file not found: {file_path}")

            else:
                logger.warning(f"Unknown Telegram action: {action}")

        except Exception as e:
            logger.error(f"Failed to execute Telegram action {action}: {e}")

# Standalone runner for testing
if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv()

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    skill = TelegramSkill()
    if skill.app:
        skill.run()
    else:
        print("Failed to initialize Telegram skill. Check TELEGRAM_BOT_TOKEN.")

-            "mime_type": update.message.voice.mime_type


Now add the `/call` command:


        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))

        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))
        self.app.add_handler(CommandHandler('call', self._handle_call))


        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))
        self.app.add_handler(CommandHandler('call', self._handle_call))

"""
ClawdBot Telegram Skill
========================

Non-blocking Telegram bot integration for Genesis system.

Features:
- Async-compatible with main event loop
- Voice note reception and forwarding
- Text message handling
- Graceful shutdown support

Usage:
    The TelegramSkill is initialized by ClawdBot main.py if TELEGRAM_BOT_TOKEN
    is set in the environment. Messages are forwarded to Genesis via Redis.
"""

import logging
import os
import asyncio
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CommandHandler,
    MessageHandler,
    filters
)

# Use the new enterprise Redis client
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ClawdBot.redis_client_v2 import redis_client

logger = logging.getLogger("ClawdBotTelegram")

CH_OBSERVATIONS = "genesis:observations"
CH_COMMANDS = "genesis:commands"

class TelegramSkill:
    """
    Non-blocking Telegram bot skill for ClawdBot.

    Key improvements over previous version:
    - Uses async polling that doesn't block the event loop
    - Proper graceful shutdown handling
    - Uses enterprise Redis client (v2)
    - Better error handling and logging
    """

    def __init__(self):
        self.token = os.getenv("TELEGRAM_BOT_TOKEN")
        self.app = None
        self._running = False

        if not self.token:
            logger.warning("TELEGRAM_BOT_TOKEN not found. Telegram Skill disabled.")
            return

        try:
            self.app = ApplicationBuilder().token(self.token).build()
            self._setup_handlers()
            logger.info("Telegram Skill initialized")
        except Exception as e:
            logger.error(f"Failed to initialize Telegram: {e}")
            self.app = None

    def _setup_handlers(self):
        """Register message handlers."""
        if not self.app:
            return

        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))

        # Message handlers
        self.app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
        self.app.add_handler(MessageHandler(filters.AUDIO, self._handle_audio))
        self.app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
        self.app.add_handler(MessageHandler(
            filters.TEXT & (~filters.COMMAND),
            self._handle_text
        ))

        # Error handler
        self.app.add_error_handler(self._handle_error)

    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "Use /help for available commands."
            )
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name
        })

    async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /status command."""
        # Check Redis connection
        redis_ok = redis_client.is_connected()

        status_text = (
            "**ClawdBot Status**\n"
            f"- Redis: {'Connected' if redis_ok else 'Disconnected'}\n"
            f"- Telegram: Connected\n"
            f"- Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=status_text,
            parse_mode='Markdown'
        )

    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_voice(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle voice notes."""
        voice_file = await update.message.voice.get_file()
        user = update.effective_user

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

        # Download file locally
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "voice_input")
            os.makedirs(save_dir, exist_ok=True)

            file_path = os.path.join(save_dir, f"voice_{user.id}_{timestamp}.ogg")
            await voice_file.download_to_drive(file_path)

            # Update observation with file path
            self._publish_observation("voice_note", {
                "user_id": user.id,
                "username": user.username,
                "first_name": user.first_name,
                "file_path": file_path,  # Critical for processing
                "file_id": voice_file.file_id,
                "duration": update.message.voice.duration,
                "chat_id": update.effective_chat.id
            })

            logger.info(f"Voice note saved to {file_path}")

        except Exception as e:
            logger.error(f"Failed to download voice note: {e}")

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Voice note received. Transmitting to Genesis Cortex..."
        )

    async def _handle_audio(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle audio files."""
        audio = update.message.audio
        audio_file = await audio.get_file()
        user = update.effective_user

        logger.info(f"Received audio from {user.first_name}: {audio.title or 'Untitled'}")

        self._publish_observation("audio", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": audio_file.file_id,
            "title": audio.title,
            "performer": audio.performer,
            "duration": audio.duration,
            "mime_type": audio.mime_type
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Audio received and forwarded to Genesis."
        )

    async def _handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle photos."""
        # Get the largest photo
        photo = update.message.photo[-1]
        photo_file = await photo.get_file()
        user = update.effective_user

        logger.info(f"Received photo from {user.first_name} ({photo.width}x{photo.height})")

        self._publish_observation("photo", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": photo_file.file_id,
            "width": photo.width,
            "height": photo.height,
            "file_size": photo.file_size
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Photo received and forwarded for analysis."
        )

    async def _handle_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle text messages."""
        user = update.effective_user
        text = update.message.text

        logger.info(f"Received text from {user.first_name}: {text[:50]}...")

        self._publish_observation("text_message", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "text": text,
            "chat_id": update.effective_chat.id
        })

        # Check if it looks like a command for Genesis
        if text.startswith("!") or text.startswith("/genesis"):
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Command forwarded to Genesis orchestrator."
            )
        else:
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Message received and logged."
            )

    async def _handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE):
        """Handle errors."""
        logger.error(f"Telegram error: {context.error}")

        self._publish_observation("error", {
            "error": str(context.error),
            "update": str(update) if update else None
        })

    def _publish_observation(self, obs_type: str, data: dict):
        """Publish observation to Redis."""
        try:
            redis_client.publish(CH_OBSERVATIONS, {
                "source": "telegram",
                "type": obs_type,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                **data
            })
        except Exception as e:
            logger.error(f"Failed to publish observation: {e}")

    async def start_async(self):
        """
        Start the Telegram bot in a non-blocking way.

        This method initializes and starts polling without blocking
        the main asyncio event loop.
        """
        if not self.app:
            logger.warning("Telegram app not initialized, cannot start")
            return

        self._running = True

        try:
            await self.app.initialize()
            await self.app.start()
            await self.app.updater.start_polling(
                allowed_updates=Update.ALL_TYPES,
                drop_pending_updates=True
            )
            logger.info("Telegram bot polling started (non-blocking)")
        except Exception as e:
            logger.error(f"Failed to start Telegram polling: {e}")
            self._running = False

    async def stop_async(self):
        """Stop the Telegram bot gracefully."""
        if not self.app or not self._running:
            return

        self._running = False

        try:
            await self.app.updater.stop()
            await self.app.stop()
            await self.app.shutdown()
            logger.info("Telegram bot stopped gracefully")
        except Exception as e:
            logger.error(f"Error stopping Telegram: {e}")

    def run(self):
        """
        Blocking run for standalone execution.

        Note: This blocks the event loop. Use start_async() for
        integration with ClawdBot main event loop.
        """
        if not self.app:
            logger.error("Cannot run - Telegram app not initialized")
            return

        logger.info("Starting Telegram Bot Polling (blocking mode)...")
        self.app.run_polling()

    @property
    def is_running(self) -> bool:
        """Check if bot is running."""
        return self._running

    async def execute(self, action: str, args: Dict[str, Any]):
        """
        Execute a command from the Skills Manager.

        Actions:
        - send_message: Send a text message
        - send_voice: Send a voice note (OGG/MP3)
        """
        if not self.app:
            logger.warning("Telegram not initialized, cannot execute command")
            return

        chat_id = args.get("chat_id")
        if not chat_id:
            # Try to get last chat_id from Redis or default?
            # For now, require chat_id.
            logger.error("Missing chat_id for Telegram command")
            return

        try:
            if action == "send_message":
                text = args.get("text")
                if text:
                    await self.app.bot.send_message(chat_id=chat_id, text=text)
                    logger.info(f"Sent message to {chat_id}: {text[:20]}...")

            elif action == "send_voice":
                file_path = args.get("file_path")
                if file_path and os.path.exists(file_path):
                    with open(file_path, 'rb') as voice:
                        await self.app.bot.send_voice(chat_id=chat_id, voice=voice)
                    logger.info(f"Sent voice note to {chat_id}: {file_path}")
                else:
                    logger.error(f"Voice file not found: {file_path}")

            else:
                logger.warning(f"Unknown Telegram action: {action}")

        except Exception as e:
            logger.error(f"Failed to execute Telegram action {action}: {e}")

# Standalone runner for testing
if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv()

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    skill = TelegramSkill()
    if skill.app:
        skill.run()
    else:
        print("Failed to initialize Telegram skill. Check TELEGRAM_BOT_TOKEN.")

+        self.app.add_handler(CommandHandler('call', self._handle_call))


Now add the handler for the `/call` command:


    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/call - Start voice conversation\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_call(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /call command - initiate voice conversation."""
        user = update.effective_user
        chat_id = update.effective_chat.id

        logger.info(f"Voice call requested by {user.first_name}")

        # Store chat_id for voice bridge to use
        try:
            redis_client.set(f"genesis:voice:active_chat:{user.id}", str(chat_id), ex=3600)
        except Exception as e:
            logger.warning(f"Could not store chat_id: {e}")

        # Notify voice bridge that user wants to talk
        self._publish_observation("call_request", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "chat_id": chat_id
        })

        await context.bot.send_message(
            chat_id=chat_id,
            text=(
                "**Voice Mode Activated**\n\n"
                "I'm ready to talk. Just send me a voice message and I'll respond with voice.\n\n"
                "Tips:\n"
                "- Hold the mic button to record\n"
                "- Speak naturally, I understand context\n"
                "- Say 'text mode' if you want text replies instead"
            ),
            parse_mode='Markdown'
        )


    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/call - Start voice conversation\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_call(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /call command - initiate voice conversation."""
        user = update.effective_user
        chat_id = update.effective_chat.id

        logger.info(f"Voice call requested by {user.first_name}")

        # Store chat_id for voice bridge to use
        try:
            redis_client.set(f"genesis:voice:active_chat:{user.id}", str(chat_id), ex=3600)
        except Exception as e:
            logger.warning(f"Could not store chat_id: {e}")

        # Notify voice bridge that user wants to talk
        self._publish_observation("call_request", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "chat_id": chat_id
        })

        await context.bot.send_message(
            chat_id=chat_id,
            text=(
                "**Voice Mode Activated**\n\n"
                "I'm ready to talk. Just send me a voice message and I'll respond with voice.\n\n"
                "Tips:\n"
                "- Hold the mic button to record\n"
                "- Speak naturally, I understand context\n"
                "- Say 'text mode' if you want text replies instead"
            ),
            parse_mode='Markdown'
        )

"""
ClawdBot Telegram Skill
========================

Non-blocking Telegram bot integration for Genesis system.

Features:
- Async-compatible with main event loop
- Voice note reception and forwarding
- Text message handling
- Graceful shutdown support

Usage:
    The TelegramSkill is initialized by ClawdBot main.py if TELEGRAM_BOT_TOKEN
    is set in the environment. Messages are forwarded to Genesis via Redis.
"""

import logging
import os
import asyncio
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from telegram import Update
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CommandHandler,
    MessageHandler,
    filters
)

# Use the new enterprise Redis client
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ClawdBot.redis_client_v2 import redis_client

logger = logging.getLogger("ClawdBotTelegram")

CH_OBSERVATIONS = "genesis:observations"
CH_COMMANDS = "genesis:commands"

class TelegramSkill:
    """
    Non-blocking Telegram bot skill for ClawdBot.

    Key improvements over previous version:
    - Uses async polling that doesn't block the event loop
    - Proper graceful shutdown handling
    - Uses enterprise Redis client (v2)
    - Better error handling and logging
    """

    def __init__(self):
        self.token = os.getenv("TELEGRAM_BOT_TOKEN")
        self.app = None
        self._running = False

        if not self.token:
            logger.warning("TELEGRAM_BOT_TOKEN not found. Telegram Skill disabled.")
            return

        try:
            self.app = ApplicationBuilder().token(self.token).build()
            self._setup_handlers()
            logger.info("Telegram Skill initialized")
        except Exception as e:
            logger.error(f"Failed to initialize Telegram: {e}")
            self.app = None

    def _setup_handlers(self):
        """Register message handlers."""
        if not self.app:
            return

        # Command handlers
        self.app.add_handler(CommandHandler('start', self._handle_start))
        self.app.add_handler(CommandHandler('status', self._handle_status))
        self.app.add_handler(CommandHandler('help', self._handle_help))
        self.app.add_handler(CommandHandler('call', self._handle_call))

        # Message handlers
        self.app.add_handler(MessageHandler(filters.VOICE, self._handle_voice))
        self.app.add_handler(MessageHandler(filters.AUDIO, self._handle_audio))
        self.app.add_handler(MessageHandler(filters.PHOTO, self._handle_photo))
        self.app.add_handler(MessageHandler(
            filters.TEXT & (~filters.COMMAND),
            self._handle_text
        ))

        # Error handler
        self.app.add_error_handler(self._handle_error)

    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "Use /help for available commands."
            )
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name
        })

    async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /status command."""
        # Check Redis connection
        redis_ok = redis_client.is_connected()

        status_text = (
            "**ClawdBot Status**\n"
            f"- Redis: {'Connected' if redis_ok else 'Disconnected'}\n"
            f"- Telegram: Connected\n"
            f"- Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=status_text,
            parse_mode='Markdown'
        )

    async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /help command."""
        help_text = (
            "**ClawdBot Commands**\n\n"
            "/start - Initialize connection\n"
            "/status - Check system status\n"
            "/help - Show this help\n\n"
            "**Message Types**\n"
            "- Send voice notes for processing\n"
            "- Send text messages as commands\n"
            "- Send photos for analysis"
        )

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=help_text,
            parse_mode='Markdown'
        )

    async def _handle_voice(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle voice notes."""
        voice_file = await update.message.voice.get_file()
        user = update.effective_user

        logger.info(f"Received voice note from {user.first_name} ({voice_file.file_size} bytes)")

        # Download file locally
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "voice_input")
            os.makedirs(save_dir, exist_ok=True)

            file_path = os.path.join(save_dir, f"voice_{user.id}_{timestamp}.ogg")
            await voice_file.download_to_drive(file_path)

            # Update observation with file path
            self._publish_observation("voice_note", {
                "user_id": user.id,
                "username": user.username,
                "first_name": user.first_name,
                "file_path": file_path,  # Critical for processing
                "file_id": voice_file.file_id,
                "duration": update.message.voice.duration,
                "chat_id": update.effective_chat.id
            })

            logger.info(f"Voice note saved to {file_path}")

        except Exception as e:
            logger.error(f"Failed to download voice note: {e}")

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Voice note received. Transmitting to Genesis Cortex..."
        )

    async def _handle_audio(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle audio files."""
        audio = update.message.audio
        audio_file = await audio.get_file()
        user = update.effective_user

        logger.info(f"Received audio from {user.first_name}: {audio.title or 'Untitled'}")

        self._publish_observation("audio", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": audio_file.file_id,
            "title": audio.title,
            "performer": audio.performer,
            "duration": audio.duration,
            "mime_type": audio.mime_type
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Audio received and forwarded to Genesis."
        )

    async def _handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle photos."""
        # Get the largest photo
        photo = update.message.photo[-1]
        photo_file = await photo.get_file()
        user = update.effective_user

        logger.info(f"Received photo from {user.first_name} ({photo.width}x{photo.height})")

        self._publish_observation("photo", {
            "user_id": user.id,
            "first_name": user.first_name,
            "file_id": photo_file.file_id,
            "width": photo.width,
            "height": photo.height,
            "file_size": photo.file_size
        })

        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="Photo received and forwarded for analysis."
        )

    async def _handle_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle text messages."""
        user = update.effective_user
        text = update.message.text

        logger.info(f"Received text from {user.first_name}: {text[:50]}...")

        self._publish_observation("text_message", {
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "text": text,
            "chat_id": update.effective_chat.id
        })

        # Check if it looks like a command for Genesis
        if text.startswith("!") or text.startswith("/genesis"):
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Command forwarded to Genesis orchestrator."
            )
        else:
            await context.bot.send_message(
                chat_id=update.effective_chat.id,
                text="Message received and logged."
            )

    async def _handle_error(self, update: object, context: ContextTypes.DEFAULT_TYPE):
        """Handle errors."""
        logger.error(f"Telegram error: {context.error}")

        self._publish_observation("error", {
            "error": str(context.error),
            "update": str(update) if update else None
        })

    def _publish_observation(self, obs_type: str, data: dict):
        """Publish observation to Redis."""
        try:
            redis_client.publish(CH_OBSERVATIONS, {
                "source": "telegram",
                "type": obs_type,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                **data
            })
        except Exception as e:
            logger.error(f"Failed to publish observation: {e}")

    async def start_async(self):
        """
        Start the Telegram bot in a non-blocking way.

        This method initializes and starts polling without blocking
        the main asyncio event loop.
        """
        if not self.app:
            logger.warning("Telegram app not initialized, cannot start")
            return

        self._running = True

        try:
            await self.app.initialize()
            await self.app.start()
            await self.app.updater.start_polling(
                allowed_updates=Update.ALL_TYPES,
                drop_pending_updates=True
            )
            logger.info("Telegram bot polling started (non-blocking)")
        except Exception as e:
            logger.error(f"Failed to start Telegram polling: {e}")
            self._running = False

    async def stop_async(self):
        """Stop the Telegram bot gracefully."""
        if not self.app or not self._running:
            return

        self._running = False

        try:
            await self.app.updater.stop()
            await self.app.stop()
            await self.app.shutdown()
            logger.info("Telegram bot stopped gracefully")
        except Exception as e:
            logger.error(f"Error stopping Telegram: {e}")

    def run(self):
        """
        Blocking run for standalone execution.

        Note: This blocks the event loop. Use start_async() for
        integration with ClawdBot main event loop.
        """
        if not self.app:
            logger.error("Cannot run - Telegram app not initialized")
            return

        logger.info("Starting Telegram Bot Polling (blocking mode)...")
        self.app.run_polling()

    @property
    def is_running(self) -> bool:
        """Check if bot is running."""
        return self._running

    async def execute(self, action: str, args: Dict[str, Any]):
        """
        Execute a command from the Skills Manager.

        Actions:
        - send_message: Send a text message
        - send_voice: Send a voice note (OGG/MP3)
        """
        if not self.app:
            logger.warning("Telegram not initialized, cannot execute command")
            return

        chat_id = args.get("chat_id")
        if not chat_id:
            # Try to get last chat_id from Redis or default?
            # For now, require chat_id.
            logger.error("Missing chat_id for Telegram command")
            return

        try:
            if action == "send_message":
                text = args.get("text")
                if text:
                    await self.app.bot.send_message(chat_id=chat_id, text=text)
                    logger.info(f"Sent message to {chat_id}: {text[:20]}...")

            elif action == "send_voice":
                file_path = args.get("file_path")
                if file_path and os.path.exists(file_path):
                    with open(file_path, 'rb') as voice:
                        await self.app.bot.send_voice(chat_id=chat_id, voice=voice)
                    logger.info(f"Sent voice note to {chat_id}: {file_path}")
                else:
                    logger.error(f"Voice file not found: {file_path}")

            else:
                logger.warning(f"Unknown Telegram action: {action}")

        except Exception as e:
            logger.error(f"Failed to execute Telegram action {action}: {e}")

# Standalone runner for testing
if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv()

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    skill = TelegramSkill()
    if skill.app:
        skill.run()
    else:
        print("Failed to initialize Telegram skill. Check TELEGRAM_BOT_TOKEN.")

+            "/call - Start voice conversation\n"

+    async def _handle_call(self, update: Update, context: ContextTypes.DEFAULT_TYPE):

+        """Handle /call command - initiate voice conversation."""

+        chat_id = update.effective_chat.id

+        logger.info(f"Voice call requested by {user.first_name}")

+        # Store chat_id for voice bridge to use

+            redis_client.set(f"genesis:voice:active_chat:{user.id}", str(chat_id), ex=3600)

+            logger.warning(f"Could not store chat_id: {e}")

+        # Notify voice bridge that user wants to talk

+        self._publish_observation("call_request", {

+            "first_name": user.first_name,

+        await context.bot.send_message(

+                "**Voice Mode Activated**\n\n"

+                "I'm ready to talk. Just send me a voice message and I'll respond with voice.\n\n"

+                "- Hold the mic button to record\n"

+                "- Speak naturally, I understand context\n"

+                "- Say 'text mode' if you want text replies instead"


Now let me also make the bot proactively message you when it starts. First, let me check your Telegram user ID so the bot can message you directly:


No matches found


Let me add a startup notification feature that stores the owner's chat_id on first `/start` and messages them when the bot comes online:


    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        await context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "Use /help for available commands."
            )
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name
        })

    async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Handle /start command."""
        user = update.effective_user
        chat_id = update.effective_chat.id

        # Store owner chat_id for startup notifications
        try:
            redis_client.set("genesis:telegram:owner_chat_id", str(chat_id))
            redis_client.set(f"genesis:telegram:user:{user.id}:chat_id", str(chat_id))
            logger.info(f"Stored owner chat_id: {chat_id}")
        except Exception as e:
            logger.warning(f"Could not store chat_id: {e}")

        await context.bot.send_message(
            chat_id=chat_id,
            text=(
                f"Greetings, {user.first_name}. I am ClawdBot, the Genesis Interface Agent.\n\n"
                "I can receive:\n"
                "- Voice messages (forwarded to Genesis)\n"
                "- Text commands\n"
                "- Screenshots\n\n"
                "**Quick Start**: Type /call to start a voice conversation.\n\n"
                "Use /help for all commands."
            ),
            parse_mode='Markdown'
        )

        # Log connection
        self._publish_observation("system", {
            "event": "telegram_user_connected",
            "user_id": user.id,
            "username": user.username,
            "first_name": user.first_name,
            "chat_id": chat_id
        })