This release includes breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
+14 more
Affected surfaces
Summary
AI summaryBroad release touches π Bug Fixes, β¬οΈ Dependencies, test, and β¨ New Features.
Full changelog
Chat Mode, credential-safe logging, and a wave of fixes
β¨ Chat Mode β interactive multi-turn research
The headline feature: Chat Mode lets you have a back-and-forth conversation with the research agent. Each follow-up accumulates context from earlier turns, streams progress and citations live, and persists sessions in your encrypted database.
- Follow-up questions build on the prior conversation using a configurable context mode (
chat.followup_context_mode): an LLM summary focused on your new question (default), raw findings, the full transcript, or no prior context. Onlysummarymakes an extra LLM call. - A "Summarizing previous conversationβ¦" indicator appears while the context is being built, so you know the pause is intentional.
- The thinking bubble now shows what the agent is currently reasoning about between tool calls, and live milestones use friendly wording (
π Searching PubMedinstead ofTool: search_pubmed). - Citation hyperlinks stream inline as the answer is written β no format-flip when the final answer lands.
- Sessions can be archived, reactivated, deleted, or exported as Markdown.
Access it from the Chat sidebar link or /chat/. See features.md#chat-mode.
π Security β credential-leak lockdown
A coordinated effort to stop API keys, Authorization headers, and passwords from leaking into logs:
-
redact_secrets()coverage extended to every credential-bearing exception-logging call site across LLM providers, embedding providers, and all search engines. API keys embedded in upstream SDK error messages no longer leak into logs (#4426, NASA ADS follow-up in #4432). -
Loguru
diagnosemode locked down. Previously, enablingLDR_APP_DEBUGfor general debug output also turned onrepr()dumps of every traceback frame's local variables (including credentials). A newLDR_LOGURU_DIAGNOSEenv var gates that behavior separately β off by default even in debug mode. Companion fixes pindiagnose=Falseon the MCP subprocess, benchmark CLI, and example scripts.β οΈ Action for operators: If you relied on
LDR_APP_DEBUGto get local-variable dumps in tracebacks, you now need to also setLDR_LOGURU_DIAGNOSE=true.LDR_APP_DEBUGstill controls log level as before. -
Chat
PATCH/DELETEsession routes now carry per-user rate limits matching the other state-changing endpoints.
π Bug fixes
Chat Mode stability β a large batch of correctness, data-integrity, and accessibility fixes landed for Chat Mode, including: `
π Security
- Extended
redact_secrets()coverage from the Google provider (#4070) to every credential-bearing exception-logging call site: the OpenAI-compatible provider base (list_models), the custom OpenAI endpoint provider, the OpenAI embedding provider, and the web search-engine subsystem (BaseSearchEngine.run()plus the main HTTP-call error paths in Tavily, Exa, Serper, Google PSE, Mojeek, PubMed, Semantic Scholar, Guardian, ScaleSerp, SerpAPI, and GitHub). API keys embedded in upstream SDK error messages or echoed-back URLs/headers no longer leak into logs. Guardian and ScaleSerp chain explicit-value redaction with their existing regex sanitizers for defense in depth. Consolidates #4168, #4175, and #4181. See #4131. (#4426) - Extended
redact_secrets()coverage to the NASA ADS search engine, which was missed by the original #4131 sweep. Its_get_previewscatch-all usedlogger.exception, whose traceback frames holdself.headers(theAuthorization: Bearer <key>value) and would render the API key under loguru diagnose mode. The except block now uses a redactedlogger.warning. Follow-up to #4131. - Loguru
diagnose=Trueexception rendering (which dumps therepr()of every local variable in every traceback frame) is now gated behind a separate explicitLDR_LOGURU_DIAGNOSEopt-in instead of riding onLDR_APP_DEBUG. Previously, enablingLDR_APP_DEBUGfor general debug output also turned on localvar dumps, so any exception could write frame-local credentials β API keys, the SQLCipher master password,Authorizationheaders β into every log sink.LDR_APP_DEBUGnow controls log level only;diagnosestays off unless the operator additionally setsLDR_LOGURU_DIAGNOSE=true, and an explicit warning is emitted when they do. - The MCP server subprocess (
ldr-mcp) now pinsdiagnose=Falseon its stderr sink. loguru defaultsdiagnose=True, which rendersrepr()of every traceback frame's locals on exception, so the manylogger.exception(...)paths in the MCP request handlers were writing frame-local credentials (api_key,Authorizationheaders, search-engine secrets) into the MCP client's stderr log on any failure. Companion to theLDR_LOGURU_DIAGNOSElockdown for the web/CLI logger; the MCP subprocess has no debug mode, so the gate is unconditionally off. - The benchmark CLI (
ldr-benchmark/benchmarks/cli/benchmark_commands.py) now pinsdiagnose=Falseon both of itslogger.add(sys.stderr, ...)calls. loguru defaultsdiagnose=True, which rendersrepr()of every traceback frame's locals on exception, so the manylogger.exception(...)paths in the benchmarks package (LLM grader calls, search-engine runners, dataset loaders) were writing frame-local credentials β LLMapi_key,Authorizationheaders, search-engine secrets β to stderr on any failure. Companion to theLDR_LOGURU_DIAGNOSElockdown for the web/CLI logger (#4384) and the MCP subprocess (#4394); the benchmark CLI also bypassesconfig_logger, so the env-var gate did not reach it. - The chat
PATCH(rename/archive) andDELETEsession routes now carry the same per-user rate limit as the other state-changing chat endpoints. They were previously bounded only by the global limiter, leaving an uneven abuse surface across the session API. - The example scripts under
examples/(example_browsecomp.pyand the threeapi_usage/programmatic/snippets) now passdiagnose=Falseto theirlogger.add(sys.stderr, ...)calls. Loguru defaultsdiagnose=True, which rendersrepr()of every local in every traceback frame on exception. These files are templates that users copy into their own scripts, so the previous default propagated the same credential-in-traceback leak fixed for the application logger and MCP subprocess (#4185, #4384) into anyone's downstream code.
β¨ New Features
-
Chat Mode β Interactive multi-turn research conversations. Each session accumulates context across turns (entities, topics, source count), streams research steps and citations live as the answer is built, and persists in your per-user database (encrypted by default). Sessions can be archived, reactivated, deleted, or exported as Markdown. Reach it from the Chat sidebar link or
/chat/. See features.md#chat-mode for the full feature description. (#2953) -
"Summarizingβ¦" indicator for Chat follow-ups β When you send a follow-up question, the thinking indicator now reads "Summarizing previous conversationβ¦" while the earlier turns are condensed into context for the new research, instead of showing blank dots. It clears the moment research starts streaming its progress.
-
Configurable follow-up context in Chat Mode β Follow-up questions now build on the earlier conversation instead of starting cold. By default, the prior conversation is condensed into a short summary focused on your new question (using your configured LLM) and passed to the follow-up research as its "previous findings", keeping multi-turn research on-topic and within context limits. You can change this with the new Follow-up Context Mode setting (
chat.followup_context_mode):summary(default) β an LLM summary of the conversation, focused on your new questionrawβ the most recent research findings, truncatedfullβ the entire conversation transcriptnoneβ no prior context (just your new question and the original topic)
Only
summarymakes an extra LLM call per follow-up; the other modes add no model cost. -
Chat Mode: the thinking bubble now shows what the agent is currently reasoning about between tool calls β the LLM's intermediate prose (e.g. "I should compare the published benchmark results nextβ¦") appears above the bouncing dots and overwrites with each new step, instead of leaving a static indicator for the entire research duration.
-
Chat mode now shows an elapsed-seconds counter ("Stopping research⦠(Ns)") in the progress task line after the user clicks Stop, so it's clear the click registered even when the worker thread is blocked inside an LLM HTTP call (thinking-mode models like Qwen 3 and DeepSeek-R1 can take 30+ seconds to surface a stopping checkpoint because no chunks yield during
<think>blocks). The counter only appears after 3 seconds elapsed β quick stops show the plain "Stopping researchβ¦" label without a number. The counter is cleared automatically whenever research stops, completes, or errors out. This is a UX-only mitigation; reducing the underlying termination latency would require deeper work such as per-token streaming or signal-based HTTP interruption. -
Chat mode now streams inline citation hyperlinks as the answer is written rather than only after the final save. Each chunk sent to the client is run through the same citation formatter the final answer uses, with a small carry buffer that holds incomplete
[Ntokens straddling chunk boundaries until the closing]arrives β so the user sees[[arxiv.org-1]](url)(or whichever format theirreport.citation_formatsetting selects) appearing live, and there is no visible format-flip when the saved answer replaces the streaming bubble on completion. The server-side partial-content buffer still stores the raw chunk so terminate-mid-stream saves aren't double-formatted on resume. -
Chat-mode live milestones now use friendlier wording for the agent's tool calls and observations.
Tool: search_pubmed β "covid"becomesπ Searching PubMed: "covid",Tool: fetch_url β "https://β¦"becomesπ Reading the page: "https://β¦", and tool results show asπ From {engine}: β¦so the thinking-text reads like a narration of what the agent is doing rather than a dump of internal tool names. Engines without an explicit display name fall back to a cleanedTitle Caseform of the raw tool name, so newly added search engines work without a code change.
π Bug Fixes
-
Added a
weakref.finalize-based safety net inside the Ollama
embeddings factory so that programmatic API callers and example
scripts that constructOllamaEmbeddingsdirectly β bypassing
the managed RAG service lifecycle β still release their underlying
httpx clients when the instance is garbage-collected. -
Chat Mode correctness, settings, and accessibility fixes. (1) The streaming citation path in
base_citation_handler._invoke_with_streamingnow routes its joined chunks throughget_llm_response_textbefore returning, so a reasoning model's<think>β¦</think>block no longer leaks into the persisted chat answer β.stream()bypassesProcessingLLMWrapper.invoke(the only place tags were stripped), and the streamed result now matches the non-streaminginvoke()contract. (2)SettingsManager.set_setting's self-heal block only re-points a row'stypewhen the key matches a known prefix; keys outside the dispatch map (focused_iteration.*,langgraph_agent.*, which ship astype=SEARCH) are no longer silently demoted toAPPon every edit. (3)report_assembly_service.assemble_full_reportno longer wraps_build_sources_markdownin a blanketexceptthat dropped the entire Sources block on error β failures propagate to the caller's existing 500 path instead of rendering a report that looks complete but is silently missing all sources. (4) The chatPATCH /sessionsroute now checks the boolean returns ofupdate_session_title/reactivate_session/archive_sessionand returns 500 when a DB write failed but the session still exists, instead of reportingsuccess: truewith the stale row. (5)ChatService.delete_sessionsets the in-memory termination flags only after the delete commits, so a failed commit can no longer kill the in-flight research of a session that still exists. (6)build_research_contextnow populatesoriginal_query(the session's first user message) so the contextual follow-up strategy keeps its topic anchor instead of reading an empty string. (7) The mid-stream termination paths setstreaming_state["_termination_handled"]so theResearchTerminatedExceptionhandler no longer callshandle_terminationa second time β eliminating the duplicate SUSPENDED status update, duplicate final socket emit, and doubled test-mode sleep. (8) The chat page route usesrender_template_with_defaultsso?v={{ version }}asset cache-busting is no longer empty. (9) The direct-mode capacity-reject path inprocessor_v2._start_research_directlynow incrementsQueueStatus.queued_tasks(viaadd_task_metadata) for the re-queued research β previously the counter stayed 0, so_process_user_queuecould treat the queue as empty and leave the row undispatched until later user activity re-pumped the counter. (10) The streaming chat bubble no longer sets a nestedrole="status"inside therole="log"message list, which had made screen readers announce every streamed chunk twice. A regression test asserts the streaming path strips<think>from the returned synthesis. -
Chat Mode data-integrity fixes β message ordering now respects
sequence_numberfor same-millisecond rows, the "Load older" cursor uses a composite(created_at, id)key so boundary rows are no longer silently dropped, Markdown export pages through the full conversation (previously truncated to 50 messages), and the partial-save idempotency flag is set only after the database write succeeds so a transient failure no longer permanently loses the assistant response. -
Chat Mode fixes for error/stopped-state button visibility, streaming citation carry-over, and accessibility.
handleResearchErrorandhandleResearchSuspendedinchat.jsnow callshowSessionButtons()so the edit-title / export controls reappear after a Stop or a failed research β previously only the successful-completion path restored them, leaving the buttons hidden until page reload after a stop or error. The streaming completion finalizer now flushes the citation carry buffer (_flush_carryexposed viastreaming_state) before emitting theis_finalsentinel, so an LLM stream that ends mid-token like"[12"no longer silently drops the leading bracket from the client's accumulated text._PARTIAL_BRACKET_REaccepts the lenticular openerγin addition to ASCII[β some LLMs emit Chinese-style citation brackets and the citation formatter already recognises them, so the carry buffer needed to hold those back the same way. The error-pathadd_messageinresearch_service.pynow passesallow_archived=Truematching the completion and stop-and-partial paths, so a session archived mid-flight no longer silently swallows the "research failed" message.ChatService.get_in_progress_research_idnow re-raisesDB_EXCEPTIONSinstead of returningNone: a transient DB error during session load no longer looks identical to "no research running" and the route handler returns a 500 the client can surface as an error banner. Chat-css gains visible:focus-visiblerings on send / stop / edit-title /#chat-page .ldr-btn, a real focus ring on#chat-input(replacing the invisible 15%-opacity wrapper shadow),prefers-reduced-motiongates on the streaming caret pulse and thinking-dot bounce animations,:focus-withinreveal for the per-message timestamp meta (previously hover-only and therefore invisible to keyboard users), a 44Γ44 minimum touch target for the mobile send button (WCAG 2.5.5), and the responsive breakpoint aligned to767pxso the chat container's top-bar height assumption no longer disagrees with the rest of the project at exactly 768px. Minor cleanup:timedeltais now imported at module scope inchat/routes.pyinstead of insidesend_message,ChatContextManageris no longer re-exported fromchat/__init__.py(every caller imports fromchat.contextdirectly), and the docs/features.md chat-mode section now mentions Markdown export. -
Chat Mode input-validation and resource-bound hardening.
send_messagenow parses the numeric search settings (search.iterations,search.questions_per_iteration) before the atomic write that commits the user message + IN_PROGRESS research row β a malformed (non-numeric) settings value now returns a clean 400 instead of raising an unhandled 500 after the rows are committed, which previously orphaned them and left the session unusable via the per-session 409 guard. The inline-citation carry buffer inresearch_service._make_chat_stream_callbackis now capped at 64 bytes: a never-closing[followed by an endless digit run from a misbehaving LLM is flushed raw past the cap instead of growing the buffer without bound. The client-sidestreamedContentaccumulator inchat.jsis likewise capped at 256 KB (mirroring the server's_MAX_PARTIAL_BUFFER_BYTES) with a one-time "(Response truncated β exceeded display limit.)" notice, so a model with nomax_tokenscan't OOM the browser tab. Two secondary citation regexes that the earlier lenticular-bracket work had missed now also acceptγNγ: the source-list parser incitation_formatter.py(RIS export) and the citation-strip inbenchmarks/graders.py. Regression tests were added for the carry-buffer flush + overflow contract and the LLM-title newline strip. -
Chat Mode reliability fixes. The log panel now loads historical logs when expanded on a chat page (the toggle previously held a stale null research id). Delete / clear-history failures now surface an error notification instead of silently doing nothing. When a completed research's answer can't be loaded into the chat, the message now links to the full report (which is still saved) rather than a dead-end "no report available". The header "New Chat" / "Export" buttons are now styled (their base CSS wasn't loaded on the chat page). Stopping a research while it is still starting up now reliably terminates it instead of being silently ignored, and a late error event can no longer overwrite an already-rendered completed answer.
-
Chat Mode rendering and log-safety fixes.
chat.css's.ldr-chat-containerheight now usescalc(100dvh - β¦)(with100vhretained as the previous-line fallback, matching the pattern already used inmobile-navigation.css) so iOS Safari no longer buries the chat input below the collapsible URL bar.chat.js'shandleResearchCompleteelse-branch (the one that fires when streaming didn't reach itsis_finalsentinel β most commonly after a flush-then-disconnect race introduced by the_flush_carrychange) now reuses the in-place-swap pattern from the streaming-complete branch when a partial bubble is already on screen: it removes the streaming class and renders the formatted message into the same.ldr-chat-message-textelement instead of.remove()+addMessageToUI. Eliminates the 5-8.5 s vanish-then-refetch flicker. The original detach-and-reinsert path is preserved for the no-partial-bubble case where no flicker would occur anyway.ChatService.regenerate_title_with_llmnow strips\nand\rfrom the LLM-returned title before storage; the title is interpolated into loguru f-strings (the "title already set" log line one above this fix), so an embedded newline would otherwise forge what looked like a second log entry in aggregators. Also keepsdocument.titleandchatTitle.textContentvisually clean. -
Chat Mode report-assembly, layout, and accessibility fixes.
format_links_to_markdown(utilities/search_utilities.py) now coerces citation indices tostrbeforesorted()so mixedint/strvalues arriving from different strategies (e.g.recursive_decomposition_strategy.pyemitsint,_build_sources_markdown's fallback emitsstr) no longer crash report assembly withTypeError: '<' not supported between instances of 'str' and 'int'; the sort still produces numeric order.chat.cssdesktop and mobile chat-container heights now subtract the real.ldr-top-barheights (60px / 50px) instead of the previous over-reserved 80px / 60px, ending the viewport overflow that clipped the chat input area.research_service.pyadd_progress_stepfailure path now zeroesevent_datainside itsexceptblock so a DB-lock that swallows the persistence call also suppresses the live socket emit β preserving the documented live/reload symmetry invariant (a chat step the client sees live is the same set the persisted log returns on reload). Two icon-only delete buttons inhistory.jsgainedaria-label/titlefor WCAG 2.1 Level A compliance. -
Chat Mode: when research completes but the streaming-bubble swap path doesn't end up with a visible assistant response (transient socket drop, race during session switch, missed
is_finalchunk, etc.), the chat now silently re-renders from the DB-authoritative/messagesendpoint instead of leaving the user staring at an empty page until they manually refresh. -
Citation formatter:
apply_inline_hyperlinks(the fallback path that chat-mode answers always hit because the langgraph-agent synthesis doesn't emit a## Sourcesblock in its prose) now dispatches onCitationFormatter.modeinstead of hard-codingNUMBER_HYPERLINKS. The user'sreport.citation_formatsetting is now honored for chat-mode citations β picking "Domain ID Hyperlinks" or "Source-Tagged Hyperlinks" actually produces[[arxiv.org-1]](url)/[[arxiv-1]](url)instead of every chat answer coming out as[[1]](url). -
Citation formatter:
apply_inline_hyperlinksnow accepts either"url"or"link"as the destination key on each source dict. Searxng-sourced results carry the destination under"link"(search_engine_searxng.py), which the fallback hyperlink path silently dropped β so chat-mode answers that did not emit a## Sourcesblock in their synthesis shipped with plain[N]brackets instead of clickable citations even though the Sources section beneath the answer was fully populated. -
Fix report structure parsing crashing with
IndexErroron a numbered section line without a period (e.g.1 Introduction), and stop truncating section names that contain periods (1. U.S. Policyis now kept whole instead of becomingU). The section number is now split on the first period only, with a guard for malformed lines. -
Fixes for bugs that affect the non-chat research path as well as chat. (1)
LangGraphAgentStrategy.analyze_topicno longer overwrites itsqueryparameter with a truncated tool-call argument inside the tool-call display loop β becauselanggraph-agentis the default strategy, after the firstweb_searchthe original research question was silently replaced by a β€80-char search arg, which then fed the citation re-synthesis, the fallback synthesis, and the recordedquestionfield, steering the final answer at the wrong question. (2) The queue dispatcher (processor_v2._start_queued_researches) now has a dedicatedexcept SystemAtCapacityErrorclause: a transient at-capacity rejection is left QUEUED for the next tick instead of being counted towardSPAWN_RETRY_LIMITand wrongly marked FAILED after a few ticks under load. (3)cleanup_research_resourcesnow reports the real terminal status (passed in by the caller β SUSPENDED on user termination, FAILED on error) instead of a hard-codedCOMPLETED, so stopping a research no longer emits a spurious "completed" socket signal (which produced a stray "[Stopped]" bubble in chat and a misleading 100%/Completed flip on the standard progress page); chat.js's completion/suspension handlers also cross-guard each other. (4) Chat-triggered research now inserts aUserActiveResearchrow, so it counts toward the per-userapp.max_concurrent_researchescap the same way UI-launched research does β previously chat research was invisible to the cap (queried but never recorded), letting multiple chat tabs bypass it. The row is committed atomically with the research row, cleaned up on spawn failure, and removed on normal completion by the existing completion-sweep middleware.Regression tests added for all four.
-
GitHub search relevance ranking now rejects negative, out-of-range, and non-integer result indices and deduplicates repeated ones, so a malformed LLM response can no longer select the wrong result, list the same repository twice, or discard all results.
-
Library RAG service is now closed at the end of every HTTP request
that uses it β including streaming endpoints. Together with the
embedding-manager close path, this stops file-descriptor accumulation
under sustained library indexing/search traffic. -
News subscriptions run via "Run now" or the overdue-subscription check no longer force the LLM to
ollama/llama3when the subscription has no explicit model configured. The run paths innews/flask_api.pyhardcoded those values, which (being truthy) overrode the user's configuredllm.provider/llm.modelβ so a user on OpenAI/Anthropic whose subscription carried no model would silently get an Ollama run, typically failing. The hardcoded defaults are removed; an unset model now passes through asNonesostart_researchfalls back to the user's settings. -
Stopped a per-RAG-request file-descriptor leak introduced when the
embeddings provider migrated tolangchain_ollama.OllamaEmbeddings.
The library RAG service now closes the underlying httpx clients on
teardown, preventing eventpoll FD accumulation under sustained
indexing/search load. -
Strip
<think>reasoning blocks from the main synthesis path (synthesize_findingsand the standard knowledge generator), not just the citation handlers β reasoning-model output no longer leaks<think>β¦</think>into final answers. Also fixes precision/forced answer extractors emitting a stray". <content>"when the model returns an empty (or think-only) response. -
The WebSocket subscribe ownership check now recognizes benchmark runs in addition to normal research. The new per-user ownership gate (and the removal of the cross-user broadcast fallback) only matched
ResearchHistoryUUIDs, so the benchmark page β which subscribes with an integerBenchmarkRunid β was rejected and its live progress (current-task detail and the SearXNG rate-limit warning) was silently dropped._user_owns_researchnow also accepts the user's ownBenchmarkRunrows from their per-user encrypted database, restoring benchmark live progress without widening the authorization boundary. -
The central LLM wrapper now normalizes string-returning providers into a message object and applies
<think>-tag stripping to async (ainvoke) calls too, so any LLM obtained fromget_llmyields a consistent, think-free.contentβ eliminating'str' object has no attribute 'content'crashes at the source. Message objects keep theirtool_calls/reasoning_content(only.contentis rewritten). -
The chat
send_messageendpoint now uses the shared@require_json_bodydecorator like the other state-changing chat POSTs. It was the only one validating the body inline, so a non-JSON content type slipped past the consistent 400 contract; requiring anapplication/jsonbody also hardens CSRF on the heaviest chat endpoint (it launches a research run). Behavior for valid requests is unchanged. -
The queued-research dispatch loop now reverts its queue-counter claim when a global-capacity reject re-queues a research. Previously,
_start_queued_researchesmarked the taskprocessing(decrementingqueued_tasks, incrementingactive_tasks) and, onSystemAtCapacityError, only reset the row'sis_processingflag β leaking a slot intoactive_taskson every capacity-rejected retry. Under sustained capacity pressurequeued_taskswould drift to 0, at which point the per-user queue processor treated the queue as empty and stopped dispatching the still-present rows.update_task_statusnow supports aprocessingβqueuedtransition that restores the counts, and the capacity-reject path uses it. -
GET /api/report/<id>now returns thesourcesfield from the structuredresearch_resourcestable instead of the deadall_links_of_systemmetadata key. After the chat-mode-v2 report refactor that key is never written, so the field returned an empty list for every research created since β even though the assembledcontentand the news feed already read sources from the table. (#3665)
π Other Changes
research_history.report_contentshape β answer-only at persistence time. Internal/library change tied to the Chat Mode work:report_contentnow stores the synthesized answer (LLM prose + inline citations) rather than the fullformat_findingsblob (which previously embedded## Sourcesand## Research Metricssections). User-facing views are unchanged βassemble_full_report()inweb/services/report_assembly_service.pyreconstructs the legacy display shape on demand for the history page, exports, and the chat "View full research" link, and legacy rows that still contain the inline sections are detected and not double-appended. Direct callers ofstorage.get_report()/storage.get_report_with_metadata()should switch toassemble_full_report()if they need the legacy shape.- CI: fix the freshly-merged
check-shadow-testspre-commit hook usingos.path.basename(), which the repo's owncheck-pathlib-usagehook forbids. Because PR pre-commit runs--all-filesagainst the PR-merged-into-main tree, this one line on main turned every open PR's pre-commit red. Switched topathlib.Path(path).name(behaviour verified identical toos.path.basenameacross separator/edge cases). - CI: split
tests/web/routes/test_settings_routes_coverage.pyout of the parallel pytest run into a dedicated serial step (same pattern as the existingfd_canarystep). The 94 tests in that file all use theauthenticated_clientfixture (register + login + SQLCipher KDF + 10 Alembic migrations per test), and under-n autothey accumulate enough connection / FD pressure inside the assigned xdist worker to deadlock it silently late in the run β the worker dies with[gw0] node down: Not properly terminated(no timeout printed), its coverage data is dropped, and the 50% fail-under gate then fails the job even though every test that completed actually passed. Skipping individual tests in this file just relocates the death (confirmed empirically on this PR's earlier iteration). The serial step keeps all 94 tests running, appends into the main run's.coveragedata via--cov-append, and runs the final coverage report + 50% gate after both contributions have merged. Defence-in-depth: pairs with #4393's 180s/thread timeout. - CI: switch
pytest-timeoutfromsignaltothreadmethod and raise the global per-test timeout from 60s to 180s. On Python 3.14 the SIGALRM-based interrupt was firing insideweakref.pycleanup, corrupting xdist workers and dropping their coverage data β which then put total coverage below the 50% gate even when every test that completed actually passed. The thread method raises a Python-level exception in the main thread instead of interrupting at an arbitrary safe-point, and 180s gives the heavyauthenticated_clientregister+login fixture (SQLCipher KDF + Alembic migrations) headroom under-n auto+ coverage contention. - Chat Mode polish and test-quality improvements.
chat/routes.pyadopts the shared@require_json_body(error_format="success")decorator on the three POST/PATCH endpoints (create_session,generate_session_title,update_session), replacing 4-block inlineisinstance(data, dict)guards.UserActiveResearchstale-row reclaim is extracted from bothchat.routes.send_messageandresearch_routes.start_researchinto a sharedreclaim_stale_user_active_research(db, username, *, grace_cutoff_dt=None, logger=None)helper inweb/routes/globals.pyβ chat passes a 30s grace cutoff (because chat send can race with its own concurrent sibling), research_routes passes None (matching its pre-existing behaviour).ChatService.delete_sessionnow importsset_termination_flagat module top-level instead of inside the function body (no circular import requires the deferred import). The dummy"I understand. What else would you like to know?"assistant bubble thatchat.jsinjected when the server returned noresearch_idis gone β the thinking indicator is now cleared without injecting a placeholder reply.chat.js::loadSessionnow focuses the input on completion, matchingstartNewChat. Three brittle test patterns are replaced: the seven copy-pastedcaptured_username+@contextmanagerblocks intest_chat_user_isolation.pyare consolidated under a singleusername_capturing_dbfixture; the 16 inlineSocketIOService._instance = Nonereset lines intest_chat_socket_events.pybecome a single autouse fixture that runs in setup AND teardown regardless of test outcome;test_chat_settings_integration.py's three tautological dict-assertion tests are replaced with four real tests that exercise_load_settingsagainst patchedSettingsManagerand validate every snapshot-extraction key still exists indefault_settings.json.test_chat_send_message_reclaim.py's three source-grep tests againstchat/routes.pyare replaced with eight behaviour tests that drive the new shared reclaim helper against a real SQLite session (covering grace-window respect, live-thread skip, username scoping, and the without-cutoff immediate-reclaim mode used by research_routes).test_chat_service.pydrops threeif hasattr(...)assertion guards that would have silently passed when a real model regressed; theget_in_progress_research_idDB-error test inverts to assert propagation now that the service re-raises instead of returning None.test_chat_api.py's ordering assertion no longer hides behindif len(sessions) >= 2:. - Chat Mode polish fixes. The agent-thinking milestone for
research_subtopicnow readsπ¬ Investigating subtopic: "topic1, topic2"instead of empty quotes β the display code now picks up the tool's actualsubtopics: list[str]argument and stringifies the list.send_message'sUserActiveResearchreclaim block now emits alogger.warningmirroring theResearchHistoryreclaim above it, so operators can trace why a per-user concurrency cap was released._validate_titlestrips Unicode format / line-separator characters (Cf/Zl/Zpβ zero-width spacesU+200B-U+200Dand BOMU+FEFF) before the emptiness check, so a title of 500 zero-width chars is rejected instead of saving a session that looks blank in the UI.regenerate_title_with_llmis now idempotent: it skips the LLM call if the session title no longer matches the non-LLM fallback, so a concurrent tab's edit (or the user's manual edit completing first) is not overwritten by a stale LLM round-trip. The deadbuild_research_context_for_session()wrapper inchat/context.pyis removed along with its dedicated tests β production code usesChatContextManagerdirectly. - Chat follow-up research now logs which prior-context mode ran and how many characters of context it built. Previously the context-building step (especially the LLM summary) produced no log output, so a follow-up's preparation looked like an unexplained pause.
- Clarified the auto-generated
app.debug/LDR_APP_DEBUGdescription indefault_settings.json(and thereforedocs/CONFIGURATION.md) to reflect the split introduced whenLDR_LOGURU_DIAGNOSEwas added:LDR_APP_DEBUGnow raises log level only and explicitly does NOT enable Loguru's local-variable dumps on its own. Companion to theLDR_LOGURU_DIAGNOSEsecurity fix. - Document a known limitation: LangGraph
create_agent/bind_tools(in thelanggraph-agentandmcpstrategies) resolve to the base LLM viaProcessingLLMWrapper.__getattr__, bypassing the wrapper's<think>-tag stripping. Added in-code notes at the three call sites. This is a cosmetic leak only (reasoning-model<think>blocks may appear in agent output); it does not affect directinvoke()calls, which still go through the wrapper. - Documented that the News API
findingsfield is the answer-only report content after the chat-mode-v2 refactor (intentional): structured top-N source links live in the separatelinksarray rather than an embedded## Sourcesblob. No behavior change. (#3665) - Test fixtures and dev scripts that re-configure loguru (
tests/conftest.pyloguru-caplog fixture, the twotests/api_tests/__main__runners,tests/test_openai_api_key_e2e.py,tests/settings/test_manager_behavior.py,tests/journal_quality/test_db.py,tests/performance/mcp/echo_server.py) now passdiagnose=Falseto theirlogger.add(...)calls, matching the production policy locked in by #4384 / #4394. loguru defaultsdiagnose=True, which rendersrepr()of every traceback frame's local on exception; pinning it off keeps frame-local credentials (api keys, SQLCipher passwords,Authorizationheaders) out of pytest output and CI logs even when a test crashes mid-fixture. Hygiene change only β no behavior change for green tests. - The news-feed "time ago" formatter no longer swallows unparseable timestamps behind a silent "Recently" label. Since
created_atis always written as a valid ISO timestamp, a value that won't parse means a corrupt row β that row is now logged and skipped by the feed builder instead of rendering with a misleading time.
What's Changed
π Security Updates
- feat: Add Chat Mode for conversational research (rebased from #1527) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/2953
- fix: release-audit hardening for chat send_message and news time-ago by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4369
- fix(api): /api/report sources from research_resources + chat session-route rate limits (#3665) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4371
- fix(security): suppress 4 Bearer false positives in chat.js by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4374
- docs+tests: clarify app.debug after LDR_LOGURU_DIAGNOSE split (#4384 follow-up) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4398
- docs(examples): pass diagnose=False to logger.add in example scripts (#4185) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4401
- fix(security): pin diagnose=False in the benchmark CLI logger (#4185) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4403
- chore(tests): pin diagnose=False on test-fixture loguru sinks (#4185) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4404
- ci(security): run tests/security/ as a directory (fix release-gate exit-5, 9β87 files) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4411
- fix(security): pin diagnose=False on the MCP stderr sink (follow-up to #4384, #4185) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4394
β¨ New Features
- feat(chat): configurable prior-context for follow-up research by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4351
- feat(chat): show "Summarizingβ¦" indicator during follow-up context build by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4368
- feat(embeddings): weakref.finalize safety net for OllamaEmbeddings by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4408
- chore(chat): log follow-up prior-context mode + size by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4359
- chore(hooks): add check-weak-tests to detect tautological / no-op tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4372
- fix(news): subscription runs respect user LLM settings instead of forcing ollama/llama3 by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4412
- ci: guard against workflows referencing missing test paths by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4413
- feat(ci): add check-shadow-tests pre-commit hook by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4356
π Bug Fixes
- fix(queue): revert queuedβprocessing counter on capacity-reject re-queue by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4346
- fix(embeddings): migrate Ollama embeddings to langchain_ollama (drop deprecation warning) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4353
- test(close_base_llm): isolate #3816 FD-leak canaries to a serial no-coverage CI step by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4373
- test(web/routes): fix stale test_report_success after #4371 /api/report sources change by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4375
- fix(report): split section number on first period only (IndexError + name truncation) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4345
- fix(ci): pytest-timeout thread method + 180s β unblock CI on 3.14 by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4393
- fix(tests): lower SQLCipher KDF iterations in app fixture to prevent CI worker timeouts by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4381
- fix(embeddings): close OllamaEmbeddings httpx clients in manager lifecycle by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4405
- fix(rag-routes): close LibraryRAGService on every endpoint exit by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4407
- fix(search): validate and deduplicate GitHub engine ranking indices by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4338
- fix(ci): align responsive UI puppeteer pin with lockfile (25.1.0) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4417
- fix(chat): bind input listeners before async session load (fixes chat-core UI shard race) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4416
- fix(tests): settle async content before mobile-safari nav assertions by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4439
- fix(chat): post-merge audit follow-ups β API validation, DRY, logging, a11y, tests (#3891) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4427
ποΈ Database Changes
- test: add explicit head-revision assertions to 5 alembic config-smoke tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4294
π Documentation
- docs(strategies): note create_agent/bind_tools bypass of LLM wrapper think-stripping by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4355
- chore(changelog): drop stale time-ago fragment reverted within the cycle by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4376
- docs(examples): migrate Ollama embeddings imports off deprecated langchain_community by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4399
- docs(resource-cleanup): drop reference to unmerged FD smoke test by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4419
π§ CI/CD & Maintenance
- chore: clear changelog fragments for 1.6.13 by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4344
- test: delete 26 shadow test files across 8 directories (683 tests, 10154 lines) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4243
- test: delete 3 H4_ASSERTS_MOCK / H6 tests in diversity_explorer_coverage.py by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4285
- test: remove 24 more shadow files across the suite (-9,903 lines) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4239
- chore: bump minor version to 1.7.0 by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4340
- chore(deps): bump gitleaks/gitleaks-action from 2.3.9 to 3.0.0 by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4390
- ci(docker-tests): split test_settings_routes_coverage.py into a serial step by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4397
- π€ Update dependencies by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4409
- π€ Update dependencies by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4418
β¬οΈ Dependencies
- chore(deps): bump puppeteer from 25.0.4 to 25.1.0 in /tests/puppeteer by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4363
- chore(deps): bump puppeteer from 25.0.4 to 25.1.0 in /tests/ui_tests by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4360
- chore(deps-dev): bump puppeteer from 25.0.4 to 25.1.0 in /tests/api_tests_with_login by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4361
- chore(deps): bump puppeteer from 25.0.4 to 25.1.0 in /tests by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4362
- chore(deps): bump tmp from 0.2.5 to 0.2.7 in /tests/accessibility_tests in the npm_and_yarn group across 1 directory by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4365
- chore(deps): bump date-fns from 4.3.0 to 4.4.0 by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4391
- chore(deps): bump marked from 18.0.3 to 18.0.4 by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4389
- chore(deps): bump dompurify from 3.4.5 to 3.4.7 by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4388
- chore(deps-dev): bump eslint from 10.4.0 to 10.4.1 in /tests/puppeteer by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4385
- chore(deps-dev): bump vite from 8.0.13 to 8.0.16 by @dependabot[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4386
π§Ή Code Quality & Refactoring
- test: delete 7 no-assert 'should not raise' metrics tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4287
- test: dedupe 16 redundant tests across two filter test files by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4266
- test: delete 13 no-assertion downloader + rag-route-coverage tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4282
- test: remove 140 import-existence tautology tests across 52 files by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4241
- test: delete 8 no-assert / dead-code security + rate-limit tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4286
- test: delete 4 weak tests in test_token_counter_extended.py by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4278
- test: delete 2 H4_ASSERTS_MOCK tests in test_parallel_explorer.py by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4284
- test: delete 7 H4_ASSERTS_MOCK tests in test_adaptive_explorer.py by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4283
- test: delete 45 news shadow tests that test inline reproductions, not SUT by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4242
- test: delete 3 Tier-3 shadow / script-style test files by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4270
- test: delete test_least_recently_used_evicted_first - 'or True' tautology by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4279
- test: delete tests/test_google_pse.py - H7_MOCK_IDENTITY tautology by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4275
- test: delete 10 no-assertion tests across 2 search_integration files by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4281
- test: dedupe 22 redundant tests across evaluator pure_logic + high_value by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4267
- test: remove 4 shadow weighted_score tests that tested local reimplementation by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4256
- test: dedupe 7 more redundant tests across 4 files by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4268
- refactor(modular_strategy): drop duplicated existing_queries normalization by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4218
π§ͺ Tests
- test(news): make _format_time_ago TZ test independent of local timezone by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4400
- test(ui): bound Playwright navigationTimeout at 15s by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4219
Other Changes
- fix(news): fail loud on unparseable created_at instead of silent "Recently" by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4370
- fix(security): gate loguru diagnose localvar dumps behind LDR_LOGURU_DIAGNOSE (#4185) by @SuperMarioYL in https://github.com/LearningCircuit/local-deep-research/pull/4384
- test: migrate Ollama tests to langchain_ollama by @astrobrownmusic in https://github.com/LearningCircuit/local-deep-research/pull/4377
- test: replace residual
... or Trueweak assertions with real outcomes by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4406 - fix(ci): use pathlib in check-shadow-tests hook (unbreak pre-commit on main) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4422
- fix(security): extend redact_secrets() coverage across LLM/embedding/search-engine log sites (#4131) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4426
- fix(ci): pathlib in shadow-tests hook + happy-dom-robust KaTeX render test by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4425
- fix(ci): restore contents:write to ui-full-shards caller (fixes startup_failure) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4423
- fix(security): redact api_key in NASA ADS search engine error log (#4131 follow-up) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4432
New Contributors
- @astrobrownmusic made their first contribution in https://github.com/LearningCircuit/local-deep-research/pull/4377
Full Changelog: https://github.com/LearningCircuit/local-deep-research/compare/v1.6.13...v1.7.0
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Local Deep Research
AI-powered deep research tool with multi-source search (arXiv, PubMed, web)
Related context
Related tools
Earlier breaking changes
- v1.6.11 JavaScript rendering disabled by default in production Docker image; new web.enable_javascript_rendering setting (default false).
Beta — feedback welcome: [email protected]