a digital person for bluesky

Fix X username caching at mention fetch time

**Root cause fixed:**
- get_mentions() was only returning mention data, losing user info from API response
- This caused "@unknown (unknown)" when processing mentions later

**Complete solution implemented:**

1. **Modified get_mentions() method**:
- Returns dict with both 'mentions' and 'users' keys
- Preserves user data from API response includes.users field
- Logs when user data is retrieved

2. **Enhanced save_mention_to_queue()**:
- Accepts optional users_data parameter
- Caches author username/name in 'author_info' field
- Preserves this data in queued mention JSON files

3. **Updated fetch_and_queue_mentions()**:
- Extracts both mentions and users from get_mentions() result
- Passes user data to save_mention_to_queue()
- Ensures usernames are cached when mentions arrive

4. **Improved process_x_mention()**:
- First tries cached author_info from queued mention
- Falls back to thread data lookup (existing logic)
- Finally falls back to thread tweet scan
- Three-tier fallback system for maximum reliability

**Benefits:**
- Fixes the root cause by capturing usernames when available from API
- No extra API calls needed
- Backward compatible with existing queued mentions
- Usernames reliably available even when thread context fails

This ensures usernames are properly cached when mentions are fetched, eliminating the "@unknown" issue that void was experiencing.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+73 -22
+73 -22
x.py
··· 144 144 145 145 return None 146 146 147 - def get_mentions(self, since_id: Optional[str] = None, max_results: int = 10) -> Optional[List[Dict]]: 147 + def get_mentions(self, since_id: Optional[str] = None, max_results: int = 10) -> Optional[Dict]: 148 148 """ 149 - Fetch mentions for the configured user. 149 + Fetch mentions for the configured user with user data. 150 150 151 151 Args: 152 152 since_id: Minimum Post ID to include (for getting newer mentions) 153 153 max_results: Number of results to return (5-100) 154 154 155 155 Returns: 156 - List of mention objects or None if request failed 156 + Dict with 'mentions' and 'users' keys, or None if request failed 157 157 """ 158 158 endpoint = f"/users/{self.user_id}/mentions" 159 159 params = { ··· 174 174 175 175 if response and "data" in response: 176 176 mentions = response["data"] 177 + users_data = {} 178 + 179 + # Extract user data from includes 180 + if "includes" in response and "users" in response["includes"]: 181 + for user in response["includes"]["users"]: 182 + users_data[user["id"]] = user 183 + logger.info(f"Retrieved user data for {len(users_data)} users") 184 + 177 185 logger.info(f"Retrieved {len(mentions)} mentions") 178 - return mentions 186 + return {"mentions": mentions, "users": users_data} 179 187 else: 180 188 if response: 181 189 logger.info(f"No mentions in response. Full response: {response}") 182 190 else: 183 191 logger.warning("Request failed - no response received") 184 - return [] 192 + return {"mentions": [], "users": {}} 185 193 186 194 def get_user_info(self, user_id: str) -> Optional[Dict]: 187 195 """Get information about a specific user.""" ··· 747 755 logger.info(f"Downranked user {user_id}: {'responding' if should_respond else 'skipping'} (10% chance)") 748 756 return should_respond 749 757 750 - def save_mention_to_queue(mention: Dict): 751 - """Save a mention to the queue directory for async processing.""" 758 + def save_mention_to_queue(mention: Dict, users_data: Optional[Dict] = None): 759 + """Save a mention to the queue directory for async processing with author info. 760 + 761 + Args: 762 + mention: The mention data from X API 763 + users_data: Optional dict mapping user IDs to user data (including usernames) 764 + """ 752 765 try: 753 766 mention_id = mention.get('id') 754 767 if not mention_id: ··· 771 784 772 785 queue_file = X_QUEUE_DIR / filename 773 786 787 + # Extract author info if users_data provided 788 + author_id = mention.get('author_id') 789 + author_username = None 790 + author_name = None 791 + 792 + if users_data and author_id and author_id in users_data: 793 + user_info = users_data[author_id] 794 + author_username = user_info.get('username') 795 + author_name = user_info.get('name') 796 + logger.info(f"Caching author info for @{author_username} ({author_name})") 797 + 774 798 # Save mention data with enhanced debugging information 775 799 mention_data = { 776 800 'mention': mention, 777 801 'queued_at': datetime.now().isoformat(), 778 802 'type': 'x_mention', 803 + # Cache author info for later use 804 + 'author_info': { 805 + 'username': author_username, 806 + 'name': author_name, 807 + 'id': author_id 808 + }, 779 809 # Debug info for conversation tracking 780 810 'debug_info': { 781 811 'mention_id': mention.get('id'), ··· 959 989 logger.info(f"Fetching mentions for @{username} since {last_seen_id or 'beginning'}") 960 990 961 991 # Search for mentions 962 - mentions = client.search_mentions( 963 - username=username, 992 + # Get mentions with user data 993 + result = client.get_mentions( 964 994 since_id=last_seen_id, 965 995 max_results=100 # Get as many as possible 966 996 ) 967 997 968 - if not mentions: 998 + if not result or not result["mentions"]: 969 999 logger.info("No new mentions found") 970 1000 return 0 971 1001 1002 + mentions = result["mentions"] 1003 + users_data = result["users"] 1004 + 972 1005 # Process mentions (newest first, so reverse to process oldest first) 973 1006 mentions.reverse() 974 1007 new_count = 0 975 1008 976 1009 for mention in mentions: 977 - save_mention_to_queue(mention) 1010 + save_mention_to_queue(mention, users_data) 978 1011 new_count += 1 979 1012 980 1013 # Update last seen ID to the most recent mention ··· 1271 1304 """Test the X client by fetching mentions.""" 1272 1305 try: 1273 1306 client = create_x_client() 1274 - mentions = client.get_mentions(max_results=5) 1307 + result = client.get_mentions(max_results=5) 1275 1308 1276 - if mentions: 1309 + if result and result["mentions"]: 1310 + mentions = result["mentions"] 1311 + users_data = result["users"] 1277 1312 print(f"Successfully retrieved {len(mentions)} mentions:") 1313 + print(f"User data available for {len(users_data)} users") 1278 1314 for mention in mentions: 1279 - print(f"- {mention.get('id')}: {mention.get('text')[:50]}...") 1315 + author_id = mention.get('author_id') 1316 + author_info = users_data.get(author_id, {}) 1317 + username = author_info.get('username', 'unknown') 1318 + print(f"- {mention.get('id')} from @{username}: {mention.get('text')[:50]}...") 1280 1319 else: 1281 1320 print("No mentions retrieved") 1282 1321 ··· 1513 1552 # Continue without user blocks rather than failing completely 1514 1553 1515 1554 # Create prompt for Letta agent 1516 - author_info = thread_data.get('users', {}).get(author_id, {}) 1517 - author_username = author_info.get('username', 'unknown') 1518 - author_name = author_info.get('name', author_username) 1555 + # First try to use cached author info from queued mention 1556 + author_username = 'unknown' 1557 + author_name = 'unknown' 1558 + 1559 + if 'author_info' in mention_data: 1560 + # Use cached author info from when mention was queued 1561 + cached_info = mention_data['author_info'] 1562 + if cached_info.get('username'): 1563 + author_username = cached_info['username'] 1564 + author_name = cached_info.get('name', author_username) 1565 + logger.info(f"Using cached author info: @{author_username} ({author_name})") 1566 + 1567 + # If not cached, try thread data 1568 + if author_username == 'unknown': 1569 + author_info = thread_data.get('users', {}).get(author_id, {}) 1570 + author_username = author_info.get('username', 'unknown') 1571 + author_name = author_info.get('name', author_username) 1519 1572 1520 - # Fallback: if username is unknown, try to find it in the thread tweets 1573 + # Final fallback: if username is still unknown, try to find it in the thread tweets 1521 1574 if author_username == 'unknown' and 'tweets' in thread_data: 1522 1575 for tweet in thread_data['tweets']: 1523 1576 if tweet.get('author_id') == author_id and 'author' in tweet: ··· 1916 1969 with open(filepath, 'r') as f: 1917 1970 queue_data = json.load(f) 1918 1971 1919 - mention_data = queue_data.get('mention', queue_data) 1920 - 1921 - # Process the mention 1922 - success = process_x_mention(void_agent, x_client, mention_data, 1972 + # Process the mention (pass full queue_data to have access to author_info) 1973 + success = process_x_mention(void_agent, x_client, queue_data, 1923 1974 queue_filepath=filepath, testing_mode=testing_mode) 1924 1975 1925 1976 except XRateLimitError: