From 89cf362790034044e34a610def05227e47987ed0 Mon Sep 17 00:00:00 2001 From: Richard A Date: Thu, 4 Sep 2025 09:21:15 +0400 Subject: [PATCH] Fix conversation context loss between Telegram messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bot was losing conversation context between messages because the session manager was incorrectly creating new sessions even when valid session IDs were provided. This happened when sessions weren't in the active cache (e.g., after bot restart). Key fixes: - Properly distinguish between resuming existing Claude sessions vs starting fresh - Load existing sessions from storage with is_new_session=False - Add comprehensive logging for session flow debugging - Use --resume flag correctly for continuing conversations - Handle both temporary and Claude-generated session IDs appropriately Verified with comprehensive test that confirms: ✅ New sessions start correctly without --resume ✅ Follow-up messages use --resume with session ID ✅ Sessions persist to storage properly ✅ Sessions resume correctly after simulated bot restart Fixes: https://github.com/RichardAtCT/claude-code-telegram/issues/5 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/bot/handlers/callback.py | 6 ++-- src/claude/facade.py | 29 ++++++++++++++++++ src/claude/integration.py | 1 + src/claude/sdk_integration.py | 1 + src/claude/session.py | 58 ++++++++++++++++++++++++++++------- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/bot/handlers/callback.py b/src/bot/handlers/callback.py index b82ad0a..ef13a33 100644 --- a/src/bot/handlers/callback.py +++ b/src/bot/handlers/callback.py @@ -1004,8 +1004,10 @@ async def handle_git_callback( else: # Clean up diff output for Telegram # Remove emoji symbols that interfere with markdown parsing - clean_diff = diff_output.replace("➕", "+").replace("➖", "-").replace("📍", "@") - + clean_diff = ( + diff_output.replace("➕", "+").replace("➖", "-").replace("📍", "@") + ) + # Limit diff output max_length = 2000 if len(clean_diff) > max_length: diff --git a/src/claude/facade.py b/src/claude/facade.py index bf14258..2eb5b1b 100644 --- a/src/claude/facade.py +++ b/src/claude/facade.py @@ -70,6 +70,15 @@ async def run_command( user_id, working_directory, session_id ) + # Log session details for debugging + logger.info( + "Session retrieved for command", + session_id=session.session_id, + is_new_session=getattr(session, "is_new_session", False), + provided_session_id=session_id, + user_id=user_id, + ) + # Track streaming updates and validate tool calls tools_validated = True validation_errors = [] @@ -143,6 +152,15 @@ async def stream_handler(update: StreamUpdate): else session.session_id ) + logger.info( + "Claude command execution parameters", + should_continue_session=should_continue, + claude_session_id=claude_session_id, + original_session_id=session_id, + is_new_session=getattr(session, "is_new_session", False), + user_id=user_id, + ) + response = await self._execute_with_fallback( prompt=prompt, working_directory=working_directory, @@ -197,9 +215,20 @@ async def stream_handler(update: StreamUpdate): if hasattr(session, "is_new_session") and response.session_id: # The session_id has been updated to Claude's session_id final_session_id = response.session_id + logger.info( + "Session ID updated from temporary to Claude session ID", + old_session_id=old_session_id, + new_session_id=response.session_id, + user_id=user_id, + ) else: # Use the original session_id for continuing sessions final_session_id = old_session_id + logger.debug( + "Using existing session ID", + session_id=final_session_id, + user_id=user_id, + ) # Ensure response has the correct session_id response.session_id = final_session_id diff --git a/src/claude/integration.py b/src/claude/integration.py index ed857f7..7760171 100644 --- a/src/claude/integration.py +++ b/src/claude/integration.py @@ -188,6 +188,7 @@ def _build_command( elif session_id and prompt and continue_session: # Follow-up message in existing session - use resume with new prompt cmd.extend(["--resume", session_id, "-p", prompt]) + logger.info("Using --resume for continuing session", session_id=session_id) elif prompt: # New session with prompt (including new sessions with session_id) cmd.extend(["-p", prompt]) diff --git a/src/claude/sdk_integration.py b/src/claude/sdk_integration.py index 9ccf964..df074b0 100644 --- a/src/claude/sdk_integration.py +++ b/src/claude/sdk_integration.py @@ -167,6 +167,7 @@ async def execute_command( working_directory=str(working_directory), session_id=session_id, continue_session=continue_session, + session_continuation_mode="continue" if continue_session else "new", ) try: diff --git a/src/claude/session.py b/src/claude/session.py index 2d8921f..464180e 100644 --- a/src/claude/session.py +++ b/src/claude/session.py @@ -174,22 +174,58 @@ async def get_or_create_session( session_id=session_id, ) - # Check for existing session - if session_id and session_id in self.active_sessions: - session = self.active_sessions[session_id] - if not session.is_expired(self.config.session_timeout_hours): - logger.debug("Using active session", session_id=session_id) - return session - - # Try to load from storage - if session_id: + # If session_id is provided, try to resume existing session + if session_id and not session_id.startswith("temp_"): + logger.debug("Attempting to resume existing session", session_id=session_id) + + # Check active sessions first + if session_id in self.active_sessions: + session = self.active_sessions[session_id] + if not session.is_expired(self.config.session_timeout_hours): + logger.debug("Using active session", session_id=session_id) + return session + else: + logger.info( + "Active session expired, will try storage", + session_id=session_id, + ) + + # Try to load from storage session = await self.storage.load_session(session_id) if session and not session.is_expired(self.config.session_timeout_hours): + # This is an existing Claude session, NOT a new session + session.is_new_session = False self.active_sessions[session_id] = session - logger.info("Loaded session from storage", session_id=session_id) + logger.info( + "Successfully loaded existing session from storage", + session_id=session_id, + ) + return session + elif session: + logger.info( + "Session found in storage but expired", + session_id=session_id, + age_hours=(datetime.utcnow() - session.last_used).total_seconds() + / 3600, + ) + else: + logger.warning( + "Session not found in storage, will create new session", + session_id=session_id, + ) + + # Check for existing temporary session in active memory + if ( + session_id + and session_id.startswith("temp_") + and session_id in self.active_sessions + ): + session = self.active_sessions[session_id] + if not session.is_expired(self.config.session_timeout_hours): + logger.debug("Using active temporary session", session_id=session_id) return session - # Check user session limit + # Check user session limit before creating new session user_sessions = await self._get_user_sessions(user_id) if len(user_sessions) >= self.config.max_sessions_per_user: # Remove oldest session