homelab/.planning/phases/02-telegram-integration/02-02-SUMMARY.md
Mikkel Georgsen d31675f98c docs(02): complete telegram-integration phase
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:12:41 +00:00

5.7 KiB

phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established duration completed
02-telegram-integration 02 telegram
telegram
bot
typing-indicator
batching
file-handling
systemd
markdownv2
phase plan provides
02-telegram-integration 01 Persistent subprocess and telegram utils
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
03-lifecycle-management
bot-reliability
added patterns
MessageBatcher (asyncio.Queue + debounce timer)
systemd user service (KillMode=mixed)
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
created modified
telegram/message_batcher.py
~/.config/systemd/user/telegram-bot.service
telegram/bot.py
telegram/claude_subprocess.py
telegram/telegram_utils.py
telegram/personas/default.json
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}}
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
~90min (including interactive debugging with user) 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