homelab/.planning/phases/02-telegram-integration/02-02-PLAN.md
Mikkel Georgsen 92318e79ab fix(02): revise plans based on checker feedback
Address 3 blockers and 1 warning:
- Clarify on_tool_use responsibility split (subprocess passes raw data, bot.py formats)
- Make verify step 6 concrete with code inspection pattern
- Add explicit subprocess auto-start pseudo-code with double-start guard
- Reframe must_haves truth from implementation detail to capability

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:07:32 +00:00

15 KiB

phase plan type wave depends_on files_modified autonomous must_haves
02-telegram-integration 02 execute 2
02-01
telegram/bot.py
telegram/message_batcher.py
false
truths artifacts key_links
User sends message in Telegram and receives Claude's response formatted in MarkdownV2
Typing indicator stays visible during entire Claude processing time (10-60s+)
User sees tool call progress notifications (e.g. 'Reading config.json...')
Rapid sequential messages are batched into a single Claude prompt
User attaches photo in Telegram and Claude auto-analyzes it
User attaches document in Telegram and Claude can reference it in session
Responses longer than 4096 chars are split across multiple messages without breaking code blocks
Bot runs as systemd user service and restarts on failure
path provides contains
telegram/bot.py Updated message handlers with typing, progress, batching, file handling typing_indicator_loop
path provides exports
telegram/message_batcher.py MessageBatcher class for debounce-based message batching
MessageBatcher
path provides contains
~/.config/systemd/user/telegram-bot.service Systemd user service unit for bot telegram-bot
from to via pattern
telegram/bot.py telegram/claude_subprocess.py ClaudeSubprocess.send_message() and callbacks send_message
from to via pattern
telegram/bot.py telegram/telegram_utils.py split_message_smart, escape_markdown_v2, typing_indicator_loop split_message_smart|escape_markdown_v2|typing_indicator_loop
from to via pattern
telegram/bot.py telegram/message_batcher.py MessageBatcher.add_message() MessageBatcher
Wire the persistent subprocess and utility functions into the Telegram bot with typing indicators, progress notifications, message batching, file handling, and systemd service setup.

Purpose: This plan makes the entire system work end-to-end. Messages flow from Telegram through the batcher to the persistent Claude subprocess, responses come back formatted in MarkdownV2 with smart splitting, and the user sees typing indicators and tool call progress throughout. File attachments land in session folders with auto-analysis. The systemd service ensures reliability across container restarts.

Output: Updated bot.py with full integration, new message_batcher.py, systemd service file, working end-to-end flow.

<execution_context> @/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md @/home/mikkel/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-telegram-integration/02-RESEARCH.md @.planning/phases/02-telegram-integration/02-CONTEXT.md @.planning/phases/02-telegram-integration/02-01-SUMMARY.md @telegram/bot.py @telegram/claude_subprocess.py @telegram/telegram_utils.py @telegram/session_manager.py Task 1: Create MessageBatcher and update bot.py with typing, progress, batching, and file handling telegram/message_batcher.py, telegram/bot.py **Part A: Create telegram/message_batcher.py**

Implement MessageBatcher class for debounce-based message batching:

class MessageBatcher:
    def __init__(self, callback: Callable, debounce_seconds: float = 2.0):
        ...
    async def add_message(self, message: str):
        """Add message, reset debounce timer. When timer expires, flush batch via callback."""
        ...
  • Uses asyncio.Queue to collect messages
  • Cancels previous debounce timer when new message arrives
  • After debounce_seconds of silence, joins all queued messages with \n\n and calls callback
  • Callback is async (receives combined message string)
  • Handles CancelledError gracefully during timer cancellation
  • Follow research pattern from 02-RESEARCH.md (MessageBatcher section)

Part B: Update telegram/bot.py — make_callbacks() overhaul

Replace the current make_callbacks() with a new version that uses telegram_utils:

from telegram_utils import split_message_smart, escape_markdown_v2, typing_indicator_loop
from message_batcher import MessageBatcher

New make_callbacks(bot, chat_id) returns dict or tuple of callbacks:

  1. on_output(text):

    • Split text using split_message_smart(text)
    • For each chunk: try sending with parse_mode='MarkdownV2' after escape_markdown_v2()
    • If MarkdownV2 parse fails (Telegram BadRequest), fall back to plain text send
    • Stop the typing indicator (set stop_event)
  2. on_error(error):

    • Send error message to chat (plain text, no MarkdownV2)
    • Stop the typing indicator
  3. on_complete():

    • Stop the typing indicator (set stop_event)
    • Log completion
  4. on_status(status):

    • Send status as a brief message (e.g., "Claude restarted with context preserved")
  5. on_tool_use(tool_name, tool_input): (NEW)

    • Format tool call notification: extract meaningful target from tool_input
    • For Bash tool: show command preview (first 50 chars)
    • For Read tool: show file path
    • For Edit tool: show file path
    • For Grep/Glob: show pattern
    • For Write tool: show file path
    • Send as a single editable progress message (edit_message_text on a progress message)
    • OR send as separate short messages (planner's discretion — separate messages are simpler and more reliable)
    • Format: italic text like _Reading config.json..._

Part C: Update handle_message()

Overhaul the message handler to use typing indicators and message batching:

  1. On message received:

    • Start typing indicator loop: stop_typing = asyncio.Event(), asyncio.create_task(typing_indicator_loop(...))
    • Pass stop_typing event to callbacks so on_output/on_complete can stop it
    • Get or create subprocess (existing logic, but use start() instead of constructor for persistent process)
  2. Message batching:

    • Create one MessageBatcher per session (store in dict alongside subprocesses)
    • Batcher callback = subprocess.send_message()
    • On message: await batcher.add_message(text) instead of direct subprocess.send_message()
    • Typing indicator starts immediately on first message, stops on Claude response
  3. Subprocess auto-start (integrate into existing handle_message after session lookup, before batcher):

    # In handle_message(), after resolving active session:
    session_id = session_manager.get_active_session(user_id)
    
    # Get or create subprocess for this session (avoid double-start)
    if session_id not in self.subprocesses or not self.subprocesses[session_id].is_alive:
        callbacks = make_callbacks(bot, chat_id, stop_typing_event)
        subprocess = ClaudeSubprocess(
            session_dir=session_dir,
            on_output=callbacks['on_output'],
            on_error=callbacks['on_error'],
            on_complete=callbacks['on_complete'],
            on_status=callbacks['on_status'],
            on_tool_use=callbacks['on_tool_use'],
        )
        await subprocess.start()
        self.subprocesses[session_id] = subprocess
    else:
        subprocess = self.subprocesses[session_id]
    
    • The is_alive check prevents double-start: only creates and starts if no subprocess exists for session or previous one died
    • self.subprocesses is a dict[str, ClaudeSubprocess] stored on the handler/application context (same pattern as existing subprocess tracking in bot.py)

Part D: Update handle_photo() and handle_document()

Save files to active session folder instead of global images/files directories:

  1. handle_photo():

    • Get active session directory from session_manager
    • If no active session, prompt user to create one
    • Download highest-quality photo to session directory as photo_YYYYMMDD_HHMMSS.jpg
    • Auto-analyze: send message to Claude subprocess: "I've attached a photo: {filename}. {caption or 'Please describe what you see.'}"
    • Start typing indicator while Claude analyzes
  2. handle_document():

    • Get active session directory from session_manager
    • If no active session, prompt user to create one
    • Download document to session directory with original filename (timestamp prefix for collision avoidance)
    • If caption provided: send caption + "The file {filename} has been saved to your session." to Claude
    • If no caption: send "User uploaded file: {filename}" to Claude (let Claude infer intent from context, per CONTEXT.md decision)

Part E: Update switch_session_cmd() and archive_session_cmd()

  • On session switch: stop typing indicator for current session if running
  • On session switch: batcher should flush immediately (don't lose queued messages)
  • On archive: terminate subprocess, remove batcher
  1. python -c "from message_batcher import MessageBatcher; print('import OK')" from ~/homelab/telegram/
  2. bot.py imports telegram_utils functions and MessageBatcher without errors
  3. make_callbacks includes on_tool_use callback
  4. handle_message uses typing_indicator_loop
  5. handle_photo saves to session directory (not global images/)
  6. handle_document saves to session directory (not global files/)
  7. MessageBatcher has add_message() method MessageBatcher debounces rapid messages with configurable timer. Bot handlers use typing indicators, progress notifications for tool calls, smart message splitting with MarkdownV2, and file handling saves to session directories with auto-analysis.
Task 2: Create systemd user service for the bot ~/.config/systemd/user/telegram-bot.service Create or update the systemd user service unit for the Telegram bot.

Service file at ~/.config/systemd/user/telegram-bot.service:

[Unit]
Description=Homelab Telegram Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/mikkel/homelab/telegram
ExecStart=/home/mikkel/venv/bin/python bot.py
Restart=on-failure
RestartSec=10
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30

# Environment
Environment=PATH=/home/mikkel/.local/bin:/home/mikkel/bin:/usr/local/bin:/usr/bin:/bin

[Install]
WantedBy=default.target

Key settings:

  • KillMode=mixed: Sends SIGTERM to main process, SIGKILL to remaining children (ensures Claude subprocesses are cleaned up)
  • RestartSec=10: Wait 10s before restart to avoid rapid restart loops
  • TimeoutStopSec=30: Give bot time to gracefully terminate subprocesses before force kill
  • WorkingDirectory: Set to telegram/ so sibling imports work

After creating the service file:

mkdir -p ~/.config/systemd/user
# Write service file
systemctl --user daemon-reload
systemctl --user enable telegram-bot.service

Do NOT start the service yet (user will start it after verifying manually).

Also ensure loginctl enable-linger is set for the mikkel user (allows user services to run without active login session). Check with loginctl show-user mikkel -p Linger. If not enabled, note it as a requirement but do NOT run the command (requires root).

  1. Service file exists at ~/.config/systemd/user/telegram-bot.service
  2. systemctl --user cat telegram-bot.service shows the service configuration
  3. systemctl --user is-enabled telegram-bot.service returns "enabled"
  4. Service file has KillMode=mixed and correct WorkingDirectory
  5. Check loginctl linger status and report Systemd user service is created and enabled (not started). Bot can be started with systemctl --user start telegram-bot.service and survives container restarts (with linger enabled). KillMode=mixed ensures Claude subprocesses are cleaned up on stop.
Complete Telegram-Claude Code bidirectional messaging system: - Persistent Claude Code subprocess with stream-json I/O (no respawn per turn) - Typing indicator while Claude processes (re-sent every 4s) - Tool call progress notifications (e.g., "Reading config.json...") - Smart message splitting at paragraph/code block boundaries with MarkdownV2 - Message batching for rapid sequential messages (2s debounce) - Photos/documents saved to session folder with auto-analysis - Systemd user service for reliability 1. Start the bot manually: `cd ~/homelab/telegram && ~/venv/bin/python bot.py` 2. In Telegram, create a session: `/new test-phase2` 3. Send a simple message: "Hello, what can you help me with?" 4. Verify: typing indicator appears while Claude processes 5. Verify: Claude's response arrives formatted properly 6. Send a message that triggers tool use: "Read the file ~/homelab/CLAUDE.md and summarize it" 7. Verify: you see tool call progress notification (e.g., "Reading CLAUDE.md...") 8. Verify: response is natural language summary (not raw code) 9. Send a photo with caption "What is this?" 10. Verify: photo is saved to ~/homelab/telegram/sessions/test-phase2/ and Claude analyzes it 11. Send 3 rapid messages (within 2 seconds): "one", "two", "three" 12. Verify: they are batched into a single Claude prompt 13. Type a long question that produces a response >4096 chars 14. Verify: response splits across multiple messages without broken code blocks 15. Check systemd: `systemctl --user status telegram-bot.service` shows enabled 16. Archive test session: `/archive test-phase2` Type "approved" or describe issues found 1. End-to-end: Send message in Telegram, receive Claude's response back 2. Typing indicator visible during processing (10-60s range) 3. Tool call notifications appear for Read, Bash, Edit operations 4. Photo attachment saved to session folder and auto-analyzed 5. Document attachment saved to session folder 6. Long response properly split across messages 7. MarkdownV2 formatting renders correctly (bold, code blocks, etc.) 8. Rapid messages batched before sending to Claude 9. Systemd service enabled and configured with KillMode=mixed 10. Session switching stops typing indicator for previous session

<success_criteria>

  • User sends message in Telegram and receives Claude's response formatted in MarkdownV2
  • Typing indicator visible for entire processing duration
  • Tool call progress notifications appear
  • Photos auto-analyzed, documents saved to session
  • Long responses split correctly
  • Rapid messages batched
  • Systemd service configured and enabled
  • Bot survives manual restart test </success_criteria>
After completion, create `.planning/phases/02-telegram-integration/02-02-SUMMARY.md`