From 36fabb41a6ad17d57222826cc8af7af55eb09106 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 4 Feb 2026 19:03:40 +0000 Subject: [PATCH] docs(02): create phase plan Phase 02: Telegram Integration - 2 plan(s) in 2 wave(s) - Wave 1: persistent subprocess + message utilities (02-01) - Wave 2: bot integration + batching + file handling + systemd (02-02) - Ready for execution Co-Authored-By: Claude Opus 4.5 --- .planning/ROADMAP.md | 7 +- .../02-telegram-integration/02-01-PLAN.md | 230 +++++++++++++ .../02-telegram-integration/02-02-PLAN.md | 316 ++++++++++++++++++ 3 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/02-telegram-integration/02-01-PLAN.md create mode 100644 .planning/phases/02-telegram-integration/02-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2e13e21..a9a7462 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -47,10 +47,11 @@ Plans: 4. User attaches file/photo in Telegram and Claude can access it in session folder 5. Long responses split at 4096 char Telegram limit with proper code block handling 6. Bot runs as systemd user service and survives container restarts -**Plans**: TBD +**Plans:** 2 plans Plans: -- [ ] TBD +- [ ] 02-01-PLAN.md -- Persistent subprocess engine + message formatting utilities +- [ ] 02-02-PLAN.md -- Bot integration with batching, file handling, and systemd service ### Phase 3: Lifecycle Management **Goal**: Sessions suspend automatically after idle period and resume transparently with full context @@ -88,6 +89,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Session & Process Foundation | 3/3 | Complete | 2026-02-04 | -| 2. Telegram Integration | 0/TBD | Not started | - | +| 2. Telegram Integration | 0/2 | Not started | - | | 3. Lifecycle Management | 0/TBD | Not started | - | | 4. Output Modes | 0/TBD | Not started | - | diff --git a/.planning/phases/02-telegram-integration/02-01-PLAN.md b/.planning/phases/02-telegram-integration/02-01-PLAN.md new file mode 100644 index 0000000..1519546 --- /dev/null +++ b/.planning/phases/02-telegram-integration/02-01-PLAN.md @@ -0,0 +1,230 @@ +--- +phase: 02-telegram-integration +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - telegram/claude_subprocess.py + - telegram/telegram_utils.py +autonomous: true + +must_haves: + truths: + - "Persistent Claude Code subprocess accepts multiple messages without respawning" + - "Stream-json events from Claude include tool_use events parseable for progress notifications" + - "Long messages are split at smart boundaries without breaking MarkdownV2 code blocks" + - "MarkdownV2 special characters are properly escaped before sending to Telegram" + - "Typing indicator can be maintained for arbitrarily long operations via re-send loop" + artifacts: + - path: "telegram/claude_subprocess.py" + provides: "Persistent subprocess with stream-json stdin/stdout" + contains: "input-format.*stream-json" + - path: "telegram/telegram_utils.py" + provides: "Message splitting, MarkdownV2 escaping, typing indicator loop" + exports: ["split_message_smart", "escape_markdown_v2", "typing_indicator_loop"] + key_links: + - from: "telegram/claude_subprocess.py" + to: "claude CLI" + via: "--input-format stream-json stdin piping" + pattern: "input-format.*stream-json" + - from: "telegram/claude_subprocess.py" + to: "callbacks" + via: "on_output/on_error/on_complete/on_status/on_tool_use" + pattern: "on_tool_use" +--- + + +Refactor ClaudeSubprocess from fresh-process-per-turn to persistent process with stream-json I/O, and create Telegram utility functions for message formatting and typing indicators. + +Purpose: The persistent process model eliminates ~1s spawn overhead per message, maintains conversation context across turns, and enables real-time tool call notifications. The utility functions provide safe MarkdownV2 formatting and smart message splitting that the bot integration (Plan 02) will consume. + +Output: Refactored `claude_subprocess.py` with persistent subprocess, new `telegram_utils.py` with message splitting/escaping/typing utilities. + + + +@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md +@/home/mikkel/.claude/get-shit-done/templates/summary.md + + + +@.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/01-session-process-foundation/01-02-SUMMARY.md +@telegram/claude_subprocess.py + + + + + + Task 1: Refactor ClaudeSubprocess to persistent process with stream-json I/O + telegram/claude_subprocess.py + +Refactor ClaudeSubprocess to maintain a single long-lived process per session using `--input-format stream-json --output-format stream-json` instead of spawning fresh `claude -p` per turn. + +**Key changes:** + +1. **New start() method:** Spawns persistent subprocess with: + ``` + claude -p --input-format stream-json --output-format stream-json --verbose + ``` + Plus persona flags (--system-prompt, --model, --max-turns) and --continue if `.claude/` exists. + Launches background tasks for `_read_stdout()` and `_read_stderr()` (concurrent readers from Phase 1 pattern). + +2. **Refactor send_message():** Instead of spawning a new process, writes NDJSON to stdin: + ```python + msg = json.dumps({"type": "user", "content": message}) + '\n' + self._process.stdin.write(msg.encode()) + await self._process.stdin.drain() # CRITICAL: prevent buffer deadlock + ``` + Sets `_busy = True`. If process not started, calls `start()` first. + If already busy, queues the message (existing queue behavior preserved). + +3. **Add on_tool_use callback:** New callback `on_tool_use(tool_name: str, tool_input: dict)` for progress notifications. + In `_handle_stdout_line()`, parse `content_block_start` events with `type: "tool_use"` to extract tool name and input. + Stream-json emits these as separate events during assistant turns. + +4. **Update _handle_stdout_line():** Handle the stream-json event types from persistent mode: + - `assistant`: Extract text blocks, call on_output with final text + - `result`: Turn complete, set `_busy = False`, call on_complete, process queue + - `content_block_start` / `content_block_delta` with tool_use type: Extract tool name + target, call on_tool_use + - `system`: System events, log and handle errors + +5. **Update _read_streams() / _read_stdout():** Since the process is persistent, the stdout reader must NOT exit when a turn completes. It stays alive reading events indefinitely. Remove the "mark as not busy" logic from _read_streams and move it to the result event handler in _handle_stdout_line instead. The reader only exits when process dies (readline returns empty bytes). + +6. **Process lifecycle:** + - `start()` — spawns process, starts readers + - `send_message()` — writes to stdin (auto-starts if needed) + - `terminate()` — closes stdin, sends SIGTERM, waits, SIGKILL fallback (existing pattern) + - `is_alive` — check process.returncode is None + - `is_busy` — check _busy flag + +7. **Crash recovery:** Keep existing crash recovery logic but adapt: when process dies unexpectedly, restart with `start()` and resend any queued messages. The --continue flag in start() ensures session context is preserved. + +8. **Remove _spawn() method:** Replace with start() for process lifecycle and send_message() for message delivery. The old pattern of passing message to _spawn() is no longer needed since messages go to stdin. + +**Preserve from Phase 1:** +- Callback architecture (on_output, on_error, on_complete, on_status + new on_tool_use) +- Message queue (asyncio.Queue) +- Graceful termination (SIGTERM → wait_for → SIGKILL → wait) +- Crash retry logic (MAX_CRASH_RETRIES, backoff) +- PATH environment setup +- Session directory as cwd +- Timing/logging instrumentation + +**Important implementation notes:** +- The stream-json input format message structure: Test with `{"type": "user", "content": "message text"}`. If that fails, try simpler `{"content": "message text"}`. Check `claude --help` or test interactively. +- Always `await proc.stdin.drain()` after writing to prevent pipe buffer deadlock +- The stdout reader task must run for the lifetime of the process, not per-turn +- Track busy state via result events, not process completion + + +1. Run `python -c "import telegram.claude_subprocess; print('import OK')"` from ~/homelab (or equivalent sibling import test) +2. Verify the class has start(), send_message(), terminate() methods +3. Verify on_tool_use callback parameter exists in __init__ +4. Verify --input-format stream-json appears in the command construction +5. Verify stdin.write + drain pattern in send_message +6. Verify stdout reader does NOT exit on result events (stays alive for persistent process) + + +ClaudeSubprocess maintains a persistent process that accepts NDJSON messages on stdin, emits stream-json events on stdout, routes tool_use events to on_tool_use callback, and tracks busy state via result events instead of process completion. No fresh process spawned per turn. + + + + + Task 2: Create telegram_utils.py with message formatting and typing indicator + telegram/telegram_utils.py + +Create `telegram/telegram_utils.py` with three utility functions consumed by the bot integration in Plan 02. + +**1. Smart message splitting — `split_message_smart(text: str, max_length: int = 4000) -> list[str]`:** +- Split long messages respecting MarkdownV2 code block boundaries +- Never split inside triple-backtick code blocks +- Prefer paragraph breaks (`\n\n`), then line breaks (`\n`), then hard character split as last resort +- Use 4000 as default max (not 4096) to leave room for MarkdownV2 escape character expansion +- Track `in_code_block` state by counting triple-backtick lines +- If a code block exceeds max_length by itself, include it whole (Telegram will handle overflow gracefully or we truncate with "... (truncated)" marker) +- Algorithm from research: iterate lines, track code block state, split only when NOT in code block and would exceed limit + +**2. MarkdownV2 escaping — `escape_markdown_v2(text: str) -> str`:** +- Escape the 17 MarkdownV2 special characters: `_ * [ ] ( ) ~ \` > # + - = | { } . !` +- BUT do NOT escape inside code blocks (text between triple backticks or single backticks) +- Strategy: Split text by code regions, escape only non-code regions, rejoin +- For inline code (single backtick): don't escape content between backticks +- For code blocks (triple backtick): don't escape content between triple backticks +- Use regex to identify code regions: find ```...``` and `...` blocks, escape everything else + +**3. Typing indicator loop — `async def typing_indicator_loop(bot, chat_id: int, stop_event: asyncio.Event)`:** +- Send `ChatAction.TYPING` every 4 seconds until stop_event is set +- Use `asyncio.wait_for(stop_event.wait(), timeout=4.0)` pattern from research +- Catch exceptions from send_chat_action (network errors) and log warning, continue loop +- Clean exit when stop_event is set +- Import from `telegram import ChatAction` + +**Module structure:** +```python +""" +Telegram message formatting and UX utilities. + +Provides smart message splitting, MarkdownV2 escaping, and typing indicator +management for the Telegram Claude Code bridge. +""" +import asyncio +import logging +import re +from telegram import ChatAction + +logger = logging.getLogger(__name__) + +TELEGRAM_MAX_LENGTH = 4096 +SAFE_LENGTH = 4000 + +def split_message_smart(text: str, max_length: int = SAFE_LENGTH) -> list[str]: + ... + +def escape_markdown_v2(text: str) -> str: + ... + +async def typing_indicator_loop(bot, chat_id: int, stop_event: asyncio.Event): + ... +``` + + +1. Run `python -c "from telegram_utils import split_message_smart, escape_markdown_v2, typing_indicator_loop; print('imports OK')"` from ~/homelab/telegram/ +2. Test split_message_smart: `split_message_smart("a" * 5000)` returns list with 2+ chunks, each <= 4000 chars +3. Test split_message_smart with code block: message containing ``` block is not split inside the block +4. Test escape_markdown_v2: `escape_markdown_v2("hello_world")` returns `"hello\_world"` +5. Test escape_markdown_v2 preserves code: text inside backticks is NOT escaped + + +telegram_utils.py exists with split_message_smart (code-block-aware splitting), escape_markdown_v2 (context-sensitive escaping), and typing_indicator_loop (4s re-send with asyncio.Event cancellation). All functions importable and tested with basic cases. + + + + + + +1. `python -c "from telegram.claude_subprocess import ClaudeSubprocess"` succeeds (or sibling import equivalent) +2. `python -c "from telegram_utils import split_message_smart, escape_markdown_v2, typing_indicator_loop"` from telegram/ succeeds +3. ClaudeSubprocess.__init__ accepts on_tool_use callback +4. ClaudeSubprocess has start(), send_message(), terminate() methods +5. send_message() writes NDJSON to stdin (not spawning new process) +6. split_message_smart handles code blocks without breaking them +7. escape_markdown_v2 escapes outside code blocks only + + + +- ClaudeSubprocess uses persistent process with --input-format stream-json +- Messages sent via stdin NDJSON, not fresh process spawn +- Tool use events parsed and routed to on_tool_use callback +- Smart message splitting respects code block boundaries +- MarkdownV2 escaping handles all 17 special characters with code block awareness +- Typing indicator loop re-sends every 4 seconds with clean cancellation + + + +After completion, create `.planning/phases/02-telegram-integration/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-telegram-integration/02-02-PLAN.md b/.planning/phases/02-telegram-integration/02-02-PLAN.md new file mode 100644 index 0000000..71fd6c2 --- /dev/null +++ b/.planning/phases/02-telegram-integration/02-02-PLAN.md @@ -0,0 +1,316 @@ +--- +phase: 02-telegram-integration +plan: 02 +type: execute +wave: 2 +depends_on: ["02-01"] +files_modified: + - telegram/bot.py + - telegram/message_batcher.py +autonomous: false + +must_haves: + truths: + - "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" + artifacts: + - path: "telegram/bot.py" + provides: "Updated message handlers with typing, progress, batching, file handling" + contains: "typing_indicator_loop" + - path: "telegram/message_batcher.py" + provides: "MessageBatcher class for debounce-based message batching" + exports: ["MessageBatcher"] + - path: "~/.config/systemd/user/telegram-bot.service" + provides: "Systemd user service unit for bot" + contains: "telegram-bot" + key_links: + - from: "telegram/bot.py" + to: "telegram/claude_subprocess.py" + via: "ClaudeSubprocess.send_message() and callbacks" + pattern: "send_message" + - from: "telegram/bot.py" + to: "telegram/telegram_utils.py" + via: "split_message_smart, escape_markdown_v2, typing_indicator_loop" + pattern: "split_message_smart|escape_markdown_v2|typing_indicator_loop" + - from: "telegram/bot.py" + to: "telegram/message_batcher.py" + via: "MessageBatcher.add_message()" + pattern: "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. + + + +@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md +@/home/mikkel/.claude/get-shit-done/templates/summary.md + + + +@.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: + +```python +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: + +```python +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: + - When no subprocess exists for active session, create ClaudeSubprocess and call `await subprocess.start()` + - Pass all 5 callbacks (on_output, on_error, on_complete, on_status, on_tool_use) + +**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`:** + +```ini +[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: +```bash +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 + + + +- 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 + + + +After completion, create `.planning/phases/02-telegram-integration/02-02-SUMMARY.md` +