@@ -133,6 +133,8 @@ class ChatFileModel(BaseModel):
133133class ChatForm (BaseModel ):
134134 chat : dict
135135 folder_id : str | None = None
136+ # Explicit deletions to prune; absent => upsert-only.
137+ deleted_message_ids : list [str ] | None = None
136138
137139
138140class ChatImportForm (ChatForm ):
@@ -496,22 +498,42 @@ async def backfill_messages_by_chat_id(self, chat_id: str, user_id: str, message
496498 log .warning ('Backfill failed for message %s in chat %s: %s' , message_id , chat_id , e )
497499
498500 async def reconcile_messages_by_chat_id (
499- self , chat_id : str , user_id : str , messages : dict [str , dict ]
501+ self ,
502+ chat_id : str ,
503+ user_id : str ,
504+ messages : dict [str , dict ],
505+ deleted_message_ids : list [str ] | None = None ,
500506 ) -> None :
501- """Sync ``chat_message`` rows with the committed JSON blob.
507+ """Upsert blob messages, then prune only the IDs the client explicitly
508+ deleted (plus their now-orphaned descendants).
502509
503- Upserts current messages via ``backfill_messages_by_chat_id``
504- and deletes orphaned rows whose message_id no longer appears
505- in the blob. Best-effort: errors are logged but never raised.
510+ Rows merely missing from the blob are never inferred as deletions — a
511+ lost write race would otherwise drop a still-valid message permanently.
512+ Best-effort: errors are logged, not raised.
506513 """
507514 try :
508515 await self .backfill_messages_by_chat_id (chat_id , user_id , messages )
509516
510- existing_map = await ChatMessages .get_messages_map_by_chat_id (chat_id )
511- if existing_map is not None :
512- orphaned_ids = set (existing_map .keys ()) - set (messages .keys ())
513- if orphaned_ids :
514- await ChatMessages .delete_message_ids_by_chat_id (chat_id , orphaned_ids )
517+ dead_ids = set (deleted_message_ids or [])
518+ if not dead_ids :
519+ return
520+
521+ existing_map = await ChatMessages .get_messages_map_by_chat_id (chat_id ) or {}
522+
523+ # Reparented children reappear in the blob and survive; truly orphaned ones go with the parent.
524+ changed = True
525+ while changed :
526+ changed = False
527+ for message_id , message in existing_map .items ():
528+ if message_id in dead_ids or message_id in messages :
529+ continue
530+ if message .get ('parentId' ) in dead_ids :
531+ dead_ids .add (message_id )
532+ changed = True
533+
534+ dead_ids &= set (existing_map .keys ())
535+ if dead_ids :
536+ await ChatMessages .delete_message_ids_by_chat_id (chat_id , dead_ids )
515537 except Exception as e :
516538 log .warning ('Failed to reconcile chat_message rows for chat %s: %s' , chat_id , e )
517539
0 commit comments