···131314141515class MessageHandler:
1616- """Handles incoming mentions using phi agent."""
1616+ """Handles incoming notifications using phi agent."""
17171818 def __init__(self, client: BotClient):
1919 self.client = client
2020 self.agent = PhiAgent()
21212222- async def handle_mention(self, notification):
2323- """Process a mention or reply notification."""
2222+ async def handle_notification(self, notification):
2323+ """Process any notification type."""
2424+ reason = notification.reason
2525+ author_handle = notification.author.handle
2626+2427 try:
2525- if notification.reason not in ["mention", "reply"]:
2626- return
2828+ if reason in ("mention", "reply", "quote"):
2929+ await self._handle_post(notification)
3030+ elif reason in ("like", "repost"):
3131+ # someone engaged with phi's content — log for awareness
3232+ logger.info(f"{reason} from @{author_handle}")
3333+ bot_status.record_mention()
3434+ elif reason == "follow":
3535+ logger.info(f"followed by @{author_handle}")
3636+ bot_status.record_mention()
3737+ else:
3838+ logger.debug(f"notification type '{reason}' from @{author_handle}")
3939+ except Exception as e:
4040+ logger.exception(f"notification handling error: {e}")
4141+ bot_status.record_error()
27422828- post_uri = notification.uri
4343+ async def _handle_post(self, notification):
4444+ """Process a mention, reply, or quote notification."""
4545+ post_uri = notification.uri
29463030- # Get the post that mentioned us
3131- posts = await self.client.get_posts([post_uri])
3232- if not posts.posts:
3333- logger.warning(f"could not find post {post_uri}")
3434- return
4747+ # Get the post
4848+ posts = await self.client.get_posts([post_uri])
4949+ if not posts.posts:
5050+ logger.warning(f"could not find post {post_uri}")
5151+ return
35523636- post = posts.posts[0]
3737- mention_text = post.record.text
3838- author_handle = post.author.handle
5353+ post = posts.posts[0]
5454+ mention_text = post.record.text
5555+ author_handle = post.author.handle
39564040- # Include embed content (images, links, quote posts) in the mention
4141- embed = post.embed if hasattr(post, "embed") and post.embed else None
4242- if not embed and hasattr(post.record, "embed") and post.record.embed:
4343- embed = post.record.embed
5757+ # Include embed content (images, links, quote posts) in the mention
5858+ embed = post.embed if hasattr(post, "embed") and post.embed else None
5959+ if not embed and hasattr(post.record, "embed") and post.record.embed:
6060+ embed = post.record.embed
44614545- embed_desc = describe_embed(embed) if embed else None
4646- if embed_desc:
4747- mention_text = f"{mention_text}\n{embed_desc}"
6262+ embed_desc = describe_embed(embed) if embed else None
6363+ if embed_desc:
6464+ mention_text = f"{mention_text}\n{embed_desc}"
48654949- # Extract image URLs for multimodal vision
5050- image_urls = extract_image_urls(embed) if embed else []
6666+ # Extract image URLs for multimodal vision
6767+ image_urls = extract_image_urls(embed) if embed else []
51685252- bot_status.record_mention()
6969+ bot_status.record_mention()
53705454- # Build reply reference
5555- parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=post.cid)
7171+ # Build reply reference
7272+ parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=post.cid)
56735757- # Check if this is part of a thread
5858- if hasattr(post.record, "reply") and post.record.reply:
5959- root_ref = post.record.reply.root
6060- thread_uri = root_ref.uri
6161- else:
6262- root_ref = parent_ref
6363- thread_uri = post_uri
7474+ # Check if this is part of a thread
7575+ if hasattr(post.record, "reply") and post.record.reply:
7676+ root_ref = post.record.reply.root
7777+ thread_uri = root_ref.uri
7878+ else:
7979+ root_ref = parent_ref
8080+ thread_uri = post_uri
64816565- # Fetch thread context directly from network
6666- thread_context = "No previous messages in this thread."
6767- try:
6868- logger.debug(f"fetching thread context for {thread_uri}")
6969- thread_data = await self.client.get_thread(thread_uri, depth=100)
7070- thread_context = build_thread_context(thread_data.thread)
7171- except Exception as e:
7272- logger.warning(f"failed to fetch thread context: {e}")
8282+ # Fetch thread context directly from network
8383+ thread_context = "No previous messages in this thread."
8484+ try:
8585+ logger.debug(f"fetching thread context for {thread_uri}")
8686+ thread_data = await self.client.get_thread(thread_uri, depth=100)
8787+ thread_context = build_thread_context(thread_data.thread)
8888+ except Exception as e:
8989+ logger.warning(f"failed to fetch thread context: {e}")
73907474- # Process with agent (has episodic memory + MCP tools)
7575- response = await self.agent.process_mention(
7676- mention_text=mention_text,
7777- author_handle=author_handle,
7878- thread_context=thread_context,
7979- thread_uri=thread_uri,
8080- image_urls=image_urls,
8181- )
9191+ # Process with agent (has episodic memory + MCP tools)
9292+ response = await self.agent.process_mention(
9393+ mention_text=mention_text,
9494+ author_handle=author_handle,
9595+ thread_context=thread_context,
9696+ thread_uri=thread_uri,
9797+ image_urls=image_urls,
9898+ )
82998383- # Handle response actions
8484- if response.action == "ignore":
8585- logger.info(f"ignoring @{author_handle}: {response.reason}")
8686- return
100100+ # Handle response actions
101101+ if response.action == "ignore":
102102+ logger.info(f"ignoring @{author_handle}: {response.reason}")
103103+ return
871048888- elif response.action == "like":
8989- await self.client.like_post(uri=post_uri, cid=post.cid)
9090- logger.info(f"liked @{author_handle}")
9191- bot_status.record_response()
9292- return
9393-9494- elif response.action == "repost":
9595- await self.client.repost(uri=post_uri, cid=post.cid)
9696- logger.info(f"reposted @{author_handle}")
9797- bot_status.record_response()
9898- return
105105+ elif response.action == "like":
106106+ await self.client.like_post(uri=post_uri, cid=post.cid)
107107+ logger.info(f"liked @{author_handle}")
108108+ bot_status.record_response()
109109+ return
99110100100- elif response.action == "reply" and response.text:
101101- # Post reply
102102- reply_ref = models.AppBskyFeedPost.ReplyRef(
103103- parent=parent_ref, root=root_ref
104104- )
105105- await self.client.create_post(response.text, reply_to=reply_ref)
111111+ elif response.action == "repost":
112112+ await self.client.repost(uri=post_uri, cid=post.cid)
113113+ logger.info(f"reposted @{author_handle}")
114114+ bot_status.record_response()
115115+ return
106116107107- bot_status.record_response()
108108- logger.info(f"replied to @{author_handle}: {response.text[:80]}")
117117+ elif response.action == "reply" and response.text:
118118+ # Post reply
119119+ reply_ref = models.AppBskyFeedPost.ReplyRef(
120120+ parent=parent_ref, root=root_ref
121121+ )
122122+ await self.client.create_post(response.text, reply_to=reply_ref)
109123110110- except Exception as e:
111111- logger.exception(f"mention handling error: {e}")
112112- bot_status.record_error()
124124+ bot_status.record_response()
125125+ logger.info(f"replied to @{author_handle}: {response.text[:80]}")
+9-15
src/bot/services/notification_poller.py
···6565 response = await self.client.get_notifications()
6666 notifications = response.notifications
67676868- unread_mentions = [
6969- n
7070- for n in notifications
7171- if not n.is_read and n.reason in ["mention", "reply"]
7272- ]
6868+ unread = [n for n in notifications if not n.is_read]
73697470 # First poll: show initial state
7571 if self._first_poll:
7672 self._first_poll = False
7773 if notifications:
7874 logger.info(
7979- f"found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)"
7575+ f"found {len(notifications)} notifications ({len(unread)} unread)"
8076 )
8181- elif unread_mentions:
8282- logger.info(f"{len(unread_mentions)} new mentions")
7777+ elif unread:
7878+ logger.info(f"{len(unread)} new notifications")
83798484- processed_any_mentions = False
8080+ processed_any = False
85818682 # Process notifications from oldest to newest
8783 for notification in reversed(notifications):
8884 if notification.is_read or notification.uri in self._processed_uris:
8985 continue
90869191- if notification.reason in ["mention", "reply"]:
9292- logger.debug(f"processing {notification.reason} notification")
9393- self._processed_uris.add(notification.uri)
9494- await self.handler.handle_mention(notification)
9595- processed_any_mentions = True
8787+ self._processed_uris.add(notification.uri)
8888+ await self.handler.handle_notification(notification)
8989+ processed_any = True
96909791 # Mark all notifications as seen
9898- if processed_any_mentions:
9292+ if processed_any:
9993 await self.client.mark_notifications_seen(check_time)
10094 logger.info("marked notifications as read")
10195