1616 coerce_session_settings ,
1717 resolve_session_limit ,
1818)
19+ from ...memory .sqlite_session import _await_mutation
1920
2021
2122class AsyncSQLiteSession (SessionABC ):
@@ -57,6 +58,7 @@ def __init__(
5758 self .sessions_table = sessions_table
5859 self .messages_table = messages_table
5960 self ._connection : aiosqlite .Connection | None = None
61+ self ._quarantined_connections : set [aiosqlite .Connection ] = set ()
6062 self ._lock = asyncio .Lock ()
6163 self ._init_lock = asyncio .Lock ()
6264 self ._closed = False
@@ -102,9 +104,46 @@ async def _get_connection(self) -> aiosqlite.Connection:
102104
103105 async with self ._init_lock :
104106 if self ._connection is None :
105- self ._connection = await aiosqlite .connect (str (self .db_path ))
106- await self ._connection .execute ("PRAGMA journal_mode=WAL" )
107- await self ._init_db_for_connection (self ._connection )
107+ connect_task = asyncio .ensure_future (aiosqlite .connect (str (self .db_path )))
108+ try :
109+ connection = await asyncio .shield (connect_task )
110+ except BaseException as acquisition_error :
111+ connection = None
112+ cleanup_cancellation : asyncio .CancelledError | None = None
113+ try :
114+ connection = await _await_mutation (connect_task )
115+ except asyncio .CancelledError as exc :
116+ cleanup_cancellation = exc
117+ try :
118+ connection = connect_task .result ()
119+ except BaseException :
120+ pass
121+ except BaseException :
122+ pass
123+ close_error = (
124+ await self ._close_owned_connection (connection )
125+ if connection is not None
126+ else None
127+ )
128+ if isinstance (acquisition_error , asyncio .CancelledError ):
129+ raise
130+ if cleanup_cancellation is not None :
131+ raise cleanup_cancellation from None
132+ if isinstance (close_error , asyncio .CancelledError ):
133+ raise close_error from None
134+ raise
135+ assert connection is not None
136+ try :
137+ await connection .execute ("PRAGMA journal_mode=WAL" )
138+ await self ._init_db_for_connection (connection )
139+ except BaseException as initialization_error :
140+ close_error = await self ._close_owned_connection (connection )
141+ if isinstance (initialization_error , asyncio .CancelledError ):
142+ raise
143+ if isinstance (close_error , asyncio .CancelledError ):
144+ raise close_error from None
145+ raise
146+ self ._connection = connection
108147
109148 return self ._connection
110149
@@ -121,6 +160,71 @@ async def _locked_connection(self) -> AsyncIterator[aiosqlite.Connection]:
121160 conn = await self ._get_connection ()
122161 yield conn
123162
163+ @asynccontextmanager
164+ async def _write_connection (self ) -> AsyncIterator [aiosqlite .Connection ]:
165+ """Provide a connection that cannot retain a failed write transaction."""
166+ async with self ._locked_connection () as conn :
167+ try :
168+ yield conn
169+ except BaseException as operation_error :
170+ rollback_task = asyncio .create_task (conn .rollback ())
171+ rollback_error : BaseException | None = None
172+ rollback_cancellation : asyncio .CancelledError | None = None
173+ try :
174+ await _await_mutation (rollback_task )
175+ except asyncio .CancelledError as exc :
176+ rollback_cancellation = exc
177+ try :
178+ rollback_task .result ()
179+ except BaseException as outcome_error :
180+ rollback_error = outcome_error
181+ except BaseException as exc :
182+ rollback_error = exc
183+
184+ invalidation_error = None
185+ if rollback_error is not None :
186+ invalidation_error = await self ._invalidate_connection (conn )
187+
188+ if isinstance (operation_error , asyncio .CancelledError ):
189+ raise
190+ if rollback_cancellation is not None :
191+ raise rollback_cancellation from None
192+ if isinstance (invalidation_error , asyncio .CancelledError ):
193+ raise invalidation_error from None
194+ raise
195+
196+ async def _invalidate_connection (self , conn : aiosqlite .Connection ) -> BaseException | None :
197+ """Close and evict a connection that could not roll back safely."""
198+ close_error = await self ._close_owned_connection (conn )
199+ if self ._connection is conn :
200+ self ._connection = None
201+ if str (self .db_path ) == ":memory:" or close_error is not None :
202+ self ._closed = True
203+ return close_error
204+
205+ async def _close_owned_connection (self , conn : aiosqlite .Connection ) -> BaseException | None :
206+ """Close an owned connection or retain it for a later cleanup retry."""
207+ close_task = asyncio .create_task (conn .close ())
208+ cancellation : asyncio .CancelledError | None = None
209+ close_error : BaseException | None = None
210+ try :
211+ await _await_mutation (close_task )
212+ except asyncio .CancelledError as exc :
213+ cancellation = exc
214+ try :
215+ close_task .result ()
216+ except BaseException as outcome_error :
217+ close_error = outcome_error
218+ except BaseException as exc :
219+ close_error = exc
220+
221+ if close_error is not None :
222+ self ._quarantined_connections .add (conn )
223+ self ._closed = True
224+ else :
225+ self ._quarantined_connections .discard (conn )
226+ return cancellation or close_error
227+
124228 async def get_items (self , limit : int | None = None ) -> list [TResponseInputItem ]:
125229 """Retrieve the conversation history for this session.
126230
@@ -206,7 +310,7 @@ async def add_items(self, items: list[TResponseInputItem]) -> None:
206310 if not items :
207311 return
208312
209- async with self ._locked_connection () as conn :
313+ async with self ._write_connection () as conn :
210314 await conn .execute (
211315 f"""
212316 INSERT OR IGNORE INTO { self .sessions_table } (session_id) VALUES (?)
@@ -231,15 +335,16 @@ async def add_items(self, items: list[TResponseInputItem]) -> None:
231335 (self .session_id ,),
232336 )
233337
234- await conn .commit ()
338+ await _await_mutation ( conn .commit () )
235339
236340 async def pop_item (self ) -> TResponseInputItem | None :
237341 """Remove and return the most recent item from the session.
238342
239343 Returns:
240344 The most recent item if it exists, None if the session is empty
241345 """
242- async with self ._locked_connection () as conn :
346+
347+ async with self ._write_connection () as conn :
243348 cursor = await conn .execute (
244349 f"""
245350 DELETE FROM { self .messages_table }
@@ -256,7 +361,7 @@ async def pop_item(self) -> TResponseInputItem | None:
256361
257362 result = await cursor .fetchone ()
258363 await cursor .close ()
259- await conn .commit ()
364+ await _await_mutation ( conn .commit () )
260365
261366 while result :
262367 message_data = result [0 ]
@@ -278,13 +383,14 @@ async def pop_item(self) -> TResponseInputItem | None:
278383 )
279384 result = await cursor .fetchone ()
280385 await cursor .close ()
281- await conn .commit ()
386+ await _await_mutation ( conn .commit () )
282387
283388 return None
284389
285390 async def clear_session (self ) -> None :
286391 """Clear all items for this session."""
287- async with self ._locked_connection () as conn :
392+
393+ async with self ._write_connection () as conn :
288394 await conn .execute (
289395 f"DELETE FROM { self .messages_table } WHERE session_id = ?" ,
290396 (self .session_id ,),
@@ -293,18 +399,42 @@ async def clear_session(self) -> None:
293399 f"DELETE FROM { self .sessions_table } WHERE session_id = ?" ,
294400 (self .session_id ,),
295401 )
296- await conn .commit ()
402+ await _await_mutation ( conn .commit () )
297403
298404 async def close (self ) -> None :
299405 """Close the database connection.
300406
301407 The session becomes terminal from the first close attempt: subsequent
302408 operations raise RuntimeError rather than reopening the database. Repeated
303- and concurrent calls are safe no-ops.
409+ and concurrent calls are safe. A repeated call retries any owned
410+ connection whose previous close did not complete.
304411 """
305412 async with self ._lock :
306413 self ._closed = True
307- if self ._connection is None :
308- return
309- await self ._connection .close ()
310- self ._connection = None
414+ connections = set (self ._quarantined_connections )
415+ if self ._connection is not None :
416+ connections .add (self ._connection )
417+
418+ first_error : BaseException | None = None
419+ cancellation : asyncio .CancelledError | None = None
420+ for connection in connections :
421+ close_task = asyncio .create_task (self ._close_owned_connection (connection ))
422+ try :
423+ close_error = await asyncio .shield (close_task )
424+ except asyncio .CancelledError as exc :
425+ if cancellation is None :
426+ cancellation = exc
427+ try :
428+ close_error = await _await_mutation (close_task )
429+ except asyncio .CancelledError :
430+ close_error = close_task .result ()
431+ if close_error is None :
432+ if self ._connection is connection :
433+ self ._connection = None
434+ elif first_error is None :
435+ first_error = close_error
436+
437+ if cancellation is not None :
438+ raise cancellation
439+ if first_error is not None :
440+ raise first_error
0 commit comments