--- phase: 02-telegram-integration plan: 01 subsystem: infra tags: [asyncio, subprocess, python, claude-code-cli, stream-json, telegram, markdown, typing-indicator] # Dependency graph requires: - phase: 01-session-process-foundation provides: Session management and subprocess foundations provides: - Persistent Claude Code subprocess with stream-json I/O (eliminates ~1s spawn overhead per turn) - Tool call progress notifications via on_tool_use callback - Smart message splitting respecting MarkdownV2 code block boundaries - MarkdownV2 escaping with code block awareness - Typing indicator loop for long operations affects: [02-02, bot-integration, message-handling] # Tech tracking tech-stack: added: - telegram.constants.ChatAction (typing indicators) patterns: - "Persistent subprocess with NDJSON stdin streaming" - "Stream-json event routing including tool_use events" - "Smart message splitting at code block boundaries" - "Context-sensitive MarkdownV2 escaping (preserves code blocks)" - "Typing indicator loop with asyncio.Event cancellation" key-files: created: - telegram/telegram_utils.py modified: - telegram/claude_subprocess.py key-decisions: - "Use persistent subprocess instead of fresh process per turn (eliminates ~1s overhead)" - "Write NDJSON to stdin with drain() to prevent pipe buffer deadlock" - "Persistent readers run for process lifetime, result events mark not busy but don't exit loop" - "Parse content_block_start events for tool_use to enable progress notifications" - "Split messages at 4000 chars (not 4096) to leave room for escape character expansion" - "Never split inside code blocks (track in_code_block state while iterating lines)" - "Escape MarkdownV2 special chars only outside code blocks (regex pattern to identify code regions)" patterns-established: - "Pattern 1: Persistent stdin streaming - start() spawns process, send_message() writes NDJSON, readers run until process dies" - "Pattern 2: Tool use notifications - Parse content_block_start with type=tool_use, extract name and input dict, pass to callback" - "Pattern 3: Smart message splitting - Track code block state, split only when NOT in code block and would exceed limit" - "Pattern 4: Code-aware escaping - Split by code regions (regex), escape only non-code text, rejoin" - "Pattern 5: Typing loop - Re-send every 4s via asyncio.wait_for timeout until Event is set" # Metrics duration: 5min completed: 2026-02-04 --- # Phase 2 Plan 1: Persistent Subprocess + Telegram Utils Summary **Persistent Claude Code subprocess with stream-json I/O and Telegram message formatting utilities for bot integration** ## Performance - **Duration:** 5 minutes - **Started:** 2026-02-04T19:12:28Z - **Completed:** 2026-02-04T19:17:24Z - **Tasks:** 2 - **Files created:** 1 - **Files modified:** 1 ## Accomplishments - Refactored ClaudeSubprocess from fresh-process-per-turn to persistent process model - Eliminated ~1s spawn overhead per message turn - Added stream-json stdin I/O for NDJSON message delivery to persistent process - Implemented tool_use event parsing and on_tool_use callback for progress notifications - Created telegram_utils.py with smart message splitting, MarkdownV2 escaping, and typing indicator loop - Smart splitting respects code block boundaries (never splits inside triple-backtick blocks) - MarkdownV2 escaping preserves code blocks while escaping 17 special characters outside code - Typing indicator loop re-sends every 4 seconds with clean asyncio.Event cancellation ## Task Commits Each task was committed atomically: 1. **Task 1: Refactor ClaudeSubprocess to persistent process** - `6a115a4` (refactor) 2. **Task 2: Create telegram_utils.py** - `6b624d7` (feat) ## Files Created/Modified **Created:** - `telegram/telegram_utils.py` - Message formatting utilities: split_message_smart (code-block-aware splitting), escape_markdown_v2 (context-sensitive escaping), typing_indicator_loop (4s re-send pattern) **Modified:** - `telegram/claude_subprocess.py` - Refactored to persistent subprocess with --input-format stream-json stdin, persistent stdout/stderr readers, tool_use event parsing, NDJSON message delivery via stdin.write + drain pattern ## Decisions Made 1. **Persistent process architecture**: Spawn Claude Code once with stream-json I/O, write NDJSON to stdin for each turn. Eliminates ~1s spawn overhead per message and maintains conversation context across turns. 2. **Tool use callback design**: Extract tool name and raw input dict from content_block_start events, pass directly to callback. Bot layer (Plan 02) will format human-readable progress messages. Keeps subprocess layer format-agnostic. 3. **Smart splitting strategy**: Track code block state (in/out of triple backticks), split only when NOT in code block and would exceed 4000 char limit. If code block itself exceeds limit, include it whole (Telegram handles overflow). 4. **Escape only outside code**: Use regex to identify code regions (```...``` and `...`), escape MarkdownV2 special chars only in non-code text. Prevents breaking code syntax while properly escaping user-facing text. 5. **4000 char split threshold**: Use 4000 instead of Telegram's 4096 limit to leave room for MarkdownV2 escape character expansion (each special char becomes 2 chars with backslash). 6. **Typing loop cancellation pattern**: Use asyncio.wait_for with 4s timeout on Event.wait(). Clean cancellation when Event is set, automatic re-send on timeout. Pattern from PTB community best practices. ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered **Issue 1: ChatAction import path** - **Problem:** Initial import `from telegram import ChatAction` failed with ImportError - **Root cause:** python-telegram-bot v22+ moved ChatAction to telegram.constants - **Resolution:** Changed to `from telegram.constants import ChatAction` - **Impact:** Minor, caught during testing, fixed immediately ## User Setup Required None - no external service configuration required. ## Next Phase Readiness **Ready for Plan 02 (Bot Integration):** - ClaudeSubprocess provides persistent process with tool call notifications - telegram_utils provides all message formatting needed for bot layer - Smart splitting and escaping handle Claude's output for Telegram delivery - Typing indicator loop ready for integration with message handlers - stdin.write + drain pattern ensures no pipe deadlock **Integration points for Plan 02:** - Call `subprocess.start()` once per session - Pass `on_tool_use` callback to format and send progress messages - Use `split_message_smart()` on Claude output before sending to Telegram - Use `escape_markdown_v2()` if sending with MarkdownV2 parse mode - Wrap `subprocess.send_message()` with `typing_indicator_loop()` background task - Handle file uploads by saving to session directory - Implement message batching with debounce timer **No blockers or concerns.** --- *Phase: 02-telegram-integration* *Completed: 2026-02-04*