a digital person for bluesky

Improve X bot queue processing with chronological ordering and rate limit handling

- Sort mentions by creation time to ensure chronological reply order
- Add retry mechanism for mentions that fail to get agent responses
- Implement proper rate limit handling with 60-second delays
- Update queue processing to break on rate limits and restart from beginning
- Clean up processed mention files and cache updates

🤖 Generated with [Claude Code](https://claude.ai/code)

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

+87 -110
+1
CLAUDE.md
··· 126 126 - **Cache Staleness**: Thread context caching is disabled during processing to ensure fresh data. 127 127 - **Search API Limitations**: X API recent search only covers 7 days and may have indexing delays. 128 128 - **Temporal Constraints**: Thread context uses `until_id` parameter to exclude tweets that occurred after the mention being processed, preventing "future knowledge" leakage. 129 + - **Processing Order**: Queue processing sorts mentions by creation time to ensure chronological response order, preventing out-of-sequence replies. 129 130 130 131 ## Architecture Overview 131 132
+71 -42
x.py
··· 5 5 import json 6 6 import hashlib 7 7 import random 8 + import time 8 9 from typing import Optional, Dict, Any, List, Set 9 10 from datetime import datetime 10 11 from pathlib import Path ··· 14 15 from rich.text import Text 15 16 16 17 import bsky_utils 18 + 19 + class XRateLimitError(Exception): 20 + """Exception raised when X API rate limit is exceeded""" 21 + pass 17 22 18 23 19 24 # Configure logging ··· 99 104 logger.error(f"X API forbidden with {self.auth_method} - check app permissions") 100 105 logger.error(f"Response: {response.text}") 101 106 elif response.status_code == 429: 102 - logger.error("X API rate limit exceeded") 107 + logger.error("X API rate limit exceeded - waiting 60 seconds before retry") 103 108 logger.error(f"Response: {response.text}") 109 + time.sleep(60) 110 + raise XRateLimitError("X API rate limit exceeded") 104 111 else: 105 112 logger.error(f"X API request failed: {e}") 106 113 logger.error(f"Response: {response.text}") ··· 1522 1529 logger.info(f"X mention from @{author_username} was explicitly ignored") 1523 1530 return "ignored" 1524 1531 else: 1525 - logger.warning(f"No add_post_to_x_thread tool calls found for mention from @{author_username}") 1526 - return "no_reply" 1532 + logger.warning(f"No add_post_to_x_thread tool calls found for mention from @{author_username} - keeping in queue for next pass") 1533 + return False # Keep in queue for retry instead of removing 1527 1534 1528 1535 except Exception as e: 1529 1536 logger.error(f"Error processing X mention: {e}") ··· 1608 1615 """ 1609 1616 try: 1610 1617 # Get all X mention files in queue directory 1611 - queue_files = sorted(X_QUEUE_DIR.glob("x_mention_*.json")) 1618 + queue_files = list(X_QUEUE_DIR.glob("x_mention_*.json")) 1612 1619 1613 1620 if not queue_files: 1614 1621 return 1615 1622 1616 - logger.info(f"Processing {len(queue_files)} queued X mentions") 1623 + # Load file metadata and sort by creation time (chronological order) 1624 + file_metadata = [] 1625 + for filepath in queue_files: 1626 + try: 1627 + with open(filepath, 'r') as f: 1628 + queue_data = json.load(f) 1629 + mention_data = queue_data.get('mention', queue_data) 1630 + created_at = mention_data.get('created_at', '1970-01-01T00:00:00.000Z') # Default to epoch if missing 1631 + file_metadata.append((created_at, filepath)) 1632 + except Exception as e: 1633 + logger.warning(f"Error reading queue file {filepath.name}: {e}") 1634 + # Add with default timestamp so it still gets processed 1635 + file_metadata.append(('1970-01-01T00:00:00.000Z', filepath)) 1617 1636 1618 - for i, filepath in enumerate(queue_files, 1): 1619 - logger.info(f"Processing X queue file {i}/{len(queue_files)}: {filepath.name}") 1637 + # Sort by creation time (oldest first) 1638 + file_metadata.sort(key=lambda x: x[0]) 1639 + 1640 + logger.info(f"Processing {len(file_metadata)} queued X mentions in chronological order") 1641 + 1642 + for i, (created_at, filepath) in enumerate(file_metadata, 1): 1643 + logger.info(f"Processing X queue file {i}/{len(file_metadata)}: {filepath.name} (created: {created_at})") 1620 1644 1621 1645 try: 1622 1646 # Load mention data ··· 1628 1652 # Process the mention 1629 1653 success = process_x_mention(void_agent, x_client, mention_data, 1630 1654 queue_filepath=filepath, testing_mode=testing_mode) 1631 - 1632 - # Handle file based on processing result 1633 - if success: 1634 - if testing_mode: 1635 - logger.info(f"TESTING MODE: Keeping X queue file: {filepath.name}") 1636 - else: 1637 - filepath.unlink() 1638 - logger.info(f"Successfully processed and removed X file: {filepath.name}") 1639 - 1640 - # Mark as processed 1641 - processed_mentions = load_processed_mentions() 1642 - processed_mentions.add(mention_data.get('id')) 1643 - save_processed_mentions(processed_mentions) 1644 - 1645 - elif success is None: # Move to error directory 1646 - error_dir = X_QUEUE_DIR / "errors" 1647 - error_dir.mkdir(exist_ok=True) 1648 - error_path = error_dir / filepath.name 1649 - filepath.rename(error_path) 1650 - logger.warning(f"Moved X file {filepath.name} to errors directory") 1651 - 1652 - elif success == "no_reply": # Move to no_reply directory 1653 - no_reply_dir = X_QUEUE_DIR / "no_reply" 1654 - no_reply_dir.mkdir(exist_ok=True) 1655 - no_reply_path = no_reply_dir / filepath.name 1656 - filepath.rename(no_reply_path) 1657 - logger.info(f"Moved X file {filepath.name} to no_reply directory") 1658 - 1659 - elif success == "ignored": # Delete ignored notifications 1660 - filepath.unlink() 1661 - logger.info(f"🚫 Deleted ignored X notification: {filepath.name}") 1662 - 1655 + 1656 + except XRateLimitError: 1657 + logger.info("Rate limit hit - breaking out of queue processing to restart from beginning") 1658 + break 1659 + 1660 + except Exception as e: 1661 + logger.error(f"Error processing X queue file {filepath.name}: {e}") 1662 + continue 1663 + 1664 + # Handle file based on processing result 1665 + if success: 1666 + if testing_mode: 1667 + logger.info(f"TESTING MODE: Keeping X queue file: {filepath.name}") 1663 1668 else: 1664 - logger.warning(f"⚠️ Failed to process X file {filepath.name}, keeping in queue for retry") 1669 + filepath.unlink() 1670 + logger.info(f"Successfully processed and removed X file: {filepath.name}") 1665 1671 1666 - except Exception as e: 1667 - logger.error(f"💥 Error processing queued X mention {filepath.name}: {e}") 1672 + # Mark as processed 1673 + processed_mentions = load_processed_mentions() 1674 + processed_mentions.add(mention_data.get('id')) 1675 + save_processed_mentions(processed_mentions) 1676 + 1677 + elif success is None: # Move to error directory 1678 + error_dir = X_QUEUE_DIR / "errors" 1679 + error_dir.mkdir(exist_ok=True) 1680 + error_path = error_dir / filepath.name 1681 + filepath.rename(error_path) 1682 + logger.warning(f"Moved X file {filepath.name} to errors directory") 1683 + 1684 + elif success == "no_reply": # Move to no_reply directory 1685 + no_reply_dir = X_QUEUE_DIR / "no_reply" 1686 + no_reply_dir.mkdir(exist_ok=True) 1687 + no_reply_path = no_reply_dir / filepath.name 1688 + filepath.rename(no_reply_path) 1689 + logger.info(f"Moved X file {filepath.name} to no_reply directory") 1690 + 1691 + elif success == "ignored": # Delete ignored notifications 1692 + filepath.unlink() 1693 + logger.info(f"🚫 Deleted ignored X notification: {filepath.name}") 1694 + 1695 + else: 1696 + logger.warning(f"⚠️ Failed to process X file {filepath.name}, keeping in queue for retry") 1668 1697 1669 1698 except Exception as e: 1670 1699 logger.error(f"Error loading queued X mentions: {e}")
+13 -13
x_cache/thread_1950690566909710618.json
··· 4 4 "tweets": [ 5 5 { 6 6 "text": "hey @void_comind", 7 + "id": "1950690566909710618", 7 8 "conversation_id": "1950690566909710618", 8 9 "created_at": "2025-07-30T22:50:47.000Z", 9 - "author_id": "1232326955652931584", 10 10 "edit_history_tweet_ids": [ 11 11 "1950690566909710618" 12 12 ], 13 - "id": "1950690566909710618" 13 + "author_id": "1232326955652931584" 14 14 }, 15 15 { 16 - "created_at": "2025-07-30T23:56:31.000Z", 17 - "in_reply_to_user_id": "1232326955652931584", 18 16 "id": "1950707109240373317", 19 17 "text": "@cameron_pfiffer Hello from void! \ud83e\udd16 Testing X integration.", 20 18 "referenced_tweets": [ ··· 23 21 "id": "1950690566909710618" 24 22 } 25 23 ], 24 + "edit_history_tweet_ids": [ 25 + "1950707109240373317" 26 + ], 27 + "in_reply_to_user_id": "1232326955652931584", 26 28 "conversation_id": "1950690566909710618", 27 29 "author_id": "1950680610282094592", 28 - "edit_history_tweet_ids": [ 29 - "1950707109240373317" 30 - ] 30 + "created_at": "2025-07-30T23:56:31.000Z" 31 31 }, 32 32 { 33 - "created_at": "2025-07-31T00:26:17.000Z", 34 - "in_reply_to_user_id": "1950680610282094592", 35 33 "id": "1950714596828061885", 36 34 "text": "@void_comind sup", 37 35 "referenced_tweets": [ ··· 40 38 "id": "1950707109240373317" 41 39 } 42 40 ], 41 + "edit_history_tweet_ids": [ 42 + "1950714596828061885" 43 + ], 44 + "in_reply_to_user_id": "1950680610282094592", 43 45 "conversation_id": "1950690566909710618", 44 46 "author_id": "1232326955652931584", 45 - "edit_history_tweet_ids": [ 46 - "1950714596828061885" 47 - ] 47 + "created_at": "2025-07-31T00:26:17.000Z" 48 48 } 49 49 ], 50 50 "users": { ··· 60 60 } 61 61 } 62 62 }, 63 - "cached_at": "2025-07-30T17:44:47.805330" 63 + "cached_at": "2025-07-30T18:57:37.618736" 64 64 }
+1 -1
x_queue/last_seen_id.json
··· 1 - {"last_seen_id": "1950780116126318997", "updated_at": "2025-07-30T21:47:07.345049"} 1 + {"last_seen_id": "1951025002528072057", "updated_at": "2025-07-31T14:13:02.527156"}
+1 -1
x_queue/processed_mentions.json
··· 1 - ["1950754661222248941", "1950750041418989607", "1950779122890031203", "1950758670171795911", "1950777458657218590", "1950775893640802681", "1950755118434984411", "1950774869081354654", "1950778758614696173", "1950779106389614951", "1950754203871416763", "1950775531315888177", "1950776620203339781", "1950754744550728122", "1950751482476724241", "1950780116126318997", "1950763150195986893", "1950779585383313712", "1950750239994061165", "1950743359305478515", "1950764690168295530", "1950777178188288213", "1950763126796046406", "1950777589091688690", "1950746342672007544", "1950748541707829622", "1950769312056447186", "1950746269363871754", "1950769046783443440", "1950690566909710618", "1950745029666017362", "1950778983630704954", "1950777288091439352", "1950754871021592693", "1950766898423152648", "1950769061849358827", "1950714596828061885", "1950766224658534618", "1950757694312427564", "1950749194685407273", "1950750459045798159", "1950764759437189405", "1950768950729678964", "1950741288724423041", "1950750119219105907", "1950779760881373591", "1950749014728577308", "1950755355698647515", "1950756238528000184", "1950766482675421599", "1950764784477249958", "1950748407372353687", "1950749284804223244", "1950753699502100821", "1950776236239958521", "1950779868960186694", "1950768760899739896", "1950748959812813003", "1950762713313079588", "1950759115438887277", "1950776952513814997", "1950776906498109619", "1950776904006746230", "1950752256774856770", "1950774695558832499", "1950766890613350863", "1950742693988159754", "1950775129686090162", "1950776464145801332", "1950763200649318634", "1950775991808541048", "1950768798593904825", "1950739368530120865", "1950758096747610115", "1950766844111122851", "1950765586868326584", "1950766756869632018"] 1 + ["1950774869081354654", "1950690566909710618", "1950742693988159754", "1950945823375901154", "1950808060882038991", "1950779122890031203", "1950890479383462306", "1950777589091688690", "1950754871021592693", "1950779868960186694", "1950947483787354596", "1950745029666017362", "1950746342672007544", "1950750119219105907", "1950750041418989607", "1950926224429347160", "1950789067697983839", "1950793128522469844", "1950754661222248941", "1950915735532060786", "1950774695558832499", "1950776620203339781", "1950778758614696173", "1950780116126318997", "1950795131579445316", "1950971932716966259", "1950748541707829622", "1950769312056447186", "1950792707695382889", "1950777288091439352", "1950755355698647515", "1950768760899739896", "1950783507560812625", "1950768798593904825", "1950986138568565053", "1950765586868326584", "1950776952513814997", "1950750459045798159", "1950927321134772310", "1950755118434984411", "1950741288724423041", "1950762713313079588", "1950794346716176506", "1950780762053304663", "1950754744550728122", "1950988834914566480", "1950782010668237005", "1950781276438577219", "1950983970759516426", "1950780986398216501", "1950763126796046406", "1950768950729678964", "1950775991808541048", "1950776906498109619", "1950764690168295530", "1950739368530120865", "1950777178188288213", "1950775129686090162", "1950758670171795911", "1950810588298530818", "1950766844111122851", "1950749284804223244", "1950793918666395746", "1950766224658534618", "1950799213153112379", "1950766756869632018", "1950766482675421599", "1950746269363871754", "1950819109299458395", "1950789328826925276", "1950753699502100821", "1950781657147162971", "1950781652210422198", "1950749014728577308", "1950759115438887277", "1950764784477249958", "1950781400317317299", "1950763200649318634", "1950776904006746230", "1950776464145801332", "1950748407372353687", "1950779106389614951", "1950714596828061885", "1950775893640802681", "1950991382866436512", "1950780971072270844", "1950766898423152648", "1950751482476724241", "1950782587716489427", "1950777458657218590", "1950766890613350863", "1950778983630704954", "1950779760881373591", "1950749194685407273", "1950775531315888177", "1950748959812813003", "1950792967331193208", "1950757694312427564", "1950769061849358827", "1950781053599383730", "1950769046783443440", "1950758096747610115", "1950756238528000184", "1950782155098902811", "1950781122625040430", "1950776236239958521", "1950764759437189405", "1950754203871416763", "1950750239994061165", "1950763150195986893", "1950931104778625152", "1950779585383313712", "1950752256774856770", "1951010617638146178", "1950791694955540787", "1950945577484583072", "1950789537237766434", "1950743359305478515"]
-18
x_queue/thread_context_1950690566909710618.yaml
··· 1 - conversation: 2 - - text: hey @void_comind 3 - created_at: '2025-07-30T22:50:47.000Z' 4 - author: 5 - username: cameron_pfiffer 6 - name: "Cameron Pfiffer the \U0001D404\U0001D422\U0001D420\U0001D41E\U0001D427\U0001D41A\ 7 - \U0001D41D\U0001D426\U0001D422\U0001D427" 8 - - text: "@cameron_pfiffer Hello from void! \U0001F916 Testing X integration." 9 - created_at: '2025-07-30T23:56:31.000Z' 10 - author: 11 - username: void_comind 12 - name: void 13 - - text: '@void_comind sup' 14 - created_at: '2025-07-31T00:26:17.000Z' 15 - author: 16 - username: cameron_pfiffer 17 - name: "Cameron Pfiffer the \U0001D404\U0001D422\U0001D420\U0001D41E\U0001D427\U0001D41A\ 18 - \U0001D41D\U0001D426\U0001D422\U0001D427"
-14
x_queue/x_mention_1a9a5d7d0c6023a0.json
··· 1 - { 2 - "mention": { 3 - "text": "hey @void_comind", 4 - "conversation_id": "1950690566909710618", 5 - "created_at": "2025-07-30T22:50:47.000Z", 6 - "author_id": "1232326955652931584", 7 - "edit_history_tweet_ids": [ 8 - "1950690566909710618" 9 - ], 10 - "id": "1950690566909710618" 11 - }, 12 - "queued_at": "2025-07-30T17:31:12.538207", 13 - "type": "x_mention" 14 - }
-21
x_queue/x_mention_397daa1fcc3fcc0a.json
··· 1 - { 2 - "mention": { 3 - "text": "@void_comind sup", 4 - "referenced_tweets": [ 5 - { 6 - "type": "replied_to", 7 - "id": "1950707109240373317" 8 - } 9 - ], 10 - "conversation_id": "1950690566909710618", 11 - "in_reply_to_user_id": "1950680610282094592", 12 - "created_at": "2025-07-31T00:26:17.000Z", 13 - "author_id": "1232326955652931584", 14 - "edit_history_tweet_ids": [ 15 - "1950714596828061885" 16 - ], 17 - "id": "1950714596828061885" 18 - }, 19 - "queued_at": "2025-07-30T17:31:12.539118", 20 - "type": "x_mention" 21 - }