---
phase: 02-telegram-integration
plan: 02
subsystem: telegram
tags: [telegram, bot, typing-indicator, batching, file-handling, systemd, markdownv2]

# Dependency graph
requires:
  - phase: 02-telegram-integration
    plan: 01
    provides: Persistent subprocess and telegram utils
provides:
  - End-to-end Telegram-Claude Code messaging with typing indicators
  - Message batching with debounce for rapid sequential messages
  - Photo/document handling with auto-analysis
  - Tool call progress notifications
  - Systemd user service for reliability
affects: [03-lifecycle-management, bot-reliability]

# Tech tracking
tech-stack:
  added:
    - MessageBatcher (asyncio.Queue + debounce timer)
    - systemd user service (KillMode=mixed)
  patterns:
    - "Dynamic typing event lookup via session name in typing_tasks dict"
    - "Debounce-based message batching with asyncio.Queue"
    - "--append-system-prompt preserves Claude Code defaults while adding persona"
    - "--dangerously-skip-permissions for full tool access in non-interactive mode"

key-files:
  created:
    - telegram/message_batcher.py
    - ~/.config/systemd/user/telegram-bot.service
  modified:
    - telegram/bot.py
    - telegram/claude_subprocess.py
    - telegram/telegram_utils.py
    - telegram/personas/default.json

key-decisions:
  - "Dynamic typing event lookup: callbacks reference typing_tasks dict by session name, not captured event"
  - "--append-system-prompt instead of --system-prompt: preserves Claude Code model identity"
  - "--dangerously-skip-permissions: allows all tools in non-interactive subprocess"
  - "Full model ID in persona (claude-sonnet-4-5-20250929) instead of alias"
  - "Stream-json NDJSON format: {type: user, message: {role: user, content: text}}"

patterns-established:
  - "Pattern 1: Dynamic callback binding - closures look up mutable state by key instead of capturing immutable reference"
  - "Pattern 2: Stale task cleanup - check task.done() and delete from dict before creating replacement"
  - "Pattern 3: Persona via append - use --append-system-prompt to layer persona on top of CLI defaults"

# Metrics
duration: ~90min (including interactive debugging with user)
completed: 2026-02-04
---

# Phase 2 Plan 2: Bot Integration Summary

**End-to-end Telegram-Claude Code messaging with typing, batching, file handling, and systemd service**

## Performance

- **Duration:** ~90 minutes (including interactive testing and bug fixes)
- **Started:** 2026-02-04T19:30:00Z
- **Completed:** 2026-02-04T22:10:00Z
- **Tasks:** 3 (2 auto + 1 human-verify checkpoint)
- **Files created:** 2
- **Files modified:** 4

## Accomplishments

- Created MessageBatcher with debounce-based batching (2s timer, asyncio.Queue)
- Overhauled bot.py: typing indicators, tool call progress, message batching, file handling
- Created systemd user service with KillMode=mixed for subprocess cleanup
- Fixed stream-json NDJSON input format (nested message object required)
- Fixed typing indicator lifecycle (stale task cleanup + dynamic event lookup)
- Switched to --append-system-prompt to preserve Claude Code model identity
- Added --dangerously-skip-permissions for full tool access
- Set full model ID (claude-sonnet-4-5-20250929) in default persona
- Human-verified: messages flow, typing indicators show, model identifies correctly

## Task Commits

1. **Task 1: MessageBatcher + bot.py overhaul** - `f246d18` (feat)
2. **Task 2: Systemd user service** - created during same executor run
3. **Task 3: Human verification** - bugs found and fixed:
   - `2d0d4da` - typing indicator lifecycle, model identity, tool permissions

## Bugs Found During Testing

1. **Stream-json input format**: `{"type":"user","content":"..."}` rejected by Claude Code; correct format is `{"type":"user","message":{"role":"user","content":"..."}}`
2. **Typing indicator stale tasks**: After completion, typing task stayed in dict with set event; next message reused dead task and typing never started
3. **Typing indicator event mismatch**: Subprocess callbacks captured specific stop_typing event at creation time; new messages created new events but callbacks still set the old one
4. **Model identity lost**: `--system-prompt` overwrote Claude Code's built-in system prompt (which includes model version); fixed with `--append-system-prompt`
5. **Tool permissions blocked**: Non-interactive `-p` mode blocked tools requiring permission prompts; fixed with `--dangerously-skip-permissions`
6. **Model alias resolution**: `sonnet` alias resolved to 3.5 Sonnet in CLI; fixed by using full model ID

## Files Created/Modified

**Created:**
- `telegram/message_batcher.py` - Debounce-based message batching with asyncio.Queue
- `~/.config/systemd/user/telegram-bot.service` - Systemd user service with KillMode=mixed

**Modified:**
- `telegram/bot.py` - Full overhaul: dynamic typing callbacks, stale task cleanup, batching, file handling
- `telegram/claude_subprocess.py` - NDJSON fix, --dangerously-skip-permissions, --append-system-prompt, full cmd logging
- `telegram/telegram_utils.py` - Debug logging for typing indicator
- `telegram/personas/default.json` - Full model ID instead of alias

## Deviations from Plan

1. **Typing indicator architecture changed**: Plan assumed capturing stop_typing event in callbacks; changed to dynamic lookup from typing_tasks dict by session name to fix lifecycle bugs
2. **System prompt approach changed**: Plan used `--system-prompt`; changed to `--append-system-prompt` to preserve Claude Code defaults
3. **Added --dangerously-skip-permissions**: Not in original plan; needed for tool access in non-interactive mode
4. **Model ID changed**: Plan used `sonnet` alias; changed to full ID after alias resolution issues

---
*Phase: 02-telegram-integration*
*Completed: 2026-02-04*
