Skip to content

fix: guard non-positive session limits and close() race in SQLite sessions#3413

Closed
rmotgi1227 wants to merge 1 commit into
openai:mainfrom
rmotgi1227:fix/async-sqlite-close-toctou
Closed

fix: guard non-positive session limits and close() race in SQLite sessions#3413
rmotgi1227 wants to merge 1 commit into
openai:mainfrom
rmotgi1227:fix/async-sqlite-close-toctou

Conversation

@rmotgi1227
Copy link
Copy Markdown

Summary

Two related correctness bugs in the SQLite-backed session implementations.

1. Non-positive session_limit parity gap (AsyncSQLiteSession + SQLAlchemySession)

MongoDBSession.get_items and RedisSession.get_items both short-circuit with return [] when the resolved limit is <= 0:

# MongoDB / Redis — already correct
if session_limit is not None and session_limit <= 0:
    return []

AsyncSQLiteSession and SQLAlchemySession pass the limit directly to SQL without this guard. The consequence differs by sign:

limit SQLite LIMIT clause Result before fix Expected
0 LIMIT 0 [] ✓ (correct by accident) []
-1 LIMIT -1 all rows ✗ []

SQLite interprets LIMIT -1 as "no limit" (SQLite docs), so get_items(limit=-1) returns the full history instead of an empty list. This is a silent behavioral difference between backends for the same SessionSettings(limit=-1).

Fix: add the same <= 0 guard before the SQL branch in both AsyncSQLiteSession.get_items and SQLAlchemySession.get_items.

2. TOCTOU race in AsyncSQLiteSession.close()

# Before
async def close(self) -> None:
    if self._connection is None:   # checked outside lock
        return
    async with self._lock:
        await self._connection.close()  # could be None if second call raced through
        self._connection = None

Two concurrent await session.close() calls can both pass the outer is None check before either acquires the lock. The second call then invokes .close() on NoneAttributeError.

Fix: add a second if self._connection is None: return inside the lock (standard double-checked pattern).

Test plan

  • Extended test_async_sqlite_session_get_items_limit with limit=-1 assertion (was already testing limit=0)
  • Extended test_get_items_with_limit in test_sqlalchemy_session.py with limit=0 and limit=-1 assertions
  • Both assertions confirm the new early-return behaviour (no DB round-trip, consistent with MongoDB/Redis)

Issue number

No tracking issue — bugs discovered during code review.

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run make lint and make format
  • I've made sure tests pass

…sions

AsyncSQLiteSession.get_items() and SQLAlchemySession.get_items() passed
the resolved session_limit directly to SQL LIMIT without checking whether
the value is non-positive.  MongoDB and Redis already guard this:

  if session_limit is not None and session_limit <= 0:
      return []

Without the guard, SQLite treats LIMIT -1 as 'no limit' and returns all
rows, while MongoDB/Redis would return [].  This inconsistency means the
same SessionSettings(limit=-1) produces different histories depending on
which backend is used.

Also fix a TOCTOU race in AsyncSQLiteSession.close(): the None check
happened outside the asyncio lock, so two concurrent close() calls could
both pass the guard and the second one would call .close() on a None
connection.  Add a second check inside the lock (standard double-checked
pattern).

Changes:
- src/agents/extensions/memory/async_sqlite_session.py: add non-positive
  limit guard in get_items(); add inside-lock None re-check in close()
- src/agents/extensions/memory/sqlalchemy_session.py: add non-positive
  limit guard in get_items()
- tests: extend existing limit tests with limit=-1 assertions for both
  AsyncSQLiteSession and SQLAlchemySession
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:sessions wontfix This will not be worked on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants