diff --git a/.planning/phases/02-telegram-integration/02-01-PLAN.md b/.planning/phases/02-telegram-integration/02-01-PLAN.md index 1519546..5375cc0 100644 --- a/.planning/phases/02-telegram-integration/02-01-PLAN.md +++ b/.planning/phases/02-telegram-integration/02-01-PLAN.md @@ -12,7 +12,7 @@ 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" + - "Subprocess emits tool_use events (tool_name + tool_input dict) that bot.py can consume 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" @@ -86,6 +86,7 @@ Refactor ClaudeSubprocess to maintain a single long-lived process per session us 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. + **Responsibility split:** `claude_subprocess.py` extracts `tool_name` (e.g. "Read", "Bash", "Edit") and passes the raw `tool_input` dict (e.g. `{"file_path": "/foo/bar"}`, `{"command": "ls -la"}`) directly to the callback. It does NOT format human-readable descriptions -- that is bot.py's job in Plan 02 (Part B, item 5). This keeps the subprocess layer format-agnostic. 4. **Update _handle_stdout_line():** Handle the stream-json event types from persistent mode: - `assistant`: Extract text blocks, call on_output with final text @@ -127,7 +128,7 @@ Refactor ClaudeSubprocess to maintain a single long-lived process per session us 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) +6. Verify `_read_stdout()` loop continues after result events: inspect code to confirm the loop only exits on empty readline (i.e. `line = await self._process.stdout.readline(); if not line: break`), NOT on receiving a result event. Result events should set `_busy = False` and call `on_complete` but NOT break the reader loop. 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. diff --git a/.planning/phases/02-telegram-integration/02-02-PLAN.md b/.planning/phases/02-telegram-integration/02-02-PLAN.md index 71fd6c2..6db5072 100644 --- a/.planning/phases/02-telegram-integration/02-02-PLAN.md +++ b/.planning/phases/02-telegram-integration/02-02-PLAN.md @@ -150,9 +150,29 @@ Overhaul the message handler to use typing indicators and message batching: - 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) +3. Subprocess auto-start (integrate into existing handle_message after session lookup, before batcher): + ```python + # 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()**