learningcircuit-local-deep-research-2517-2502
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 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]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]$ 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
$ 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),
)$ 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"$ 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__(
$ 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)$ 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; doneoutput · 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)
$ 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
$ 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.$ cd /repo/src/local_deep_research/advanced_search_system; grep -rln "class AdvancedSearchSystem" --include=*.py .;
$ 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):
$ 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:$ 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-
$ 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):$ 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)
$ 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 -20output · 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:Now let me implement. First `thread_cleanup`:
$ cd /repo; python - <<'PY'
output · 1 lines
all ok
$ cd /repo/src/local_deep_research/advanced_search_system/strategies; python - <<'PY'
output · 1 lines
done
$ 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($ 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,$ 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.$ 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
)$ 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.$ 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
$ 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
$ 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
$ 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)$ 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):
$ 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.$ 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):
$ 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
$ 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)
$ 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_decoding8 more lines
candidate_exploration candidates conftest.py constraint_checking constraints evidence filters findings
$ 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.$ 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'$ cd /repo; python - <<'PY'
output · 1 lines
ok
$ 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
$ 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)$ 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
$ 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
$ 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_components1 more lines
1 failed, 3 warnings in 20.23s
$ 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- )$ 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
$ 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
$ 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
$ 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
$ 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
$ 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$ 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}"
)$ 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'
$ 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
$ cd /repo; python - <<'PY'
output · 1 lines
ALL OK
$ 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
$ 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):
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.