From 824c33729da36d1dbebb0920a1980ec6d86a9344 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:11:05 -0700 Subject: [PATCH] fix(session_search): coerce limit to int to prevent TypeError with non-int values (#10522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models (especially open-source like qwen3.5-plus) may send non-int values for the limit parameter — None (JSON null), string, or even a type object. This caused TypeError: '<=' not supported between instances of 'int' and 'type' when the value reached min()/comparison operations. Changes: - Add defensive int coercion at session_search() entry with fallback to 3 - Clamp limit to [1, 5] range (was only capped at 5, not floored) - Add tests for None, type object, string, negative, and zero limit values Reported by community user ludoSifu via Discord. --- tests/tools/test_session_search.py | 57 ++++++++++++++++++++++++++++++ tools/session_search_tool.py | 10 +++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index 852ac7b9e..f5d75bb91 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -290,6 +290,63 @@ class TestSessionSearch: assert result["results"] == [] assert result["sessions_searched"] == 0 + def test_limit_none_coerced_to_default(self): + """Model sends limit=null → should fall back to 3, not TypeError.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=None, + )) + assert result["success"] is True + + def test_limit_type_object_coerced_to_default(self): + """Model sends limit as a type object → should fall back to 3, not TypeError.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=int, + )) + assert result["success"] is True + + def test_limit_string_coerced(self): + """Model sends limit as string '2' → should coerce to int.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit="2", + )) + assert result["success"] is True + + def test_limit_clamped_to_range(self): + """Negative or zero limit should be clamped to 1.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=-5, + )) + assert result["success"] is True + + result = json.loads(session_search( + query="test", db=mock_db, limit=0, + )) + assert result["success"] is True + def test_current_root_session_excludes_child_lineage(self): """Delegation child hits should be excluded when they resolve to the current root session.""" from unittest.mock import MagicMock diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 9be73a04a..1398bdfff 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -310,7 +310,15 @@ def session_search( if db is None: return tool_error("Session database not available.", success=False) - limit = min(limit, 5) # Cap at 5 sessions to avoid excessive LLM calls + # Defensive: models (especially open-source) may send non-int limit values + # (None when JSON null, string "int", or even a type object). Coerce to a + # safe integer before any arithmetic/comparison to prevent TypeError. + if not isinstance(limit, int): + try: + limit = int(limit) + except (TypeError, ValueError): + limit = 3 + limit = max(1, min(limit, 5)) # Clamp to [1, 5] # Recent sessions mode: when query is empty, return metadata for recent sessions. # No LLM calls — just DB queries for titles, previews, timestamps.