Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/agents/extensions/memory/advanced_sqlite_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ def _allow_all_sqlite_actions(
return sqlite3.SQLITE_OK


def _stored_message_data_needle(search_term: str) -> str:
"""Return ``search_term`` in the encoding used by the stored ``message_data`` column.

Session rows are written as ``json.dumps(item)``, which escapes every non-ASCII character
as ``\\uXXXX`` and escapes ``"``, ``\\``, and control characters. Matching a raw search term
against that column therefore never finds non-ASCII content. Re-encoding the term the same
way keeps ASCII searches byte-identical while making non-ASCII searches match.
"""
return json.dumps(search_term)[1:-1]


def _content_preview(content: Any, max_length: int | None = None) -> str:
"""Return a string preview of a stored user-message ``content``.

Expand Down Expand Up @@ -1598,7 +1609,11 @@ def _search_sync():
AND am.message_data LIKE ?
ORDER BY ms.branch_turn_number
""",
(self.session_id, resolved_branch_id, f"%{search_term}%"),
(
self.session_id,
resolved_branch_id,
f"%{_stored_message_data_needle(search_term)}%",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape LIKE metacharacters in the encoded needle

When a non-ASCII search term also contains % or _, those characters remain SQL wildcards after _stored_message_data_needle() and can return turns that do not contain the requested text. For example, searching for 100% 東京 matches content such as 100 dollars 東京; create_branch_from_content() can consequently branch from the wrong turn. Escape LIKE metacharacters and specify an ESCAPE character when constructing the pattern.

AGENTS.md reference: AGENTS.md:L163-L164

Useful? React with 👍 / 👎.

),
)

matches = []
Expand Down
35 changes: 35 additions & 0 deletions tests/extensions/memory/test_advanced_sqlite_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,41 @@ async def test_get_conversation_turns_with_list_content():
session.close()


async def test_find_turns_by_content_matches_non_ascii_content():
"""Non-ASCII and JSON-escaped user content must be searchable."""
session_id = "find_turns_non_ascii_test"
session = AdvancedSQLiteSession(session_id=session_id, create_tables=True)

await session.add_items(
[
{"role": "user", "content": "東京の天気はどうですか"},
{"role": "assistant", "content": "晴れです"},
]
)
await session.add_items(
[
{"role": "user", "content": "What is the weather in Tokyo?"},
{"role": "assistant", "content": "Sunny."},
]
)
await session.add_items(
[
{"role": "user", "content": 'Le café est "très" bon'},
{"role": "assistant", "content": "Oui."},
]
)

assert [turn["turn"] for turn in await session.find_turns_by_content("東京")] == [1]
assert [turn["turn"] for turn in await session.find_turns_by_content("weather")] == [2]
assert [turn["turn"] for turn in await session.find_turns_by_content('café est "très"')] == [3]
assert await session.find_turns_by_content("大阪") == []

branch_id = await session.create_branch_from_content("東京", "tokyo_branch")
assert branch_id == "tokyo_branch"

session.close()


async def test_find_turns_by_content_with_list_content():
"""find_turns_by_content returns a string preview for list (multimodal) content."""
session_id = "find_turns_list_content_test"
Expand Down
Loading