diff --git a/src/task_scheduler.py b/src/task_scheduler.py index f2f59d65e..89c9217fd 100644 --- a/src/task_scheduler.py +++ b/src/task_scheduler.py @@ -342,6 +342,36 @@ def _normalize_chat_endpoint(url: str) -> str: return url +def _resolve_research_settings(get_setting_fn): + """Resolve the scheduled-research settings with crash-proof fallbacks. + + A hand-edited or agent-written data/settings.json can hold non-numeric + values for the research_* keys; the bare int() reads previously raised + inside TaskScheduler._execute_research_task and killed every scheduled + research run until settings.json was fixed by hand (#5049). + src/research_handler.py already guards the same reads defensively — + this mirrors that for the scheduler. Valid values pass through + unchanged (numeric strings like "4096" still parse). + """ + try: + max_tokens = int(get_setting_fn("research_max_tokens", 8192)) + except (TypeError, ValueError): + max_tokens = 8192 + try: + extraction_timeout = int( + get_setting_fn("research_extraction_timeout_seconds", 90) or 90 + ) + except (TypeError, ValueError): + extraction_timeout = 90 + try: + extraction_concurrency = int( + get_setting_fn("research_extraction_concurrency", 3) or 3 + ) + except (TypeError, ValueError): + extraction_concurrency = 3 + return max_tokens, extraction_timeout, extraction_concurrency + + class TaskScheduler: def __init__(self, session_manager): self._session_manager = session_manager @@ -2068,9 +2098,9 @@ class TaskScheduler: except Exception: pass - max_tokens = int(get_setting("research_max_tokens", 8192)) - extraction_timeout = int(get_setting("research_extraction_timeout_seconds", 90) or 90) - extraction_concurrency = int(get_setting("research_extraction_concurrency", 3) or 3) + max_tokens, extraction_timeout, extraction_concurrency = ( + _resolve_research_settings(get_setting) + ) researcher = DeepResearcher( llm_endpoint=endpoint_url, diff --git a/tests/test_scheduler_research_settings_guard.py b/tests/test_scheduler_research_settings_guard.py new file mode 100644 index 000000000..0cdfa2b87 --- /dev/null +++ b/tests/test_scheduler_research_settings_guard.py @@ -0,0 +1,79 @@ +"""Guarded resolution of scheduled-research settings (#5049). + +The scheduler reads ``research_max_tokens`` / +``research_extraction_timeout_seconds`` / ``research_extraction_concurrency`` +from settings.json with bare ``int()`` casts. A hand-edited value like +``{"research_max_tokens": "invalid"}`` used to raise inside +``TaskScheduler._execute_research_task`` and kill every scheduled research +run until settings.json was fixed by hand. + +``_resolve_research_settings`` must fall back to the documented defaults +(8192 / 90 / 3) for non-numeric input while preserving valid-value behavior +(numeric strings still parse). ``src/research_handler.py`` already guards the +same reads defensively; this is the scheduler's equivalent. +""" + +from __future__ import annotations + +from src.task_scheduler import _resolve_research_settings + + +def _fake_get_setting(values): + """A get_setting stand-in backed by a plain dict.""" + + def get(key, default=None): + return values.get(key, default) + + return get + + +def test_non_numeric_values_fall_back_to_documented_defaults(): + get = _fake_get_setting( + { + "research_max_tokens": "invalid", + "research_extraction_timeout_seconds": "bogus", + "research_extraction_concurrency": "not-a-number", + } + ) + + assert _resolve_research_settings(get) == (8192, 90, 3) + + +def test_none_values_fall_back_to_documented_defaults(): + get = _fake_get_setting( + { + "research_max_tokens": None, + "research_extraction_timeout_seconds": None, + "research_extraction_concurrency": None, + } + ) + + assert _resolve_research_settings(get) == (8192, 90, 3) + + +def test_numeric_strings_still_parse(): + get = _fake_get_setting( + { + "research_max_tokens": "4096", + "research_extraction_timeout_seconds": "45", + "research_extraction_concurrency": "2", + } + ) + + assert _resolve_research_settings(get) == (4096, 45, 2) + + +def test_missing_keys_keep_documented_defaults(): + assert _resolve_research_settings(_fake_get_setting({})) == (8192, 90, 3) + + +def test_valid_ints_pass_through(): + get = _fake_get_setting( + { + "research_max_tokens": 2048, + "research_extraction_timeout_seconds": 120, + "research_extraction_concurrency": 6, + } + ) + + assert _resolve_research_settings(get) == (2048, 120, 6)