SWE-Race › Tasks › learningcircuit-local-deep-research-2517-2502 ← prevnext →

learningcircuit-local-deep-research-2517-2502

LearningCircuit/local-deep-researchsplitcompositemerged 2026-03-06MITfix: 20 files, +147 −4310 fail-to-pass · 141 pass-to-pass
Results
Modelsolved / attemptsmedian stepsmedian costattempts
GPT-5.6 Luna6/622$0.0211✓ 2✓ 3✓ 4✓ 5✓ 6✓
DeepSeek V4 Flash1/288$0.0841✗ 2✓
GLM-5.3 Flash2/264$0.0301✓ 2✓
The prompt the agent sees

Two independent defects in the research pipeline.

The settings snapshot never reaches citation handling. When a strategy is built with a `settings_snapshot`, custom citation settings such as `general.output_instructions` are ignored, so a snapshot asking for a different output language has no effect. Every strategy that owns a citation handler must pass its snapshot down: for each of the strategy names `source-based`, `standard`, `parallel`, `rapid`, `iterdrag` and `focused-iteration`, building it through `local_deep_research.search_system_factory.create_strategy(strategy_name=..., model=..., search=..., settings_snapshot=snapshot)` must leave `strategy.citation_handler` wired to that snapshot, so that `strategy.citation_handler._handler._get_output_instruction_prefix()` contains the value of `general.output_instructions` from the snapshot, and is the empty string when the snapshot has no such key. Concretely, the snapshot has to be accepted as a `settings_snapshot` keyword by those strategies' `__init__` (including the direct-search strategy, which did not take one), forwarded to the base-strategy constructor, and passed to the `CitationHandler` the strategy constructs, alongside whatever other arguments it already passes such as `handler_type`. When no snapshot is given the handler is still constructed explicitly with `settings_snapshot=None`, so a test patching the citation-handler class can assert `assert_called_once_with(model, settings_snapshot=None)`.

Worker threads leak thread-local resources. `thread_cleanup` in `local_deep_research.database.thread_local_session` currently only works as a plain decorator and swallows cleanup errors silently. It must support all four call shapes and keep working as before otherwise: - as a bare decorator, `@thread_cleanup` over a function returning 42 gives 42 and runs the cleanup exactly once; - as a decorator factory, `@thread_cleanup()` over a function returning 99 gives 99 and runs it exactly once; - as a context manager, `with thread_cleanup():` runs it exactly once on exit; - as an inline wrapper, `thread_cleanup(worker)(5)` returns `worker(5)` and runs it exactly once. "Runs the cleanup" means calling the module's `cleanup_current_thread`, which the tests patch by that name in `local_deep_research.database.thread_local_session`, and also clearing the thread settings context. Decorated functions keep their identity, so `functools.wraps` metadata survives: a decorated `my_worker` still has `__name__ == "my_worker"` and its original docstring. When cleanup itself raises, the failure is reported through `logger.debug(...)` on that module's `logger` and is not re-raised: a worker returning `"ok"` still returns `"ok"` while `cleanup_current_thread` raises `RuntimeError`, and a worker raising `ValueError("original error")` still propagates that same `ValueError` rather than the cleanup error. The parallel search paths wrap the work they submit to their executors in `thread_cleanup` so the cleanup actually runs per worker.

Hidden tests · 10 fail-to-pass, 141 pass-to-passrun after the agent submits, in a clean verifier
test_init_creates_componentstest_cleanup_exception_logged_not_raisedtest_context_manager_runs_cleanuptest_factory_decorator_runs_cleanuptest_output_instructions_reach_citation_handler[focused-itertest_output_instructions_reach_citation_handler[iterdrag]test_output_instructions_reach_citation_handler[parallel]test_output_instructions_reach_citation_handler[rapid]+2 more
Test patch · 258 lines
diff --git a/tests/advanced_search_system/strategies/test_iterdrag_strategy.py b/tests/advanced_search_system/strategies/test_iterdrag_strategy.py
index 47ce3292..ec886379 100644
--- a/tests/advanced_search_system/strategies/test_iterdrag_strategy.py
+++ b/tests/advanced_search_system/strategies/test_iterdrag_strategy.py
@@ -106,7 +106,9 @@ class TestIterDRAGStrategyInit:
 
         IterDRAGStrategy(search=mock_search, model=mock_model)
 
-        mock_citation.assert_called_once_with(mock_model)
+        mock_citation.assert_called_once_with(
+            mock_model, settings_snapshot=None
+        )
         mock_question.assert_called_once_with(mock_model)
         mock_knowledge.assert_called_once_with(mock_model)
         mock_findings.assert_called_once_with(mock_model)
diff --git a/tests/database/test_thread_local_session.py b/tests/database/test_thread_local_session.py
index 65b850f1..d6a18e4d 100644
--- a/tests/database/test_thread_local_session.py
+++ b/tests/database/test_thread_local_session.py
@@ -404,3 +404,164 @@ class TestGlobalInstance:
         )
 
         assert isinstance(thread_session_manager, ThreadLocalSessionManager)
+
+
+class TestThreadCleanup:
+    """Tests for thread_cleanup decorator / context manager."""
+
+    def _patch_all_cleanup(self):
+        """Return a stack of patches for the three cleanup functions."""
+        return (
+            patch(
+                "local_deep_research.database.thread_local_session.cleanup_current_thread"
+            ),
+            patch(
+                "local_deep_research.database.thread_local_session._ThreadCleanup.__exit__",
+                wraps=None,
+            ),
+        )
+
+    def test_bare_decorator_runs_cleanup(self):
+        """@thread_cleanup runs cleanup on exit and returns result."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        mock_cleanup = Mock()
+
+        with patch(
+            "local_deep_research.database.thread_local_session.cleanup_current_thread",
+            mock_cleanup,
+        ):
+
+            @thread_cleanup
+            def worker():
+                return 42
+
+            result = worker()
+
+        assert result == 42
+        mock_cleanup.assert_called_once()
+
+    def test_factory_decorator_runs_cleanup(self):
+        """@thread_cleanup() (with parens) runs cleanup on exit and returns result."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        mock_cleanup = Mock()
+
+        with patch(
+            "local_deep_research.database.thread_local_session.cleanup_current_thread",
+            mock_cleanup,
+        ):
+
+            @thread_cleanup()
+            def worker():
+                return 99
+
+            result = worker()
+
+        assert result == 99
+        mock_cleanup.assert_called_once()
+
+    def test_context_manager_runs_cleanup(self):
+        """with thread_cleanup(): runs cleanup on exit."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        mock_cleanup = Mock()
+
+        with patch(
+            "local_deep_research.database.thread_local_session.cleanup_current_thread",
+            mock_cleanup,
+        ):
+            with thread_cleanup():
+                pass
+
+        mock_cleanup.assert_called_once()
+
+    def test_inline_wrapper_runs_cleanup(self):
+        """thread_cleanup(func) as inline wrapper runs cleanup on exit."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        mock_cleanup = Mock()
+
+        def worker(x):
+            return x * 2
+
+        with patch(
+            "local_deep_research.database.thread_local_session.cleanup_current_thread",
+            mock_cleanup,
+        ):
+            wrapped = thread_cleanup(worker)
+            result = wrapped(5)
+
+        assert result == 10
+        mock_cleanup.assert_called_once()
+
+    def test_cleanup_exception_logged_not_raised(self):
+        """Cleanup exceptions are logged at debug level, not raised."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        with (
+            patch(
+                "local_deep_research.database.thread_local_session.cleanup_current_thread",
+                side_effect=RuntimeError("cleanup boom"),
+            ),
+            patch(
+                "local_deep_research.database.thread_local_session.logger"
+            ) as mock_logger,
+        ):
+
+            @thread_cleanup
+            def worker():
+                return "ok"
+
+            result = worker()
+
+        assert result == "ok"
+        mock_logger.debug.assert_called()
+
+    def test_original_exception_propagates_when_cleanup_fails(self):
+        """Original exceptions propagate even when cleanup fails."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        with (
+            patch(
+                "local_deep_research.database.thread_local_session.cleanup_current_thread",
+                side_effect=RuntimeError("cleanup boom"),
+            ),
+            patch("local_deep_research.database.thread_local_session.logger"),
+        ):
+
+            @thread_cleanup
+            def worker():
+                raise ValueError("original error")
+
+            try:
+                worker()
+                assert False, "Should have raised ValueError"
+            except ValueError as e:
+                assert str(e) == "original error"
+
+    def test_functools_wraps_metadata_preserved(self):
+        """functools.wraps metadata preserved on decorated functions."""
+        from local_deep_research.database.thread_local_session import (
+            thread_cleanup,
+        )
+
+        @thread_cleanup
+        def my_worker():
+            """My docstring."""
+            pass
+
+        assert my_worker.__name__ == "my_worker"
+        assert my_worker.__doc__ == "My docstring."
diff --git a/tests/strategies/test_strategy_instantiation.py b/tests/strategies/test_strategy_instantiation.py
index b8ccad76..9a545e39 100644
--- a/tests/strategies/test_strategy_instantiation.py
+++ b/tests/strategies/test_strategy_instantiation.py
@@ -228,6 +228,69 @@ class TestStrategyDefaultValues:
         assert isinstance(strategy.questions_by_iteration, dict)
 
 
+class TestOutputInstructionsPropagation:
+    """Regression test: settings_snapshot with output_instructions reaches citation handler."""
+
+    STRATEGIES_WITH_CITATION_HANDLER = [
+        "source-based",
+        "standard",
+        "parallel",
+        "rapid",
+        "iterdrag",
+        "focused-iteration",
+    ]
+
+    @pytest.mark.parametrize("strategy_name", STRATEGIES_WITH_CITATION_HANDLER)
+    def test_output_instructions_reach_citation_handler(
+        self,
+        strategy_name: str,
+        strategy_mock_llm,
+        strategy_mock_search,
+        strategy_settings_snapshot,
+    ):
+        """Test that general.output_instructions in the snapshot is visible to the citation handler."""
+        from local_deep_research.search_system_factory import create_strategy
+
+        snapshot = {
+            **strategy_settings_snapshot,
+            "general.output_instructions": "Respond in Spanish",
+        }
+
+        strategy = create_strategy(
+            strategy_name=strategy_name,
+            model=strategy_mock_llm,
+            search=strategy_mock_search,
+            settings_snapshot=snapshot,
+        )
+
+        handler = strategy.citation_handler
+        # The internal handler should produce the output instruction prefix
+        prefix = handler._handler._get_output_instruction_prefix()
+        assert "Respond in Spanish" in prefix
+
+    @pytest.mark.parametrize("strategy_name", STRATEGIES_W
… [882 more characters]
Reference fix · 20 files, +147 −43the upstream merge, used only for grading calibration

The agent could not see this: the repository holds one commit and the sandbox has no network. Leak audit.

src/local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py, src/local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py, src/local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py, src/local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py, src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py, src/local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py, src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py, src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py

diff --git a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
index 71da2c971b1..e793413311c 100644
--- a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
@@ -41,9 +41,13 @@ def __init__(
         filter_reindex: bool = True,
         cross_engine_max_results: int = None,
         all_links_of_system=None,
+        settings_snapshot=None,
     ):
         """Initialize with minimal components for efficiency."""
-        super().__init__(all_links_of_system=all_links_of_system)
+        super().__init__(
+            all_links_of_system=all_links_of_system,
+            settings_snapshot=settings_snapshot,
+        )
         self.search = search
         self.model = model
         self.progress_callback = None
@@ -62,7 +66,9 @@ def __init__(
         )
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
         self.findings_repository = FindingsRepository(self.model)
 
     def analyze_topic(self, query: str) -> Dict:
diff --git a/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py b/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
index 2b92ae56d37..3c9393bda45 100644
--- a/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
@@ -121,7 +121,9 @@ def __init__(
             "forced_answer" if use_browsecomp_optimization else "standard"
         )
         self.citation_handler = citation_handler or CitationHandler(
-            self.model, handler_type=handler_type
+            self.model,
+            handler_type=handler_type,
+            settings_snapshot=settings_snapshot,
         )
         self.findings_repository = FindingsRepository(self.model)
 
diff --git a/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py b/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
index 539d6db5e63..3c1961b823c 100644
--- a/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
@@ -54,8 +54,10 @@ def __init__(
         self.progress_callback = None
         # Note: questions_by_iteration is already initialized by parent class
 
-        # Use provided citation_handler or create one
-        self.citation_handler = CitationHandler(self.model)
+        # Create citation handler with settings
+        self.citation_handler = CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize components
         self.question_generator = DecompositionQuestionGenerator(self.model)
diff --git a/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
index 180860f6603..2065ccae461 100644
--- a/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
@@ -77,7 +77,9 @@ def __init__(
             self.search.include_full_content = include_text_content
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize components
         self.question_generator = StandardQuestionGenerator(self.model)
diff --git a/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
index ee55d8d26db..4a9810b25c5 100644
--- a/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
@@ -41,7 +41,9 @@ def __init__(
         # Note: questions_by_iteration is already initialized by parent class
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize components
         self.question_generator = StandardQuestionGenerator(self.model)
diff --git a/src/local_deep_research/advanced_search_system/strategies/source_based_strategy.py b/src/local_deep_research/advanced_search_system/strategies/source_based_strategy.py
index 8ea18d53993..8b95d0c0d1c 100644
--- a/src/local_deep_research/advanced_search_system/strategies/source_based_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/source_based_strategy.py
@@ -127,7 +127,9 @@ def __init__(
             self.search.include_full_content = include_text_content
 
         # Use provided citation_handler or create default
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize question generator (atomic facts variant is experimental)
         if use_atomic_facts:
diff --git a/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py b/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py
index 456e365ec7a..f5958112e6f 100644
--- a/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py
@@ -56,7 +56,9 @@ def __init__(
         # Note: questions_by_iteration is already initialized by parent class
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize specialized components
         self.question_generator = StandardQuestionGenerator(self.model)
diff --git a/src/local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py b/src/local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py
index c58f9ad0656..af6e235f0ba 100644
--- a/src/local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py
+++ b/src/local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py
@@ -11,6 +11,7 @@
 
 from loguru import logger
 
+from ...database.thread_local_session import thread_cleanup
 from ..candidates.base_candidate import Candidate
 from ..constraints.base_constraint import Constraint
 from .base_explorer import (
@@ -84,7 +85,9 @@ def explore(
 
                 # Submit all queries for parallel execution
                 future_to_query = {
-                    executor.submit(self._execute_search, query): query
+                    executor.submit(
+                        thread_cleanup(self._execute_search), query
+                    ): query
                     for query in current_queries
                 }
 
diff --git a/src/local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py b/src/local_deep_research/advanced_sea
… [18548 more characters]

What each model did

Submitted diff first, then every command. Outputs show 12 lines; expand for the rest.
GLM-5.3 Flash · attempt 2 · solved · 54 steps · $0.026
Submitted diff · 277 lines
diff --git a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
index 71da2c9..e793413 100644
--- a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
@@ -41,9 +41,13 @@ class DirectSearchStrategy(BaseSearchStrategy):
         filter_reindex: bool = True,
         cross_engine_max_results: int = None,
         all_links_of_system=None,
+        settings_snapshot=None,
     ):
         """Initialize with minimal components for efficiency."""
-        super().__init__(all_links_of_system=all_links_of_system)
+        super().__init__(
+            all_links_of_system=all_links_of_system,
+            settings_snapshot=settings_snapshot,
+        )
         self.search = search
         self.model = model
         self.progress_callback = None
@@ -62,7 +66,9 @@ class DirectSearchStrategy(BaseSearchStrategy):
         )
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
         self.findings_repository = FindingsRepository(self.model)
 
     def analyze_topic(self, query: str) -> Dict:
diff --git a/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py b/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
index 2b92ae5..b4f14e8 100644
--- a/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py
@@ -25,6 +25,7 @@ from typing import Dict, List
 from loguru import logger
 
 from ...citation_handler import CitationHandler
+from ...database.thread_local_session import thread_cleanup
 
 # Model and search should be provided by AdvancedSearchSystem
 from ...utilities.thread_context import (
@@ -121,7 +122,9 @@ class FocusedIterationStrategy(BaseSearchStrategy):
             "forced_answer" if use_browsecomp_optimization else "standard"
         )
         self.citation_handler = citation_handler or CitationHandler(
-            self.model, handler_type=handler_type
+            self.model,
+            handler_type=handler_type,
+            settings_snapshot=settings_snapshot,
         )
         self.findings_repository = FindingsRepository(self.model)
 
@@ -370,6 +373,7 @@ class FocusedIterationStrategy(BaseSearchStrategy):
 
         all_results = []
 
+        @thread_cleanup
         def search_question(q):
             try:
                 # Get the current research context to pass explicitly
@@ -412,6 +416,7 @@ class FocusedIterationStrategy(BaseSearchStrategy):
         completed_searches = 0
         total_searches = len(queries)
 
+        @thread_cleanup
         def search_question_with_progress(q):
             nonlocal completed_searches
             try:
diff --git a/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py b/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
index 539d6db..5c310d2 100644
--- a/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py
@@ -55,7 +55,9 @@ class IterDRAGStrategy(BaseSearchStrategy):
         # Note: questions_by_iteration is already initialized by parent class
 
         # Use provided citation_handler or create one
-        self.citation_handler = CitationHandler(self.model)
+        self.citation_handler = CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize components
         self.question_generator = DecompositionQuestionGenerator(self.model)
diff --git a/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
index 180860f..4487c8d 100644
--- a/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py
@@ -8,6 +8,7 @@ from typing import Dict
 from loguru import logger
 
 from ...citation_handler import CitationHandler
+from ...database.thread_local_session import thread_cleanup
 
 # Model and search should be provided by AdvancedSearchSystem
 from ...utilities.search_utilities import extract_links_from_search_results
@@ -77,7 +78,9 @@ class ParallelSearchStrategy(BaseSearchStrategy):
             self.search.include_full_content = include_text_content
 
         # Use provided citation_handler or create one
-        self.citation_handler = citation_handler or CitationHandler(self.model)
+        self.citation_handler = citation_handler or CitationHandler(
+            self.model, settings_snapshot=settings_snapshot
+        )
 
         # Initialize components
         self.question_generator = StandardQuestionGenerator(self.model)
@@ -227,6 +230,7 @@ class ParallelSearchStrategy(BaseSearchStrategy):
                 )
 
                 # Function for thread pool
+                @thread_cleanup
                 def search_question(q):
                     try:
                         result = self.search.run(q)
diff --git a/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
index ee55d8d..4a9810b 100644
--- a/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
@@ -41,7 +41,9 @@ class RapidSearchS
… [6099 more characters]
step 1
$ ls /repo; ls /repo/local_deep_research 2>/dev/null | head
output · 26 lines
CONTRIBUTING.md
Dockerfile
LICENSE
MANIFEST.in
README.md
SECURITY.md
bearer.yml
community_benchmark_results
cookiecutter-docker
docker-compose.gpu.override.yml
docker-compose.unraid.yml
docker-compose.yml
14 more lines
docs
eslint.config.js
eslint.config.mjs
examples
package-lock.json
package.json
pdm.lock
pyproject.toml
scripts
src
tests
unraid-templates
vite.config.js
vulture_whitelist.py
step 2
$ cat /repo/src/local_deep_research/search_system_factory.py
output · 43 lines
"""
Factory for creating search strategies.
This module provides a centralized way to create search strategies
to avoid code duplication.
"""

from loguru import logger
from typing import Optional, Dict, Any, List
from langchain_core.language_models import BaseChatModel


def _get_setting(
31 more lines
    settings_snapshot: Optional[Dict], key: str, default: Any
) -> Any:
    """Get a setting value from the snapshot, handling nested dict structure."""
    if not settings_snapshot or key not in settings_snapshot:
        return default
    value = settings_snapshot[key]
    # Extract value from dict structure if needed
    if isinstance(value, dict) and "value" in value:
        return value["value"]
    return value


def create_strategy(
    strategy_name: str,
    model: BaseChatModel,
    search: Any,
    all_links_of_system: Optional[List[Dict]] = None,
    settings_snapshot: Optional[Dict] =
…[8630 chars omitted]…
  )

        return SourceBasedSearchStrategy(
            model=model,
            search=search,
            include_text_content=True,
            use_cross_engine_filter=True,
            all_links_of_system=all_links_of_system,
            use_atomic_facts=False,
            settings_snapshot=settings_snapshot,
            search_original_query=kwargs.get("search_original_query", True),
        )
step 3
$ grep -rn "class CitationHandler" /repo/src/local_deep_research/citation_handler.py; sed -n '1,80p' /repo/src/local_deep_research/citation_handler.py
output · 42 lines
8:class CitationHandler:
# citation_handler.py

from typing import Any, Dict, List, Optional, Union

from loguru import logger


class CitationHandler:
    """
    Configurable citation handler that delegates to specific implementations.
    Maintains backward compatibility while allowing strategy-specific handlers.
30 more lines
    """

    def __init__(
        self, llm, handler_type: Optional[str] = None, settings_snapshot=None
    ):
        self.llm = llm
        self.settings_snapshot = settings_snapshot or {}

        # Determine which handler to use
        if handler_type is None:
            # Try to get from settings snapshot, default to standard
            if "citation.handler_type" in self.settings_snapshot:
                value = self.settings_snapshot["citation.handler_type"]
                handler_type = (
                    value["value"]
                    if isinstance(val
…[1612 chars omitted]…
andler,
            )

            logger.info(
                "Using PrecisionExtractionHandler for precise answer extraction"
            )
            return PrecisionExtractionHandler(
                self.llm, settings_snapshot=self.settings_snapshot
            )

        else:
            logger.warning(
                f"Unknown citation handler type: {handler_type}, falling back to standard"
step 4
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; for f in direct_search_strategy source_based_strategy standard_strategy parallel_search_strategy rapid_search_strategy iterdrag_strategy focused_iteration_strategy; do echo "=== $f"; sed -n '1,140p' $f.py | grep -n "def __init__" ; done
output · 14 lines
=== direct_search_strategy
33:    def __init__(
=== source_based_strategy
64:    def __init__(
=== standard_strategy
21:    def __init__(
=== parallel_search_strategy
26:    def __init__(
=== rapid_search_strategy
25:    def __init__(
=== iterdrag_strategy
25:    def __init__(
2 more lines
=== focused_iteration_strategy
53:    def __init__(
step 5
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; for f in direct_search_strategy source_based_strategy standard_strategy parallel_search_strategy rapid_search_strategy iterdrag_strategy focused_iteration_strategy; do echo "=== $f"; awk '/def __init__/,/self\.citation_handler|super\(\)/' $f.py | head -60; done
output · 35 lines
=== direct_search_strategy
    def __init__(
        self,
        search,
        model,
        citation_handler=None,
        include_text_content: bool = True,
        use_cross_engine_filter: bool = True,
        filter_reorder: bool = True,
        filter_reindex: bool = True,
        cross_engine_max_results: int = None,
        all_links_of_system=None,
23 more lines
    ):
        """Initialize with minimal components for efficiency."""
        super().__init__(all_links_of_system=all_links_of_system)
=== source_based_strategy
    def __init__(
        self,
        search,
        model,
        citation_handler=None,
        include_text_content: bool = True,
        use_cross_engine_filter: bool = True,
        filter_reorder: bool = True,
        filter_reindex: bool = True,
        cross_engine_max_results: int = None,
        all_links_of_system=None,
        use_atomic_facts: bool =
…[5270 chars omitted]…
, 200=main behavior)
        prompt_knowledge_truncate: int = 1500,  # Truncate knowledge in LLM prompt (None=unlimited, 1500=main behavior)
        previous_searches_limit: int = 10,  # Limit previous searches shown to LLM (None=unlimited, 10=main behavior)
    ):
        """Initialize with components optimized for focused iteration."""
        super().__init__(all_links_of_system, settings_snapshot)
step 6
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; for f in direct_search_strategy source_based_strategy standard_strategy parallel_search_strategy rapid_search_strategy iterdrag_strategy focused_iteration_strategy; do echo "=== $f"; grep -n -B3 -A3 "CitationHandler(" $f.py | head -20; done
output · 31 lines
=== direct_search_strategy
62-        )
63-
64-        # Use provided citation_handler or create one
65:        self.citation_handler = citation_handler or CitationHandler(self.model)
66-        self.findings_repository = FindingsRepository(self.model)
67-
68-    def analyze_topic(self, query: str) -> Dict:
=== source_based_strategy
127-            self.search.include_full_content = include_text_content
128-
129-        # Use provided citation_handler or create default
19 more lines
130:        self.citation_handler = citation_handler or CitationHandler(self.model)
131-
132-        # Initialize question generator (atomic facts variant is experimental)
133-        if use_atomic_facts:
=== standard_strategy
56-        # Note: questions_by_iteration is already initialized by parent class
57-
58-        # Use provided citation_handler or create one
59:        self.citation_handler = citation_handler or
…[1183 chars omitted]…
compositionQuestionGenerator(self.model)
=== focused_iteration_strategy
120-        handler_type = (
121-            "forced_answer" if use_browsecomp_optimization else "standard"
122-        )
123:        self.citation_handler = citation_handler or CitationHandler(
124-            self.model, handler_type=handler_type
125-        )
126-        self.findings_repository = FindingsRepository(self.model)
step 7
$ cd /repo/src/local_deep_research/advanced_search_system; grep -n "def __init__" -A 20 base_search_strategy.py 2>/dev/null || ls; grep -rn "def __init__" -A15 strategies/direct_search_strategy.py | head -30
output · 33 lines
__init__.py
answer_decoding
candidate_exploration
candidates
constraint_checking
constraints
evidence
filters
findings
knowledge
query_generation
questions
21 more lines
repositories
search_optimization
source_management
strategies
tools
33:    def __init__(
34-        self,
35-        search,
36-        model,
37-        citation_handler=None,
38-        include_text_content: bool = True,
39-        use_cross_engine_filter: bool = True,
40-        filter_reorder: bool = True,
41-        filter_reindex: bool = True,
42-        cross_engine_max_results: int = None,
43-        all_links_of_system=None,
44-    ):
45-        """Initialize with minimal components for efficiency."""
46-        super().__init__(all_links_of_system=all_links_of_system)
47-        self.search = search
48-        self.model = model
step 8
$ cd /repo/src/local_deep_research/advanced_search_system; grep -rn "class AdvancedSearchSystem" --include=*.py .; grep -rn "def __init__" -A 15 $(grep -rln "class AdvancedSearchSystem" --include=*.py .)
output · 21 lines
answer_decoding/browsecomp_answer_decoder.py:29:    def __init__(self):
answer_decoding/browsecomp_answer_decoder.py-30-        self.encoding_schemes = [
answer_decoding/browsecomp_answer_decoder.py-31-            "base64",
answer_decoding/browsecomp_answer_decoder.py-32-            "hex",
answer_decoding/browsecomp_answer_decoder.py-33-            "url_encoding",
answer_decoding/browsecomp_answer_decoder.py-34-            "rot13",
answer_decoding/browsecomp_answer_decoder.py-35-            "caesar_cipher",
answer_decoding/browsecomp_answer_decoder.py-36-        ]
answer_decoding/browsecomp_answer_decoder.py-37-
answer_decoding/browsecomp_answer_decoder.py-38-        # Patterns that suggest encoded content
answer_decoding/browsecomp_answer_decoder.py-39-        self.encoded_patterns = [
answer_decoding/browsecomp_answer_decoder.py-40-            r"^[A-Za-z0-9+/]+=*$",  # Base64 patter
9 more lines
…[8630 chars omitted]…
py-35-        self.name = name
tools/base_tool.py-36-        self.description = description
tools/base_tool.py-37-        self.parameters: Dict[str, Dict[str, Any]] = {}
tools/base_tool.py-38-
tools/base_tool.py-39-    @abstractmethod
tools/base_tool.py-40-    def execute(self, **kwargs) -> Any:
tools/base_tool.py-41-        """
tools/base_tool.py-42-        Execute the tool with the given parameters.
step 9
$ cd /repo/src/local_deep_research/advanced_search_system; grep -rln "class AdvancedSearchSystem" --include=*.py .;
step 10
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; grep -n "import\|from" direct_search_strategy.py | head; grep -rn "class .*SearchStrategy\|class AdvancedSearchSystem" *.py | head
output · 16 lines
10:from typing import Dict
12:from loguru import logger
14:from ...citation_handler import CitationHandler
17:from ..filters.cross_engine_filter import CrossEngineFilter
18:from ..findings.repository import FindingsRepository
19:from .base_strategy import BaseSearchStrategy
adaptive_decomposition_strategy.py:43:class AdaptiveDecompositionStrategy(BaseSearchStrategy):
base_strategy.py:12:class BaseSearchStrategy(ABC):
browsecomp_entity_strategy.py:97:class BrowseCompEntityStrategy(BaseSearchStrategy):
browsecomp_optimized_strategy.py:35:class BrowseCompOptimizedStrategy(BaseSearchStrategy):
constrained_search_strategy.py:24:class ConstrainedSearchStrategy(EvidenceBasedStrategy):
direct_search_strategy.py:22:class DirectSearchStrategy(BaseSearchStrategy):
4 more lines
entity_aware_source_strategy.py:13:class EntityAwareSourceStrategy(SourceBasedSearchStrategy):
evidence_based_strategy.py:24:class EvidenceBasedStrategy(BaseSearchStrategy):
focused_iteration_strategy.py:40:class FocusedIterationStrategy(BaseSearchStrategy):
improved_evidence_based_strategy.py:41:class ImprovedEvidenceBasedStrategy(BaseSearchStrategy):
step 11
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; sed -n '1,80p' base_strategy.py
output · 38 lines
"""
Base class for all search strategies.
Defines the common interface and shared functionality for different search approaches.
"""

from abc import ABC, abstractmethod
from typing import Any, Callable, Optional

from loguru import logger


class BaseSearchStrategy(ABC):
26 more lines
    """Abstract base class for all search strategies."""

    def __init__(
        self,
        all_links_of_system=None,
        settings_snapshot=None,
        questions_by_iteration=None,
        search_original_query: bool = True,
    ):
        """Initialize the base strategy with common attributes.

        Args:
            all_links_of_system: List to store all discovered links
            settings_snapshot: Settings snapshot for configuration
            questions_by_iteration: Dictionary of questions by iteration
            search_original_query: Whether to include the original query in the first iterati
…[1907 chars omitted]…
opagated.  A dedicated
        ``set_termination_check`` method could be introduced later if the
        callback-based approach becomes a maintenance burden.

        The callback in ``research_service.py`` recognises the
        ``"termination_check"`` phase and returns immediately after the
        flag check -- no UI logging or socket emission occurs.
        """
        if self.progress_callback:
step 12
$ grep -n "_get_output_instruction_prefix" -A 20 /repo/src/local_deep_research/citation_handlers/base_citation_handler.py | head -40
output · 21 lines
28:    def _get_output_instruction_prefix(self) -> str:
29-        """
30-        Get formatted output instructions from settings if present.
31-
32-        This allows users to customize output language, tone, style, and formatting
33-        for research answers and reports. Instructions are prepended to prompts
34-        sent to the LLM.
35-
36-        Returns:
37-            str: Formatted instruction prefix if custom instructions are set,
38-                 empty string otherwise.
39-
9 more lines
40-        Examples:
41-            - "Respond in Spanish with formal academic tone"
42-            - "Use simple language suitable for beginners"
43-            - "Be concise with bullet points"
44-        """
45-        output_instructions = self.get_setting(
46-            "general.output_instructions", ""
47-        ).strip()
48-
step 13
$ cd /repo; sed -n '1,120p' tests/database/test_thread_local_session.py; echo ====; grep -rn "thread_cleanup" src tests | grep -v test_thread_local | head -20
output · 30 lines
"""Tests for database/thread_local_session.py."""

import threading
from unittest.mock import Mock, call, patch

from sqlalchemy.exc import OperationalError, PendingRollbackError


class TestThreadLocalSessionManager:
    """Tests for ThreadLocalSessionManager class."""

    def test_init_creates_thread_local_storage(self):
18 more lines
        """Test that initialization creates thread-local storage."""
        from local_deep_research.database.thread_local_session import (
            ThreadLocalSessionManager,
        )

        manager = ThreadLocalSessionManager()
        assert hasattr(manager, "_local")
        assert isinstance(manager._local, threading.local)

    def test_init_creates_credentials_tracking(self):
        """Test that initialization creates credentials tracking dict."""
        from local_deep_research.database.thread_local_session import (
            ThreadLocalSessionMa
…[5012 chars omitted]…
manager_extended.py:234:    def test_thread_cleanup_by_thread_id(self, mock_data_dir):
tests/database/test_database_manager_extended.py:257:    def test_thread_cleanup_all(self, mock_data_dir):
tests/database/test_encrypted_db_manager.py:1016:    def test_concurrent_thread_cleanup(self, mock_data_dir, tmp_path):
tests/integration/test_error_recovery.py:464:    def test_thread_cleanup_on_failure(self):
step 14
$ cd /repo; grep -rn "thread_cleanup" tests/database/test_thread_local_session.py | head; grep -rn "thread_cleanup\|executor\|submit" src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py | head -20
output · 2 lines
245:                ) as executor:
247:                        executor.submit(search_question, q)
step 15
$ cd /repo; sed -n '200,270p' src/local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py; grep -rn "ThreadPoolExecutor\|submit(" src/local_deep_research/advanced_search_system/strategies/*.py | head -20
output · 23 lines
questions_per_iteration=int(questions_per_iter)
                        if questions_per_iter is not None
                        else 3,
                        questions_by_iteration=self.questions_by_iteration,
                    )

                    # Use only the new questions for this iteration's searches
                    all_questions = questions

                    # Store in questions_by_iteration
                    self.questions_by_iteration[iteration] = questions
                    logger.info(
11 more lines
                        f"Generated questions for iteration {iteration}: {questions}"
                    )

                # Skip if no questions (all_questions includes original query in iteration 1)
                if not all_questions:
                    logger.warning(
                        f"No questions generated for iteration {iteration},
…[5082 chars omitted]…
rategies/modular_strategy.py:7:from concurrent.futures import ThreadPoolExecutor, as_completed
src/local_deep_research/advanced_search_system/strategies/modular_strategy.py:522:                # Execute batch searches in parallel using ThreadPoolExecutor
src/local_deep_research/advanced_search_system/strategies/modular_strategy.py:524:                with ThreadPoolExecutor(max_workers=8) as executor:
step 16

Now let me implement. First `thread_cleanup`:

$ cd /repo; python - <<'PY'
output · 1 lines
all ok
step 17
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; python - <<'PY'
output · 1 lines
done
step 18
$ cd /repo; grep -n "rapid\|parallel\|iterdrag\|direct" src/local_deep_research/search_system_factory.py | head -20
output · 20 lines
313:    elif strategy_name_lower == "iterdrag":
314:        from .advanced_search_system.strategies.iterdrag_strategy import (
326:    elif strategy_name_lower == "parallel":
327:        from .advanced_search_system.strategies.parallel_search_strategy import (
341:    elif strategy_name_lower == "rapid":
342:        from .advanced_search_system.strategies.rapid_search_strategy import (
491:        "parallel-constrained",
492:        "parallel_constrained",
494:        from .advanced_search_system.strategies.parallel_constrained_strategy import (
509:            parallel_workers=kwargs.get("parallel_workers", 100),
533:            parallel_workers=kwargs.get("parallel_workers", 100),
556:            parallel_workers=kwargs.get("parallel_workers", 100),
8 more lines
583:            parallel_workers=kwargs.get("parallel_workers", 100),
591:            use_direct_property_search=kwargs.get(
592:                "use_direct_property_search", True
619:            parallel_workers=kwargs.get("parallel_workers", 100),
627:            use_direct_property_search=kwargs.get(
628:                "use_direct_property_search", True
660:            parallel_workers=kwargs.get("parallel_workers", 10),
668:            use_direct_property_search=kwargs.get(
step 19
$ cd /repo; sed -n '310,360p' src/local_deep_research/search_system_factory.py
output · 41 lines
)

    # IterDRAG strategy
    elif strategy_name_lower == "iterdrag":
        from .advanced_search_system.strategies.iterdrag_strategy import (
            IterDRAGStrategy,
        )

        return IterDRAGStrategy(
            model=model,
            search=search,
            all_links_of_system=all_links_of_system,
29 more lines
            settings_snapshot=settings_snapshot,
        )

    # Parallel strategy
    elif strategy_name_lower == "parallel":
        from .advanced_search_system.strategies.parallel_search_strategy import (
            ParallelSearchStrategy,
        )

        return ParallelSearchStrategy(
            model=model,
            search=search,
            include_text_content=kwargs.get("include_text_content", True),
            use_cross_engine_filter=kwargs.get("use_cross_engine_filter", True),
            all_links_of_system=all_links_of_system,
       
…[364 chars omitted]…
nks_of_system,
            settings_snapshot=settings_snapshot,
        )

    # Recursive decomposition strategy
    elif strategy_name_lower in ["recursive", "recursive-decomposition"]:
        from .advanced_search_system.strategies.recursive_decomposition_strategy import (
            RecursiveDecompositionStrategy,
        )

        return RecursiveDecompositionStrategy(
            model=model,
step 20
$ cd /repo; python - <<'PY'
output · 11 lines
source-based 'User-Specified Output Style: Respond in Spanish\n\n'
Traceback (most recent call last):
  File "<stdin>", line 7, in <module>
  File "/repo/src/local_deep_research/search_system_factory.py", line 859, in create_strategy
    return StandardSearchStrategy(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/repo/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py", line 39, in __init__
    get_setting_from_snapshot(
  File "/repo/src/local_deep_research/config/thread_settings.py", line 154, in get_setting_from_snapshot
    raise NoSettingsContextError(
local_deep_research.config.thread_settings.NoSettingsContextError: No settings context available in thread for key 'search.iterations'. All settings must be passed via settings_snapshot.
step 21
$ cd /repo; sed -n '30,60p' src/local_deep_research/advanced_search_system/strategies/standard_strategy.py; sed -n '140,170p' src/local_deep_research/config/thread_settings.py
output · 42 lines
super().__init__(
            all_links_of_system=all_links_of_system,
            settings_snapshot=settings_snapshot,
        )
        self.search = search
        self.model = model

        # Get iterations setting
        self.max_iterations = int(
            get_setting_from_snapshot(
                "search.iterations", settings_snapshot=settings_snapshot
            )
30 more lines
        )

        self.questions_per_iteration = int(
            get_setting_from_snapshot(
                "search.questions_per_iteration",
                settings_snapshot=settings_snapshot,
            )
        )
        self.context_limit = int(
            get_setting_from_snapshot(
                "general.knowledge_accumulation_context_limit",
                settings_snapshot=settings_snapshot,
            )
        )
        # Note: questions_by_iteration is already initialized by parent c
…[780 chars omitted]…
    )


def get_llm_setting_from_snapshot(
    key, default=None, username=None, settings_snapshot=None
):
    """Get setting from snapshot with fallback LLM check enabled.

    Convenience wrapper used by LLM provider modules that always need
    ``check_fallback_llm=True``.
    """
    return get_setting_from_snapshot(
        key, default, username, settings_snapshot, check_fallback_llm=True
    )
step 22
$ cd /repo; python - <<'PY'
output · 11 lines
source-based 'User-Specified Output Style: Respond in Spanish\n\n'
Traceback (most recent call last):
  File "<stdin>", line 8, in <module>
  File "/repo/src/local_deep_research/search_system_factory.py", line 859, in create_strategy
    return StandardSearchStrategy(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/repo/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py", line 51, in __init__
    get_setting_from_snapshot(
  File "/repo/src/local_deep_research/config/thread_settings.py", line 154, in get_setting_from_snapshot
    raise NoSettingsContextError(
local_deep_research.config.thread_settings.NoSettingsContextError: No settings context available in thread for key 'general.knowledge_accumulation_context_limit'. All settings must be passed via settings_snapshot.
step 23
$ cd /repo; python - <<'PY'
output · 7 lines
source-based 'User-Specified Output Style: Respond in Spanish\n\n'
standard 'User-Specified Output Style: Respond in Spanish\n\n'
parallel 'User-Specified Output Style: Respond in Spanish\n\n'
rapid 'User-Specified Output Style: Respond in Spanish\n\n'
iterdrag 'User-Specified Output Style: Respond in Spanish\n\n'
focused-iteration 'User-Specified Output Style: Respond in Spanish\n\n'
OK
step 24
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; python - <<'PY'
output · 10 lines
1
5:import concurrent.futures
6:from typing import Dict
8:from loguru import logger
10:from ...citation_handler import CitationHandler
13:from ...utilities.search_utilities import extract_links_from_search_results
14:from ..filters.cross_engine_filter import CrossEngineFilter
15:from ..findings.repository import FindingsRepository
16:from ..questions.standard_question import StandardQuestionGenerator
17:from .base_strategy import BaseSearchStrategy
step 25
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; python - <<'PY'
output · 10 lines
0
22:import concurrent.futures
23:from typing import Dict, List
25:from loguru import logger
27:from ...citation_handler import CitationHandler
30:from ...utilities.thread_context import (
34:from ..candidate_exploration import ProgressiveExplorer
35:from ..findings.repository import FindingsRepository
36:from ..questions import BrowseCompQuestionGenerator
37:from .base_strategy import BaseSearchStrategy
step 26
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; sed -n '370,400p;470,495p' focused_iteration_strategy.py
output · 35 lines
logger.warning("No queries provided for parallel search")
            return []

        all_results = []

        def search_question(q):
            try:
                # Get the current research context to pass explicitly

                current_context = get_search_context()
                result = self.search.run(q, research_context=current_context)
                return {"question": q, "results": result or []}
23 more lines
            except Exception:
                logger.exception(f"Error searching '{q}'")
                return {"question": q, "results": [], "error": "Search failed"}

        # Create context-preserving wrapper for the search function
        context_aware_search = preserve_research_context(search_question)

        # Run searches in parallel
        with concurrent.futures.ThreadPoolExecutor(
            max_workers=len(queries)
        ) as executor:
 
…[845 chars omitted]…
aware_search_with_progress, q)
                for q in queries
            ]

            total_results_found = 0
            for future in concurrent.futures.as_completed(futures):
                result_dict = future.result()
                results = result_dict.get("results", [])
                all_results.extend(results)
                total_results_found += result_dict.get("result_count", 0)
step 27
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; python - <<'PY'
output · 3 lines
ok
377:        def search_question(q):
420:        def search_question_with_progress(q):
step 28
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; sed -n '375,380p;418,424p' focused_iteration_strategy.py; sed -n '78,92p' parallel_search_strategy.py
output · 26 lines
@thread_cleanup
        def search_question(q):
            try:
                # Get the current research context to pass explicitly


        @thread_cleanup
        def search_question_with_progress(q):
            nonlocal completed_searches
            try:
                # Report starting this search
                self._update_progress(
14 more lines
            self.search.include_full_content = include_text_content

        # Use provided citation_handler or create one
        self.citation_handler = citation_handler or CitationHandler(
            self.model, settings_snapshot=settings_snapshot
        )

        # Initialize components
        self.question_generator = StandardQuestionGenerator(self.model)
        self.findings_repository = FindingsRepository(self.model)

    def analyze_topic(self, query: str) -> Dict:
        """
        Analyze a topic using parallel search, supporting multiple iterations.
step 29
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; grep -n "def search_question" -B2 parallel_search_strategy.py
output · 3 lines
232-                # Function for thread pool
233-                @thread_cleanup
234:                def search_question(q):
step 30
$ cd /repo; python -m pytest tests/database/test_thread_local_session.py tests/core/test_citation_handler_strategies.py tests/core/test_search_system_factory.py tests/core/test_search_system_factory_extended.py -q 2>&1 | tail -15
output · 15 lines
/usr/local/lib/python3.12/site-packages/_pytest/config/__init__.py:1464: PytestConfigWarning: Unknown config option: timeout
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

../usr/local/lib/python3.12/site-packages/_pytest/config/__init__.py:1464
  /usr/local/lib/python3.12/site-packages/_pytest/config/__init__.py:1464: PytestConfigWarning: Unknown config option: timeout_method
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

tests/database/test_thread_local_session.py::TestThreadLocalSessionManager::test_init_creates_thread_local_storage
  /repo/src/local_deep_research/web_search_engines/engines/local_embedding_manager.py:7: DeprecationWarning: `langchain-community` is being sunset and is no longer actively maintained. See https://github.com/langchain-ai/langchain-community/issues/674 for details and migration guidance toward standalone integration packages.
    from langchain_community.embeddings import (
3 more lines
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
159 passed, 3 warnings in 26.87s
step 31
$ cd /repo; python -m pytest tests/core/test_citation_handler.py tests/core/test_citation_handler_factory.py tests/core/test_citation_handler_extended.py tests/database -q 2>&1 | tail -3
output · 2 lines
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1269 passed, 8 skipped, 4 warnings in 213.74s (0:03:33)
step 32
$ cd /repo; python - <<'PY'
output · 20 lines
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
  File "/usr/local/lib/python3.12/unittest/mock.py", line 961, in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/unittest/mock.py", line 949, in assert_called_with
    raise AssertionError(_error_message()) from cause
AssertionError: expected call not found.
Expected: CitationHandler(<MagicMock id='130186507096080'>, settings_snapshot=None)
  Actual: CitationHandler(<MagicMock id='130186507096080'>, settings_snapshot={'search.iterations': 1, 'search.questions_per_iteration': 1, 'general.knowledge_accumulation_context_limit': 2})
__init__.py
answer_decoding
8 more lines
candidate_exploration
candidates
conftest.py
constraint_checking
constraints
evidence
filters
findings
step 33
$ cd /repo; python - <<'PY'
output · 7 lines
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "/repo/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py", line 39, in __init__
    get_setting_from_snapshot(
  File "/repo/src/local_deep_research/config/thread_settings.py", line 154, in get_setting_from_snapshot
    raise NoSettingsContextError(
local_deep_research.config.thread_settings.NoSettingsContextError: No settings context available in thread for key 'search.iterations'. All settings must be passed via settings_snapshot.
step 34
$ cd /repo; python - <<'PY'
output · 6 lines
Traceback (most recent call last):
  File "<stdin>", line 7, in <module>
  File "/repo/src/local_deep_research/advanced_search_system/strategies/standard_strategy.py", line 38, in __init__
    self.max_iterations = int(
                          ^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
step 35
$ cd /repo; python - <<'PY'
output · 1 lines
ok
step 36
$ cd /repo; ls tests/advanced_search_system/strategies 2>/dev/null; python -m pytest tests/advanced_search_system -q -x 2>&1 | tail -3
output · 31 lines
__init__.py
conftest.py
test_base_strategy.py
test_browsecomp_entity_strategy.py
test_browsecomp_optimized_strategy.py
test_constrained_search_extended.py
test_constrained_search_strategy.py
test_constraint_parallel_strategy.py
test_decomposition_strategies.py
test_dual_confidence_strategy.py
test_evidence_based_strategy.py
test_evidence_based_v2.py
19 more lines
test_evidence_based_v2_extended.py
test_focused_iteration_strategy.py
test_iterative_reasoning_strategy.py
test_iterative_refinement_strategy.py
test_iterdrag_strategy.py
test_llm_driven_modular_strategy.py
test_modular_strategy.py
test_news_strategy_pure_logic.py
test_parallel_strategies.py
test_rapid_search_strategy_extended.py
test_smart_query_strategy.py
test_standard_strategy.py
test_strategy_helpers_pure_logic.py
test_strategy_pure_logic.py
test_topic_organization_extended.py
test_topic_organization_strategy.py
ERROR tests/advanced_search_system/questions/test_news_question_generator.py
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
3 warnings, 1 error in 5.81s
step 37
$ cd /repo; python -m pytest tests/advanced_search_system -q 2>&1 | tail -5; git stash; python -m pytest tests/advanced_search_system/questions/test_news_question_generator.py -q 2>&1 | tail -3; git stash pop
output · 23 lines
=========================== short test summary info ============================
ERROR tests/advanced_search_system/questions/test_news_question_generator.py
ERROR tests/advanced_search_system/strategies/test_news_strategy_pure_logic.py
!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!!
3 warnings, 2 errors in 8.65s
Saved working directory and index state WIP on master: b18f5f9 base
ERROR tests/advanced_search_system/questions/test_news_question_generator.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
2 warnings, 1 error in 1.05s
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
11 more lines
  (use "git restore <file>..." to discard changes in working directory)
	modified:   src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
	modified:   src
…[358 chars omitted]…
h_strategy.py
	modified:   src/local_deep_research/advanced_search_system/strategies/source_based_strategy.py
	modified:   src/local_deep_research/advanced_search_system/strategies/standard_strategy.py
	modified:   src/local_deep_research/database/thread_local_session.py

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (bacf3c8c00358a2fddfccf1a3a14ade9b7cb00b0)
step 38
$ cd /repo; python -m pytest tests/advanced_search_system -q --ignore=tests/advanced_search_system/questions/test_news_question_generator.py --ignore=tests/advanced_search_system/strategies/test_news_strategy_pure_logic.py 2>&1 | tail -3
output · 3 lines
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_generate_intelligent_combinations
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_quick_confidence_check
6 failed, 2876 passed, 3 skipped, 5 warnings in 38.89s
step 39
$ cd /repo; python -m pytest tests/advanced_search_system -q --ignore=tests/advanced_search_system/questions/test_news_question_generator.py --ignore=tests/advanced_search_system/strategies/test_news_strategy_pure_logic.py 2>&1 | grep FAILED; git stash -q; python -m pytest tests/advanced_search_system/strategies/test_modular_strategy.py tests/advanced_search_system/strategies/test_parallel_strategies.py -q 2>&1 | tail -3; git stash pop -q
output · 9 lines
FAILED tests/advanced_search_system/strategies/test_browsecomp_entity_strategy.py::TestBrowseCompEntityStrategySearch::test_search_returns_tuple
FAILED tests/advanced_search_system/strategies/test_browsecomp_entity_strategy.py::TestBrowseCompEntityStrategySearch::test_search_calls_progress_callback
FAILED tests/advanced_search_system/strategies/test_iterdrag_strategy.py::TestIterDRAGStrategyInit::test_init_creates_components
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_decompose_constraints_intelligently
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_generate_intelligent_combinations
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_quick_confidence_check
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_generate_intelligent_combinations
FAILED tests/advanced_search_system/strategies/test_modular_strategy.py::TestAsyncMethods::test_quick_confidence_check
3 failed, 35 passed, 4 warnings in 21.61s
step 40
$ cd /repo; python -m pytest tests/advanced_search_system/strategies/test_iterdrag_strategy.py::TestIterDRAGStrategyInit::test_init_creates_components -q 2>&1 | tail -30
output · 13 lines
2026-10-01 20:05:37.480 | DEBUG    | src.local_deep_research.llm.providers.auto_discovery:discover_providers:116 -   Found class: OpenAICompatibleProvider, bases: (<class 'local_deep_research.llm.providers.base.BaseLLMProvider'>,)
2026-10-01 20:05:37.480 | DEBUG    | src.local_deep_research.llm.providers.auto_discovery:discover_providers:116 -   Found class: OpenAIProvider, bases: (<class 'local_deep_research.llm.providers.openai_base.OpenAICompatibleProvider'>,)
2026-10-01 20:05:37.481 | DEBUG    | src.local_deep_research.llm.providers.auto_discovery:discover_providers:102 - Processing module: openrouter from /repo/src/local_deep_research/llm/providers/implementations/openrouter.py
2026-10-01 20:05:37.481 | DEBUG    | src.local_deep_research.llm.providers.auto_discovery:discover_providers:111 - Inspecting module openrouter for Provider classes
2026-10-01 20:05:37.481 | DEBUG    | src
…[2444 chars omitted]…
e toward standalone integration packages.
    from langchain_community.embeddings import (

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/advanced_search_system/strategies/test_iterdrag_strategy.py::TestIterDRAGStrategyInit::test_init_creates_components
1 more lines
1 failed, 3 warnings in 20.23s
step 41
$ cd /repo; python -m pytest tests/advanced_search_system/strategies/test_iterdrag_strategy.py::TestIterDRAGStrategyInit::test_init_creates_components -q 2>&1 | grep -B5 "Error\|assert" | head -30; grep -n "test_init_creates_components" -A 20 tests/advanced_search_system/strategies/test_iterdrag_strategy.py
output · 34 lines
mock_search = Mock()
        mock_model = Mock()
    
        IterDRAGStrategy(search=mock_search, model=mock_model)
    
>       mock_citation.assert_called_once_with(mock_model)

tests/advanced_search_system/strategies/test_iterdrag_strategy.py:109: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.12/unittest/mock.py:961: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
--
22 more lines
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <MagicMock name='CitationHandler' id='138954893423392'>
args = (<Mock id='138954892007744'>,), kwargs = {}

    def assert_called_with(self, /, *args, **kwargs):
        """assert that the last call was made with the specified arguments.
    
        Raises an AssertionError if the args and keyword args passed in are
--
        if sel
…[795 chars omitted]…
model=mock_model)
108-
109-        mock_citation.assert_called_once_with(mock_model)
110-        mock_question.assert_called_once_with(mock_model)
111-        mock_knowledge.assert_called_once_with(mock_model)
112-        mock_findings.assert_called_once_with(mock_model)
113-
114-    @patch(
115-        "local_deep_research.advanced_search_system.strategies.iterdrag_strategy.CitationHandler"
116-    )
step 42
$ cd /repo; git stash -q; python -m pytest tests/advanced_search_system/strategies/test_browsecomp_entity_strategy.py tests/advanced_search_system/strategies/test_iterdrag_strategy.py -q 2>&1 | tail -3; git stash pop -q
output · 3 lines
FAILED tests/advanced_search_system/strategies/test_browsecomp_entity_strategy.py::TestBrowseCompEntityStrategySearch::test_search_returns_tuple
FAILED tests/advanced_search_system/strategies/test_browsecomp_entity_strategy.py::TestBrowseCompEntityStrategySearch::test_search_calls_progress_callback
2 failed, 40 passed, 3 warnings in 19.84s
step 43
$ cd /repo; git diff --stat; python -m pytest tests/database/test_thread_local_session.py -q 2>&1 | tail -2
output · 11 lines
.../strategies/direct_search_strategy.py           | 10 ++-
 .../strategies/focused_iteration_strategy.py       |  7 +-
 .../strategies/iterdrag_strategy.py                |  4 +-
 .../strategies/parallel_search_strategy.py         |  6 +-
 .../strategies/rapid_search_strategy.py            |  4 +-
 .../strategies/source_based_strategy.py            |  4 +-
 .../strategies/standard_strategy.py                |  4 +-
 .../database/thread_local_session.py               | 86 +++++++++++++++++-----
 8 files changed, 98 insertions(+), 27 deletions(-)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
22 passed, 3 warnings in 17.45s
step 44
$ cd /repo; git diff src/local_deep_research/database/thread_local_session.py
output · 47 lines
diff --git a/src/local_deep_research/database/thread_local_session.py b/src/local_deep_research/database/thread_local_session.py
index 8ced415..4411376 100644
--- a/src/local_deep_research/database/thread_local_session.py
+++ b/src/local_deep_research/database/thread_local_session.py
@@ -187,36 +187,84 @@ def cleanup_current_thread():
     thread_session_manager.cleanup_thread()
 
 
-def thread_cleanup(func):
-    """Decorator ensuring all thread-local resources are cleaned up when func exits.
+class thread_cleanup:
+    """Ensure all thread-local resources are cleaned up when a function exits.
35 more lines
+
+    Supports four usage shapes:
+
+    1. Bare decorator::
+
+           @thread_cleanup
+           def worker():
+               ...
+
+    2. Decorator factory::
+
+           @thread_cleanup()
+           def worker():
+               ...
+
+    3. Context manager::
+
+           with th
…[2087 chars omitted]…
== 1 and callable(args[0]) and not kwargs:
+            return thread_cleanup(args[0])
 
-    return wrapper
+        # Used as a decorator factory or context manager: thread_cleanup()
+        return self
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self._run_cleanup()
+        return False
 
 
 # Context manager for automatic cleanup
step 45
$ cd /repo; python -m pytest tests/news/subscription_manager -q 2>&1 | tail -2; python -m pytest tests/core -q -k "thread" 2>&1 | tail -2
output · 4 lines
FAILED tests/news/subscription_manager/test_scheduler.py::TestCheckUserOverdueSubscriptions::test_overdue_schedules_immediate_jobs
7 failed, 146 passed, 3 warnings in 23.25s
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
395 deselected, 2 warnings in 4.82s
step 46
$ cd /repo; git stash -q; python -m pytest tests/news/subscription_manager -q 2>&1 | tail -2; git stash pop -q
output · 2 lines
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
153 passed, 3 warnings in 21.80s
step 47
$ cd /repo; python -m pytest tests/news/subscription_manager/test_scheduler.py -q 2>&1 | grep -A 30 "test_overdue_schedules_immediate_jobs" | head -50
output · 29 lines
___ TestCheckUserOverdueSubscriptions.test_overdue_schedules_immediate_jobs ____

self = <subscription_manager.test_scheduler.TestCheckUserOverdueSubscriptions object at 0x7a15246c1520>
scheduler = <local_deep_research.news.subscription_manager.scheduler.NewsScheduler object at 0x7a135ab5eb10>
mock_background_scheduler = <MagicMock name='BackgroundScheduler()' id='134223544837440'>

    def test_overdue_schedules_immediate_jobs(
        self, scheduler, mock_background_scheduler
    ):
        """_check_user_overdue_subscriptions schedules overdue subscriptions."""
        scheduler.user_sessions["testuser"] = {
            "password": "testpass",
17 more lines
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
    
        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.name = "Overdue"
        mock_subscription
…[557 chars omitted]…
ions("testuser")

tests/news/subscription_manager/test_scheduler.py:662: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/local_deep_research/database/thread_local_session.py:253: in __call__
--
FAILED tests/news/subscription_manager/test_scheduler.py::TestCheckUserOverdueSubscriptions::test_overdue_schedules_immediate_jobs
7 failed, 45 passed, 3 warnings in 22.49s
step 48
$ cd /repo; sed -n '595,625p' src/local_deep_research/news/subscription_manager/scheduler.py
output · 31 lines
logger.info(
                    f"[DOC_SCHEDULER] Successfully verified job {job_id} exists, next run: {job.next_run_time}"
                )
            else:
                logger.error(
                    f"[DOC_SCHEDULER] Failed to verify job {job_id} exists!"
                )

        except Exception:
            logger.exception(
                f"Error scheduling document processing for {username}"
            )
19 more lines
    @thread_cleanup
    def _process_user_documents(self, username: str):
        """Process documents for a user."""
        logger.info(f"[DOC_SCHEDULER] Processing documents for user {username}")
        start_time = datetime.now(UTC)

        try:
            session_info = self.user_sessions.get(username)
            if not session_info:
                logger.warning(
                    f"[DOC_SCHEDULER] No session info found for user {username}"
                )
                return

            password = session_info["password"]
            logger.debug(
                f"[DOC_SCHEDULER] Starting document processing for {username}"
            )
step 49
$ cd /repo; python -m pytest tests/news/subscription_manager/test_scheduler.py::TestCheckUserOverdueSubscriptions::test_overdue_schedules_immediate_jobs -q 2>&1 | sed -n '/thread_local_session.py:253/,/Error/p' | head -30
output · 14 lines
src/local_deep_research/database/thread_local_session.py:253: in __call__
    return self._run(self.func, args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <local_deep_research.database.thread_local_session.thread_cleanup object at 0x715281869610>
func = <function NewsScheduler._check_user_overdue_subscriptions at 0x7152817af420>
args = ('testuser',), kwargs = {}

    def _run(self, func, args, kwargs):
        try:
>           return func(*args, **kwargs)
2 more lines
                   ^^^^^^^^^^^^^^^^^^^^^
E           TypeError: NewsScheduler._check_user_overdue_subscriptions() missing 1 required positional argument: 'username'
step 50
$ cd /repo; python - <<'PY'
output · 2 lines
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
153 passed, 3 warnings in 19.25s
step 51
$ cd /repo; python - <<'PY'
output · 1 lines
ALL OK
step 52
$ cd /repo; python -m pytest tests/database tests/core/test_citation_handler_strategies.py tests/core/test_search_system_factory.py tests/core/test_search_system_factory_extended.py tests/news/subscription_manager -q 2>&1 | tail -2; python -m pytest tests/advanced_search_system/strategies/test_parallel_strategies.py tests/advanced_se
output · 4 lines
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1478 passed, 8 skipped, 4 warnings in 231.30s (0:03:51)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
115 passed, 3 warnings in 17.32s
step 53
$ cd /repo; git diff src/local_deep_research/advanced_search_system/ | head -120
output · 23 lines
diff --git a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
index 71da2c9..e793413 100644
--- a/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/direct_search_strategy.py
@@ -41,9 +41,13 @@ class DirectSearchStrategy(BaseSearchStrategy):
         filter_reindex: bool = True,
         cross_engine_max_results: int = None,
         all_links_of_system=None,
+        settings_snapshot=None,
     ):
         """Initialize with minimal components for efficiency."""
-        super().__init__(all_links_of_system=all_links_of_system)
11 more lines
+        super().__init__(
+            all_links_of_system=all_links_of_system,
+            settings_snapshot=settings_snapshot,
+        )
 
…[4727 chars omitted]…
rch_system/strategies/rapid_search_strategy.py b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
index ee55d8d..4a9810b 100644
--- a/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
+++ b/src/local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py
@@ -41,7 +41,9 @@ class RapidSearchStrategy(BaseSearchStrategy):
step 54

All changes are in place and validated:

$ done: {"notes": "Wired settings_snapshot into CitationHandler construction across all strategies (incl. direct-search, which now accepts it); rewrote thread_cleanup as a flexible callable class supporting bare decorator, factory, context manager, and inline wrapper shapes with wraps metadata, debug-logged cleanup errors, and method-descriptor support; wrapped parallel/focused-iteration executor workers in thread_cleanup."}
output · 1 lines
submitted.