@@ -113,7 +113,7 @@ def __init__(
113113 ) -> None :
114114 # Local store
115115 self .local_memory : list [dict [str , Any ]] = []
116- self ._unflushed_turn_counts : dict [tuple [str , Optional [ str ] ], int ] = {}
116+ self ._unflushed_turn_counts : dict [tuple [str , str ], int ] = {}
117117
118118 self ._background_tasks : set [asyncio .Task [Any ]] = set ()
119119 try :
@@ -361,6 +361,11 @@ def add_local(
361361 ttl = ttl ,
362362 salience = salience ,
363363 )
364+ if memory_type == "turn" and not thread_id :
365+ raise ValidationError (
366+ "thread_id is required for memory_type='turn' so the auto-trigger "
367+ "counter can group turns per conversation. Set thread_id explicitly."
368+ )
364369 self .local_memory .append (memory )
365370 if memory_type == "turn" :
366371 key = (user_id , thread_id )
@@ -491,6 +496,8 @@ async def connect_cosmos(
491496 try :
492497 from azure .cosmos .aio import CosmosClient
493498
499+ await self ._drain_cosmos_client ()
500+
494501 client = CosmosClient (self ._cosmos_endpoint , credential = self ._cosmos_credential )
495502 db = client .get_database_client (self ._cosmos_database )
496503 container_handle = db .get_container_client (self ._cosmos_container )
@@ -573,6 +580,8 @@ async def create_memory_store(
573580 from azure .cosmos import PartitionKey , ThroughputProperties
574581 from azure .cosmos .aio import CosmosClient
575582
583+ await self ._drain_cosmos_client ()
584+
576585 client = CosmosClient (self ._cosmos_endpoint , credential = self ._cosmos_credential )
577586
578587 db = await client .create_database_if_not_exists (id = self ._cosmos_database )
@@ -733,7 +742,34 @@ async def push_to_cosmos(self, batch_size: int = 25) -> None:
733742
734743 for start in range (0 , len (records ), batch_size ):
735744 batch = records [start : start + batch_size ]
736- tasks = [self ._container_client .upsert_item (body = r .to_cosmos_dict ()) for r in batch ]
745+ bodies = [r .to_cosmos_dict () for r in batch ]
746+
747+ # Batch-embed non-turn memories that don't already carry a
748+ # vector — one /embeddings POST per Cosmos batch instead of
749+ # N concurrent ones.
750+ to_embed_idx : list [int ] = []
751+ to_embed_text : list [str ] = []
752+ for i , body in enumerate (bodies ):
753+ if body .get ("type" ) != "turn" and body .get ("content" ) and not body .get ("embedding" ):
754+ to_embed_idx .append (i )
755+ to_embed_text .append (body ["content" ])
756+ if to_embed_text :
757+ try :
758+ vectors = await self ._embeddings_client .generate_batch (to_embed_text )
759+ for i , vec in zip (to_embed_idx , vectors ):
760+ bodies [i ]["embedding" ] = vec
761+ # Persist the embedding back to local_memory so a
762+ # repeat push_to_cosmos() doesn't re-embed.
763+ self .local_memory [start + i ]["embedding" ] = vec
764+ except Exception as exc : # noqa: BLE001
765+ logger .warning (
766+ "push_to_cosmos: batch embedding generation failed (%s); "
767+ "proceeding without embeddings for %d records" ,
768+ exc ,
769+ len (to_embed_text ),
770+ )
771+
772+ tasks = [self ._container_client .upsert_item (body = b ) for b in bodies ]
737773 try :
738774 await asyncio .gather (* tasks )
739775 except Exception as exc :
@@ -1247,7 +1283,7 @@ def _drain_pipeline_resources(self) -> None:
12471283 try :
12481284 close ()
12491285 except Exception :
1250- pass
1286+ logger . warning ( "Failed to close prior sync Cosmos client" , exc_info = True )
12511287 self ._sync_cosmos_client = None
12521288
12531289 prior_sync_embeddings = getattr (self , "_sync_embeddings_client" , None )
@@ -1257,9 +1293,34 @@ def _drain_pipeline_resources(self) -> None:
12571293 try :
12581294 close ()
12591295 except Exception :
1260- pass
1296+ logger . warning ( "Failed to close prior sync EmbeddingsClient" , exc_info = True )
12611297 self ._sync_embeddings_client = None
12621298
1299+ async def _drain_cosmos_client (self ) -> None :
1300+ """Close any prior async Cosmos client before reassigning the field.
1301+
1302+ Also nulls the cached pipeline, counter container handle, and any
1303+ lazily-created processor — otherwise they retain references to
1304+ the drained sync container client and the next op fails with an
1305+ opaque "Object disposed" error. A user-supplied processor is left
1306+ intact since the SDK does not own its lifecycle.
1307+ """
1308+ prior = self ._cosmos_client
1309+ if prior is None :
1310+ return
1311+ close = getattr (prior , "close" , None )
1312+ if callable (close ):
1313+ try :
1314+ await close ()
1315+ except Exception :
1316+ logger .warning ("Failed to close prior async Cosmos client during reconnect" , exc_info = True )
1317+ self ._cosmos_client = None
1318+ self ._container_client = None
1319+ self ._counter_container_client = None
1320+ self ._pipeline = None
1321+ if not self ._processor_explicit :
1322+ self ._processor = None
1323+
12631324 def _init_pipeline (self ) -> None :
12641325 """Initialize the ProcessingPipeline with a sync container client.
12651326
0 commit comments