Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

35 KiB
Raw Permalink Blame History

🔒 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_previews catch-all used logger.exception, whose traceback frames hold self.headers (the Authorization: Bearer <key> value) and would render the API key under loguru diagnose mode. The except block now uses a redacted logger.warning. Follow-up to #4131.
  • Loguru diagnose=True exception rendering (which dumps the repr() of every local variable in every traceback frame) is now gated behind a separate explicit LDR_LOGURU_DIAGNOSE opt-in instead of riding on LDR_APP_DEBUG. Previously, enabling LDR_APP_DEBUG for general debug output also turned on localvar dumps, so any exception could write frame-local credentials — API keys, the SQLCipher master password, Authorization headers — into every log sink. LDR_APP_DEBUG now controls log level only; diagnose stays off unless the operator additionally sets LDR_LOGURU_DIAGNOSE=true, and an explicit warning is emitted when they do.
  • The MCP server subprocess (ldr-mcp) now pins diagnose=False on its stderr sink. loguru defaults diagnose=True, which renders repr() of every traceback frame's locals on exception, so the many logger.exception(...) paths in the MCP request handlers were writing frame-local credentials (api_key, Authorization headers, search-engine secrets) into the MCP client's stderr log on any failure. Companion to the LDR_LOGURU_DIAGNOSE lockdown 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 pins diagnose=False on both of its logger.add(sys.stderr, ...) calls. loguru defaults diagnose=True, which renders repr() of every traceback frame's locals on exception, so the many logger.exception(...) paths in the benchmarks package (LLM grader calls, search-engine runners, dataset loaders) were writing frame-local credentials — LLM api_key, Authorization headers, search-engine secrets — to stderr on any failure. Companion to the LDR_LOGURU_DIAGNOSE lockdown for the web/CLI logger (#4384) and the MCP subprocess (#4394); the benchmark CLI also bypasses config_logger, so the env-var gate did not reach it.
  • The chat PATCH (rename/archive) and DELETE session 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.py and the three api_usage/programmatic/ snippets) now pass diagnose=False to their logger.add(sys.stderr, ...) calls. Loguru defaults diagnose=True, which renders repr() 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 question
    • raw — the most recent research findings, truncated
    • full — the entire conversation transcript
    • none — no prior context (just your new question and the original topic)

    Only summary makes 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 [N tokens straddling chunk boundaries until the closing ] arrives — so the user sees [[arxiv.org-1]](url) (or whichever format their report.citation_format setting 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 cleaned Title Case form 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 construct OllamaEmbeddings directly — 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_streaming now routes its joined chunks through get_llm_response_text before returning, so a reasoning model's <think>…</think> block no longer leaks into the persisted chat answer — .stream() bypasses ProcessingLLMWrapper.invoke (the only place tags were stripped), and the streamed result now matches the non-streaming invoke() contract. (2) SettingsManager.set_setting's self-heal block only re-points a row's type when the key matches a known prefix; keys outside the dispatch map (focused_iteration.*, langgraph_agent.*, which ship as type=SEARCH) are no longer silently demoted to APP on every edit. (3) report_assembly_service.assemble_full_report no longer wraps _build_sources_markdown in a blanket except that 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 chat PATCH /sessions route now checks the boolean returns of update_session_title / reactivate_session / archive_session and returns 500 when a DB write failed but the session still exists, instead of reporting success: true with the stale row. (5) ChatService.delete_session sets 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_context now populates original_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 set streaming_state["_termination_handled"] so the ResearchTerminatedException handler no longer calls handle_termination a second time — eliminating the duplicate SUSPENDED status update, duplicate final socket emit, and doubled test-mode sleep. (8) The chat page route uses render_template_with_defaults so ?v={{ version }} asset cache-busting is no longer empty. (9) The direct-mode capacity-reject path in processor_v2._start_research_directly now increments QueueStatus.queued_tasks (via add_task_metadata) for the re-queued research — previously the counter stayed 0, so _process_user_queue could 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 nested role="status" inside the role="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_number for 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. handleResearchError and handleResearchSuspended in chat.js now call showSessionButtons() 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_carry exposed via streaming_state) before emitting the is_final sentinel, 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_RE accepts 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-path add_message in research_service.py now passes allow_archived=True matching 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_id now re-raises DB_EXCEPTIONS instead of returning None: 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-visible rings 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-motion gates on the streaming caret pulse and thinking-dot bounce animations, :focus-within reveal 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 to 767px so the chat container's top-bar height assumption no longer disagrees with the rest of the project at exactly 768px. Minor cleanup: timedelta is now imported at module scope in chat/routes.py instead of inside send_message, ChatContextManager is no longer re-exported from chat/__init__.py (every caller imports from chat.context directly), and the docs/features.md chat-mode section now mentions Markdown export.

  • Chat Mode input-validation and resource-bound hardening. send_message now 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 in research_service._make_chat_stream_callback is 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-side streamedContent accumulator in chat.js is 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 no max_tokens can'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 in citation_formatter.py (RIS export) and the citation-strip in benchmarks/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-container height now uses calc(100dvh - …) (with 100vh retained as the previous-line fallback, matching the pattern already used in mobile-navigation.css) so iOS Safari no longer buries the chat input below the collapsible URL bar. chat.js's handleResearchComplete else-branch (the one that fires when streaming didn't reach its is_final sentinel — most commonly after a flush-then-disconnect race introduced by the _flush_carry change) 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-text element 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_llm now strips \n and \r from 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 keeps document.title and chatTitle.textContent visually clean.

  • Chat Mode report-assembly, layout, and accessibility fixes. format_links_to_markdown (utilities/search_utilities.py) now coerces citation indices to str before sorted() so mixed int/str values arriving from different strategies (e.g. recursive_decomposition_strategy.py emits int, _build_sources_markdown's fallback emits str) no longer crash report assembly with TypeError: '<' not supported between instances of 'str' and 'int'; the sort still produces numeric order. chat.css desktop and mobile chat-container heights now subtract the real .ldr-top-bar heights (60px / 50px) instead of the previous over-reserved 80px / 60px, ending the viewport overflow that clipped the chat input area. research_service.py add_progress_step failure path now zeroes event_data inside its except block 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 in history.js gained aria-label / title for 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_final chunk, etc.), the chat now silently re-renders from the DB-authoritative /messages endpoint 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 ## Sources block in its prose) now dispatches on CitationFormatter.mode instead of hard-coding NUMBER_HYPERLINKS. The user's report.citation_format setting 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_hyperlinks now 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 ## Sources block 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 IndexError on a numbered section line without a period (e.g. 1 Introduction), and stop truncating section names that contain periods (1. U.S. Policy is now kept whole instead of becoming U). 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_topic no longer overwrites its query parameter with a truncated tool-call argument inside the tool-call display loop — because langgraph-agent is the default strategy, after the first web_search the original research question was silently replaced by a ≤80-char search arg, which then fed the citation re-synthesis, the fallback synthesis, and the recorded question field, steering the final answer at the wrong question. (2) The queue dispatcher (processor_v2._start_queued_researches) now has a dedicated except SystemAtCapacityError clause: a transient at-capacity rejection is left QUEUED for the next tick instead of being counted toward SPAWN_RETRY_LIMIT and wrongly marked FAILED after a few ticks under load. (3) cleanup_research_resources now reports the real terminal status (passed in by the caller — SUSPENDED on user termination, FAILED on error) instead of a hard-coded COMPLETED, 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 a UserActiveResearch row, so it counts toward the per-user app.max_concurrent_researches cap 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/llama3 when the subscription has no explicit model configured. The run paths in news/flask_api.py hardcoded those values, which (being truthy) overrode the user's configured llm.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 as None so start_research falls 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_findings and 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 ResearchHistory UUIDs, so the benchmark page — which subscribes with an integer BenchmarkRun id — was rejected and its live progress (current-task detail and the SearXNG rate-limit warning) was silently dropped. _user_owns_research now also accepts the user's own BenchmarkRun rows 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 from get_llm yields a consistent, think-free .content — eliminating 'str' object has no attribute 'content' crashes at the source. Message objects keep their tool_calls/reasoning_content (only .content is rewritten).

  • The chat send_message endpoint now uses the shared @require_json_body decorator 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 an application/json body 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_researches marked the task processing (decrementing queued_tasks, incrementing active_tasks) and, on SystemAtCapacityError, only reset the row's is_processing flag — leaking a slot into active_tasks on every capacity-rejected retry. Under sustained capacity pressure queued_tasks would 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_status now supports a processingqueued transition that restores the counts, and the capacity-reject path uses it.

  • GET /api/report/<id> now returns the sources field from the structured research_resources table instead of the dead all_links_of_system metadata 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 assembled content and the news feed already read sources from the table. (#3665)

📝 Other Changes

  • research_history.report_content shape — answer-only at persistence time. Internal/library change tied to the Chat Mode work: report_content now stores the synthesized answer (LLM prose + inline citations) rather than the full format_findings blob (which previously embedded ## Sources and ## Research Metrics sections). User-facing views are unchanged — assemble_full_report() in web/services/report_assembly_service.py reconstructs 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 of storage.get_report() / storage.get_report_with_metadata() should switch to assemble_full_report() if they need the legacy shape.
  • CI: fix the freshly-merged check-shadow-tests pre-commit hook using os.path.basename(), which the repo's own check-pathlib-usage hook forbids. Because PR pre-commit runs --all-files against the PR-merged-into-main tree, this one line on main turned every open PR's pre-commit red. Switched to pathlib.Path(path).name (behaviour verified identical to os.path.basename across separator/edge cases).
  • CI: split tests/web/routes/test_settings_routes_coverage.py out of the parallel pytest run into a dedicated serial step (same pattern as the existing fd_canary step). The 94 tests in that file all use the authenticated_client fixture (register + login + SQLCipher KDF + 10 Alembic migrations per test), and under -n auto they 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 .coverage data 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-timeout from signal to thread method and raise the global per-test timeout from 60s to 180s. On Python 3.14 the SIGALRM-based interrupt was firing inside weakref.py cleanup, 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 heavy authenticated_client register+login fixture (SQLCipher KDF + Alembic migrations) headroom under -n auto + coverage contention.
  • Chat Mode polish and test-quality improvements. chat/routes.py adopts 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 inline isinstance(data, dict) guards. UserActiveResearch stale-row reclaim is extracted from both chat.routes.send_message and research_routes.start_research into a shared reclaim_stale_user_active_research(db, username, *, grace_cutoff_dt=None, logger=None) helper in web/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_session now imports set_termination_flag at 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 that chat.js injected when the server returned no research_id is gone — the thinking indicator is now cleared without injecting a placeholder reply. chat.js::loadSession now focuses the input on completion, matching startNewChat. Three brittle test patterns are replaced: the seven copy-pasted captured_username + @contextmanager blocks in test_chat_user_isolation.py are consolidated under a single username_capturing_db fixture; the 16 inline SocketIOService._instance = None reset lines in test_chat_socket_events.py become 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_settings against patched SettingsManager and validate every snapshot-extraction key still exists in default_settings.json. test_chat_send_message_reclaim.py's three source-grep tests against chat/routes.py are 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.py drops three if hasattr(...) assertion guards that would have silently passed when a real model regressed; the get_in_progress_research_id DB-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 behind if len(sessions) >= 2:.
  • Chat Mode polish fixes. The agent-thinking milestone for research_subtopic now reads 🔬 Investigating subtopic: "topic1, topic2" instead of empty quotes — the display code now picks up the tool's actual subtopics: list[str] argument and stringifies the list. send_message's UserActiveResearch reclaim block now emits a logger.warning mirroring the ResearchHistory reclaim above it, so operators can trace why a per-user concurrency cap was released. _validate_title strips Unicode format / line-separator characters (Cf/Zl/Zp — zero-width spaces U+200B-U+200D and BOM U+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_llm is 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 dead build_research_context_for_session() wrapper in chat/context.py is removed along with its dedicated tests — production code uses ChatContextManager directly.
  • 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_DEBUG description in default_settings.json (and therefore docs/CONFIGURATION.md) to reflect the split introduced when LDR_LOGURU_DIAGNOSE was added: LDR_APP_DEBUG now raises log level only and explicitly does NOT enable Loguru's local-variable dumps on its own. Companion to the LDR_LOGURU_DIAGNOSE security fix.
  • Document a known limitation: LangGraph create_agent/bind_tools (in the langgraph-agent and mcp strategies) resolve to the base LLM via ProcessingLLMWrapper.__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 direct invoke() calls, which still go through the wrapper.
  • Documented that the News API findings field is the answer-only report content after the chat-mode-v2 refactor (intentional): structured top-N source links live in the separate links array rather than an embedded ## Sources blob. No behavior change. (#3665)
  • Test fixtures and dev scripts that re-configure loguru (tests/conftest.py loguru-caplog fixture, the two tests/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 pass diagnose=False to their logger.add(...) calls, matching the production policy locked in by #4384 / #4394. loguru defaults diagnose=True, which renders repr() of every traceback frame's local on exception; pinning it off keeps frame-local credentials (api keys, SQLCipher passwords, Authorization headers) 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_at is 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.