@@ -109,28 +109,41 @@ async def fetch_messages_data_by_ids(
109109 f"Some messages({ message_ids } ) not found in database: { e } "
110110 )
111111
112- if not ordered_messages :
112+ return await hydrate_message_parts (ordered_messages , user_kek = user_kek )
113+
114+ except Exception as e :
115+ return Result .reject (f"Error fetching messages by IDs { message_ids } : { e } " )
116+
117+
118+ async def hydrate_message_parts (
119+ messages : List [Message ],
120+ user_kek : bytes | None = None ,
121+ ) -> Result [List [Message ]]:
122+ """
123+ Load parts from S3 for already-fetched message rows.
124+
125+ Mutates each message in place by setting `message.parts`.
126+ """
127+ try :
128+ if not messages :
113129 return Result .resolve ([])
114130
115- # Fetch parts concurrently for all messages
116131 parts_tasks = [
117132 _fetch_message_parts (message .parts_asset_meta , user_kek = user_kek )
118- for message in ordered_messages
133+ for message in messages
119134 ]
120135 parts_results = await asyncio .gather (* parts_tasks )
121136
122- # Assign parts to messages
123- for message , parts_result in zip (ordered_messages , parts_results ):
137+ for message , parts_result in zip (messages , parts_results ):
124138 d , eil = parts_result .unpack ()
125139 if eil :
126140 message .parts = None
127141 continue
128142 message .parts = d
129143
130- return Result .resolve (ordered_messages )
131-
144+ return Result .resolve (messages )
132145 except Exception as e :
133- return Result .reject (f"Error fetching messages by IDs { message_ids } : { e } " )
146+ return Result .reject (f"Error hydrating message parts : { e } " )
134147
135148
136149async def fetch_message_branch_path_ids (
@@ -226,6 +239,69 @@ async def fetch_message_branch_path_rows(
226239 )
227240
228241
242+ async def fetch_message_branch_path_messages (
243+ db_session : AsyncSession ,
244+ message_id : asUUID ,
245+ session_id : asUUID | None = None ,
246+ ) -> Result [List [Message ]]:
247+ """
248+ Fetch one message's branch path as ordered Message rows.
249+
250+ Uses a recursive CTE to walk parent_id upward in one query.
251+ """
252+ try :
253+ query = text (
254+ """
255+ WITH RECURSIVE message_path AS (
256+ SELECT id, parent_id, session_id, 0 AS depth
257+ FROM messages
258+ WHERE id = :message_id
259+
260+ UNION ALL
261+
262+ SELECT
263+ parent.id,
264+ parent.parent_id,
265+ parent.session_id,
266+ child.depth + 1 AS depth
267+ FROM messages AS parent
268+ JOIN message_path AS child
269+ ON parent.id = child.parent_id
270+ )
271+ SELECT m.*
272+ FROM message_path AS mp
273+ JOIN messages AS m ON m.id = mp.id
274+ ORDER BY mp.depth DESC, m.id ASC
275+ """
276+ )
277+ result = await db_session .execute (
278+ select (Message ).from_statement (query ),
279+ {"message_id" : message_id },
280+ )
281+ messages = list (result .scalars ().all ())
282+
283+ if not messages :
284+ return Result .reject (f"Message { message_id } doesn't exist" )
285+
286+ path_session_ids = {message .session_id for message in messages }
287+
288+ if session_id is not None and path_session_ids != {session_id }:
289+ return Result .reject (
290+ f"Message { message_id } does not belong to session { session_id } "
291+ )
292+
293+ if len (path_session_ids ) != 1 :
294+ return Result .reject (
295+ f"Message { message_id } has an invalid cross-session parent chain"
296+ )
297+
298+ return Result .resolve (messages )
299+ except Exception as e :
300+ return Result .reject (
301+ f"Error fetching branch path messages for message { message_id } : { e } "
302+ )
303+
304+
229305async def branch_pending_message_length (
230306 db_session : AsyncSession ,
231307 message_id : asUUID ,
@@ -267,13 +343,11 @@ async def fetch_message_branch_path_data(
267343 """
268344 Fetch one message's branch path with parts loaded from S3.
269345 """
270- r = await fetch_message_branch_path_ids (db_session , message_id , session_id )
271- message_ids , eil = r .unpack ()
346+ r = await fetch_message_branch_path_messages (db_session , message_id , session_id )
347+ messages , eil = r .unpack ()
272348 if eil :
273349 return Result .reject (str (eil ))
274- return await fetch_messages_data_by_ids (
275- db_session , message_ids , user_kek = user_kek
276- )
350+ return await hydrate_message_parts (messages , user_kek = user_kek )
277351
278352
279353async def fetch_session_messages (
0 commit comments