35 KiB
🔒 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 to
langchain_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.