Skip to content

Refactor/backend architecture#17

Merged
LinMoQC merged 17 commits intomainfrom
refactor/backend-architecture
Mar 19, 2026
Merged

Refactor/backend architecture#17
LinMoQC merged 17 commits intomainfrom
refactor/backend-architecture

Conversation

@LinMoQC
Copy link
Copy Markdown
Owner

@LinMoQC LinMoQC commented Mar 18, 2026

Summary

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactor / code cleanup
  • CI / tooling

Related issue

Closes #

Changes

How to test

Screenshots (if applicable)

Checklist

  • My code follows the project's coding conventions
  • I have run ./lyra lint and there are no type errors
  • I have added/updated tests for the changed functionality
  • I have updated the documentation if behavior changed
  • The PR title follows Conventional Commits format (feat:, fix:, etc.)
  • I have read the CONTRIBUTING.md

Summary by CodeRabbit

  • New Features

    • Deep Research tasks with background execution, live event streaming, report/deliverable generation, and a floating progress indicator
    • Proactive insights, AI suggestions, and cross-notebook related-knowledge endpoints
    • Notebook icon registry, interactive choice cards, file-attachment support in chat, and thinking/reasoning display
    • Streaming text polish and writing-context lookup
  • Improvements

    • Enhanced chat toolbar, model grouping & "thinking" badges, dark-mode UI refinements, setup accepts optional email, clearer API base path in docs
  • Removals

    • Mock-data toggle removed from docs/environment info
  • Tests

    • New unit, integration, and end-to-end tests for conversations, deep research, and utilities

LinMoQC added 8 commits March 18, 2026 16:50
将 20 个平铺文件按领域分组到子目录:
- core/: Agent 引擎 (brain, engine, state, instructions, react_agent, tools)
- rag/: 检索增强 (retrieval, ingestion)
- memory/: 记忆系统 (extraction, retrieval, notebook, file_storage)
- research/: 深度研究 (deep_research, evaluation, reflection)
- writing/: 写作辅助 (composer, ghost_text, scene_detector)
- kg/: 知识图谱 (knowledge_graph)

每个子目录 __init__.py 导出公共 API,全部外部 import 已更新

Made-with: Cursor
- providers/llm.py 新增 get_client() 和 get_model() 统一入口
- ai/router.py 5 处 AsyncOpenAI(...) 替换为 get_client()
- workers/tasks.py 3 处 AsyncOpenAI(...) 替换为 get_client()
- 保留 config/setup test-llm 端点的直接使用(测试自定义凭证)

Made-with: Cursor
- 新增 services/conversation_service.py (class-based, DI db + user_id)
- conversation/router.py 从 473 行瘦身至 110 行(薄路由)
- 所有 DB 操作、Agent 编排、后台任务调度移入 Service
- 参考 LobeHub AiChatService 模式

Made-with: Cursor
- 新增 services/source_service.py,提取所有上传/导入/下载/删除业务逻辑
- source/router.py 从 ~440 行瘦身至 147 行
- 参考 LobeHub class-based service DI 模式

Made-with: Cursor
原 733 行 God File 拆分为:
- routers/suggestions.py: AI 建议、上下文问候、来源建议
- routers/research.py: 深度研究 SSE 流
- routers/writing.py: 文本润色、写作上下文
- routers/knowledge.py: 跨笔记本知识发现
- routers/insights.py: 主动洞察 CRUD

ai/router.py 仅保留 20 行聚合路由

Made-with: Cursor
将 ai/memory/config/knowledge_graph/knowledge/feedback/auth/setup/skill
9 个 domain 的 inline Pydantic schemas 提取到独立 schemas.py
router 仅保留 HTTP 逻辑,schema 定义集中管理

Made-with: Cursor
- start.sh 新增 _check_port() 函数,dev/docker 模式启动前检测 8000/3000 端口占用
- _tcp_ready 从 /dev/tcp 改为 nc -z,修复 macOS 兼容性
- venv 创建移除 pyenv 硬依赖,fallback 到 python3
- lyra init 生产模式日志修正(不再误报 api/.env 已生成)
- handleUnauthorized() 增加 cookie 清除逻辑

Made-with: Cursor
- 输入框:圆角胶囊、箭头发送按钮、浅蓝 hover 工具栏
- 工具栏:+ 号弹出菜单(添加文件、深度研究、思考模式),激活项以 pill 展示
- 深度研究 pill:默认图标、hover 显示取消 X,模式下拉改为「简要/全面」,取消按钮 hover 背景缩小
- 思考模式:仅在选择 thinking 模型时显示,文案「思考模式」/THINKING,灯泡图标,hover 同位置变 X、浅蓝背景
- 消息气泡:Mermaid 流程图渲染与放大、表格/代码块/标题层级、Gemini 风格思考过程折叠
- 后端:深度研究任务关联会话、流式错误处理、日志与 Celery 配置、mind_map enum 修正

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lyra-note Ready Ready Preview, Comment Mar 19, 2026 4:27am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

Warning

Rate limit exceeded

@LinMoQC has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbd59226-21d8-4c56-b285-eddc3db54e01

📥 Commits

Reviewing files that changed from the base of the PR and between 5424216 and 32801f2.

📒 Files selected for processing (3)
  • api/tests/e2e/test_conversation.py
  • api/tests/integration/test_conversation_service.py
  • api/tests/unit/test_deep_research_utils.py
📝 Walkthrough

Walkthrough

Introduces a deep-research background task system with streaming TaskBuffer and ResearchTask persistence; reorganizes agent packages into core/rag/research/memory/writing with many shimmed re-exports; centralizes schemas/services, adds new AI sub-routers, logging, migrations, frontend deep-research UI/store, and related tests and UX updates.

Changes

Cohort / File(s) Summary
Migrations & Models
api/alembic/versions/025_drop_clerk_id.py, api/alembic/versions/026_add_research_tasks.py, api/alembic/versions/027_research_task_conversation.py, api/alembic/versions/028_message_reasoning.py, api/app/models.py
Drop clerk_id; add research_tasks table and conversation_id FK; add reasoning column to messages; include upgrade/downgrade migrations.
Research graph & orchestration
api/app/agents/research/deep_research.py, api/app/agents/research/task_manager.py
Add deep-research LangGraph implementation, ModeConfig, ResearchState, and TaskBuffer; introduce run_research_task to stream events, build timeline/report/deliverable, persist ResearchTask, and support reconnect/replay.
AI routers & schemas
api/app/domains/ai/router.py, api/app/domains/ai/routers/*, api/app/domains/ai/schemas.py
Split AI router into sub-routers (suggestions, research, writing, knowledge, insights); add endpoints and Pydantic schemas for suggestions, deep research lifecycle, writing polish/context, knowledge discovery, and insights.
Services: conversation & source
api/app/services/conversation_service.py, api/app/services/source_service.py
New ConversationService and SourceService encapsulate conversation/message flows, streaming agent logic, source ingestion/download/rechunking, and background task orchestration; routers delegate to these services.
Agent package reorganization & shims
api/app/agents/... (many files)
Move implementations into app.agents.core, app.agents.rag, app.agents.memory, app.agents.research, app.agents.kg, app.agents.writing; replace many legacy modules with shim re-exports to preserve import paths.
Memory package & file storage
api/app/agents/memory/__init__.py, api/app/agents/memory/*, api/app/agents/memory.py (removed)
Add memory package re-exports (retrieval, extraction, notebook); update code to import file_storage/extraction submodules and restore unified memory API surface.
Centralized schemas
api/app/domains/*/schemas.py (auth, config, feedback, knowledge, memory, knowledge_graph, setup, skill, notebook)
Move many Pydantic models out of routers into dedicated schema modules; add/extend fields and validators (e.g., setup validators, notebook cover fields, memory schemas, knowledge-graph schemas).
Logging & LLM providers
api/app/logging_config.py, api/app/providers/llm.py, api/app/providers/openai_provider.py, api/app/database.py
Add colorized logging and setup helper; provide get_client/get_model helpers; configure OpenAI client timeout/retries; silence SQL echo by default.
Response helpers & setup flow
api/app/schemas/response.py, api/app/config.py, api/app/auth.py
Add CODE_SUCCESS/CODE_NOT_CONFIGURED and not_configured() helper; minor doc/config text tweaks.
Workers & Celery
api/app/workers/tasks.py
Enhance Celery worker logging and reload DB-backed settings per worker; switch tasks to centralized LLM client and updated module paths.
Frontend: deep-research UI & store
web/src/store/use-deep-research-store.ts, web/src/features/chat/use-deep-research.ts, web/src/features/chat/dr-*.tsx, web/src/services/ai-service.ts, web/src/lib/api-routes.ts
Add Zustand deep-research store, DR UI components (floating indicator, toggle), new ai-service APIs (create/subscribe/status), SSE handling, and client routes for streaming and status.
Frontend: chat, reasoning & tools
web/src/features/chat/*, web/src/components/chat-input/*, web/src/features/chat/choice-cards.tsx, web/src/features/chat/use-chat-stream.ts, web/src/lib/chat-tools.ts
Add ChoiceCards parser/component, Mermaid/code-block support, reasoning streaming support, ChatToolbar extraction and definitions, thinking toggles, UI/UX tweaks, and token streaming handling.
Frontend: notebook icon system
web/src/features/notebook/notebook-icons.tsx, web/src/features/notebook/notebook-card.tsx, web/src/services/notebook-service.ts
Introduce centralized icon registry and deterministic picker; replace emoji/gradient covers with iconId/cover_emoji flow; add updateNotebook API.
Client error handling (not configured)
web/src/lib/http-client.ts, web/src/lib/request-error.ts
Add CODE_NOT_CONFIGURED constant and handleNotConfigured() redirect-to-setup; wire http-client to call it when API envelope returns not-configured code.
Docs, scripts & packaging
README*.md, docs-site/*, scripts/start.sh, web/README.md, web/package.json, lyra
Remove NEXT_PUBLIC_USE_MOCK from docs, drop @clerk/nextjs from package.json, update getting-started docs, add port-check helper in start.sh, and small messaging edits.
Tests
api/tests/*
Add e2e, integration, and unit tests covering conversation service, deep-research utilities/extraction, and response helpers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Web as Web Client
    participant API as FastAPI API
    participant DB as PostgreSQL
    participant LLM as LLM Provider

    User->>Web: POST /ai/deep-research (query, mode, notebook_id)
    Web->>API: createDeepResearch -> {task_id, conversation_id}
    API->>DB: Persist ResearchTask (status=running) and Conversation/Message
    API->>API: spawn run_research_task(task_id)
    API->>LLM: plan / research / synthesize steps
    LLM-->>API: plan/learning/token/deliverable events
    API->>API: TaskBuffer.push(event)
    loop streaming events
      Web->>API: GET /ai/deep-research/{task_id}/events (SSE)
      API->>Web: stream buffered events to client
    end
    API->>DB: Update ResearchTask (report, deliverable, timeline, status, completed_at)
Loading
sequenceDiagram
    participant User
    participant Web as Web Client
    participant API as FastAPI API
    participant DB as PostgreSQL
    participant RAG as RAG Retrieval
    participant LLM as LLM Provider

    User->>Web: POST /conversations/{id}/messages (content, attachments)
    Web->>API: ConversationService.stream_agent(...)
    API->>DB: save user Message
    API->>DB: load conversation history and notebook summary
    API->>API: build_memory_context (memory service)
    API->>RAG: retrieve_chunks(query, notebook_id)
    RAG-->>API: chunks
    API->>LLM: run_agent (ReAct with tools + retrieval)
    loop streaming
      LLM-->>API: tokens / tool calls / thoughts / reasoning
      API->>Web: SSE events (token, citations, reasoning, done)
      alt tool call
        API->>API: execute_tool -> result
        API->>LLM: tool result fed back
      end
    end
    API->>DB: persist assistant Message (content, citations, reasoning)
    API->>API: dispatch background tasks (memory extraction, reflection...)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through diffs and nibbled threads of code,

Buffers buzzed and events along the road,
Icons swapped their tiny hats and beams,
Deep research grew from tokens, plans, and dreams,
A crunchy carrot for the refactor-load.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/backend-architecture

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
web/src/components/chat-input/chat-input.tsx (1)

142-149: ⚠️ Potential issue | 🟡 Minor

Disable the action button when streaming without onCancel.

Line 199 keeps the button enabled whenever streaming is true, but Line 144 can no-op if onCancel is missing. That leaves an active control that does nothing.

💡 Suggested fix
-    const canSend = !disabled && !streaming && !!value.trim();
+    const canSend = !disabled && !streaming && !!value.trim();
+    const canCancel = streaming && !!onCancel;
+    const isActionDisabled = streaming ? !canCancel : !canSend;
...
-                disabled={!streaming && !canSend}
+                disabled={isActionDisabled}
                 className={cn(
                   "flex items-center justify-center rounded-full transition-all",
                   isCompact ? "h-8 w-8" : "h-9 w-9",
                   streaming
-                    ? "bg-foreground text-background hover:bg-foreground/80"
+                    ? canCancel
+                      ? "bg-foreground text-background hover:bg-foreground/80"
+                      : "cursor-not-allowed bg-muted/60 text-muted-foreground/30"
                     : canSend
                       ? "bg-foreground text-background hover:bg-foreground/80 active:scale-95"
                       : "cursor-not-allowed bg-muted/60 text-muted-foreground/30",
                 )}

Also applies to: 199-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chat-input/chat-input.tsx` around lines 142 - 149, The
action button remains enabled while streaming even if no onCancel is provided,
causing clicks to no-op; update the button's disabled logic to prevent
interaction when streaming and onCancel is undefined (e.g., set
disabled={streaming && !onCancel} or compute a canInteract = !streaming ||
!!onCancel and use that) and ensure handleSendClick remains unchanged (it
already calls onCancel?.() and onSubmit(q)). Target the component around
handleSendClick and the button render where streaming/value/onCancel/onSubmit
are used.
api/app/domains/knowledge_graph/router.py (2)

116-130: ⚠️ Potential issue | 🔴 Critical

Gate notebook rebuilds by ownership before enqueueing work.

_current_user is unused here, so any authenticated caller who knows a notebook UUID can trigger a rebuild against another user's notebook. Please verify ownership before the task is queued or the inline fallback runs.

🔐 Suggested guard
 async def rebuild_graph(
     notebook_id: UUID,
     _current_user: CurrentUser,
     db: DbDep,
 ):
     """Trigger a full rebuild of the knowledge graph for a notebook."""
+    from app.models import Notebook
+
+    owned_notebook = await db.execute(
+        select(Notebook.id).where(
+            Notebook.id == notebook_id,
+            Notebook.user_id == _current_user.id,
+            Notebook.status == "active",
+        )
+    )
+    if owned_notebook.scalar_one_or_none() is None:
+        raise NotFoundError("笔记本不存在")
+
     try:
         from app.workers.tasks import rebuild_knowledge_graph_task
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/knowledge_graph/router.py` around lines 116 - 130, The
rebuild_graph endpoint currently ignores _current_user and allows any
authenticated caller to enqueue or run a rebuild; add an ownership check before
queuing or running the fallback: load the notebook record (by notebook_id) from
the DB using the existing db dependency, compare its owner/user id to
_current_user.id (or appropriate identity field on CurrentUser), and if they
don't match return an unauthorized/forbidden response; perform this check before
calling rebuild_knowledge_graph_task.delay(...) and again before calling the
inline rebuild_notebook_graph fallback so both paths are gated by ownership
(refer to the rebuild_graph function, the _current_user param, notebook_id, db,
and rebuild_knowledge_graph_task/rebuild_notebook_graph symbols).

122-130: ⚠️ Potential issue | 🟠 Major

Don't run the rebuild synchronously in the task-dispatch fallback.

Because this catches every exception around import/dispatch, a broker outage or wiring bug silently turns these background endpoints into long-running request handlers. rebuild-all also still returns status="started" after doing the work inline, which makes the API contract misleading.

Also applies to: 176-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/knowledge_graph/router.py` around lines 122 - 130, Replace
the broad except block that falls back to running rebuild synchronously: do not
call rebuild_notebook_graph inline. Instead catch the dispatch/import error as e
(avoid bare except Exception), log the error, and return a failure response
(e.g., RebuildStatusOut(status="failed", detail=str(e)) or an appropriate error
message) so the API doesn't claim "started" when work ran inline; apply the same
change for the other fallback at the second occurrence (references:
rebuild_knowledge_graph_task.delay, rebuild_notebook_graph, RebuildStatusOut).
api/app/agents/memory/file_storage.py (1)

185-226: ⚠️ Potential issue | 🟠 Major

The default Chinese MEMORY.md sections all upsert under the same key.

_parse_memory_doc_sections() only preserves [a-z0-9_], so headings like 关于我 / 技术背景 collapse to file_. In sync_memory_doc_to_db(), each _upsert_memory() then overwrites the previous section and only the last block survives in memory.

🛠️ One simple fallback
slug = re.sub(r"[^a-z0-9_]", "_", heading.lower()).strip("_")
if not slug:
    slug = f"section_{i // 2}"
key = f"file_{slug}"

Also applies to: 229-272

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/memory/file_storage.py` around lines 185 - 226, _pars
e_memory_doc_sections currently strips all non a-z0-9_ characters which makes
headings like "关于我" collapse to an empty slug so every item becomes the same key
and later _upsert_memory overwrites previous entries; fix by producing a stable
fallback slug when the sanitized heading is empty (e.g., compute slug =
re.sub(...).strip("_"); if not slug: slug = f"section_{i//2}" or include the
loop index or a short hash) and then build the key as f"file_{slug}"; apply the
same fallback logic in the other parsing block referenced around lines 229-272
and ensure sync_memory_doc_to_db/_upsert_memory receive unique keys per section.
api/app/workers/tasks.py (1)

210-227: ⚠️ Potential issue | 🟠 Major

generate_notebook_summary still hard-codes the model.

This call site now uses the centralized client, but it still pins "gpt-4o-mini". That bypasses runtime config and can break as soon as the selected provider/model changes.

Suggested change
-        from app.providers.llm import get_client
+        from app.providers.llm import get_client, get_model
         client = get_client()
@@
-                model="gpt-4o-mini",
+                model=get_model() or "gpt-4o-mini",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/workers/tasks.py` around lines 210 - 227, The call in
generate_notebook_summary hardcodes "gpt-4o-mini"; update it to use the
configured runtime model instead—locate the client returned by get_client() and
replace the literal model string in the client.chat.completions.create call with
the provider/model value from your runtime config or the client object (e.g.,
use client.default_model or settings.get("OPENAI_MODEL") / app config used
elsewhere), so the selected provider/model is respected at runtime; ensure the
symbol names referenced are generate_notebook_summary, get_client, and
client.chat.completions.create.
🟡 Minor comments (15)
web/src/components/settings/settings-primitives.tsx-82-84 (1)

82-84: ⚠️ Potential issue | 🟡 Minor

Hardcoded UI strings should use translation system.

The component uses useTranslations for other text but has hardcoded strings:

  • Lines 83, 107: "Thinking" (English)
  • Line 132: "搜索模型..." (Chinese)
  • Line 151: "无匹配模型" (Chinese)

These should use the translation system for consistency and proper i18n support.

🌐 Proposed fix to use translations

Add translation keys and update the code:

-            <span className="flex-shrink-0 rounded-md bg-violet-500/15 px-1.5 py-0.5 text-[10px] font-medium text-violet-400">
-              Thinking
-            </span>
+            <span className="flex-shrink-0 rounded-md bg-violet-500/15 px-1.5 py-0.5 text-[10px] font-medium text-violet-400">
+              {tc("thinking")}
+            </span>
-                  placeholder="搜索模型..."
+                  placeholder={tc("searchModel")}
-                <div className="px-3 py-2 text-xs text-muted-foreground">无匹配模型</div>
+                <div className="px-3 py-2 text-xs text-muted-foreground">{tc("noMatchingModel")}</div>

Also applies to: 106-108, 132-132, 151-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/settings-primitives.tsx` around lines 82 - 84,
The three hardcoded UI strings in settings-primitives.tsx ("Thinking" in the
status badge, "搜索模型..." placeholder, and "无匹配模型" empty-state text) must use the
existing translation hook instead of literal text: import/use the
useTranslations instance already used in this file (t) and replace those
literals with t('settings.thinking'), t('settings.searchPlaceholder') and
t('settings.noMatchingModels') (or similar keys), then add those keys and
translations to your i18n resource files; update the span, input placeholder,
and empty-state render in the component to call t(...) so all UI text is
localized.
web/src/components/settings/settings-primitives.tsx-46-51 (1)

46-51: ⚠️ Potential issue | 🟡 Minor

Missing cleanup for setTimeout can cause stale focus.

If the dropdown closes before the 50ms timeout fires, the focus call will execute on a closed dropdown, potentially causing unexpected focus behavior.

🛡️ Proposed fix to add timeout cleanup
   useEffect(() => {
     if (open) {
       setSearch("");
-      setTimeout(() => inputRef.current?.focus(), 50);
+      const timerId = setTimeout(() => inputRef.current?.focus(), 50);
+      return () => clearTimeout(timerId);
     }
   }, [open]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/settings-primitives.tsx` around lines 46 - 51,
The effect that focuses the input after a 50ms setTimeout (inside useEffect) can
run after the dropdown closes; capture the timer id when calling setTimeout and
return a cleanup function from useEffect that calls clearTimeout for that id so
the pending focus call is cancelled when open changes or the component unmounts;
update the effect that uses setSearch, setTimeout and inputRef.current?.focus()
to store the timeout handle and clear it in the cleanup.
api/app/services/source_service.py-298-301 (1)

298-301: ⚠️ Potential issue | 🟡 Minor

Preserve exception chain when re-raising.

When catching FileNotFoundError and raising NotFoundError, use exception chaining to preserve the original traceback for debugging.

🔧 Proposed fix
                 except FileNotFoundError:
-                    raise NotFoundError("文件未找到")
+                    raise NotFoundError("文件未找到") from None

Using from None explicitly suppresses the chain if you don't want the original exception shown, or use from err to preserve it:

except FileNotFoundError as err:
    raise NotFoundError("文件未找到") from err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/source_service.py` around lines 298 - 301, The except block
that catches FileNotFoundError around the call to get_storage().download should
preserve the original exception chain when re-raising NotFoundError; update the
handler in the source_service.py code that wraps get_storage().download so it
catches FileNotFoundError as a variable (e.g., err) and re-raises NotFoundError
using exception chaining (raise NotFoundError("文件未找到") from err) to keep the
original traceback available for debugging while still returning the
higher-level NotFoundError.
scripts/start.sh-44-55 (1)

44-55: ⚠️ Potential issue | 🟡 Minor

_check_port only handles one PID and may leave the port occupied.

Line 44 takes only the first listener (head -1). If multiple listeners exist, startup can still fail later with the same port conflict.

Suggested patch
 _check_port() {
   local port=$1 name=$2
-  if lsof -i ":$port" -sTCP:LISTEN -t &>/dev/null; then
-    local pid
-    pid=$(lsof -i ":$port" -sTCP:LISTEN -t | head -1)
+  if lsof -i ":$port" -sTCP:LISTEN -t &>/dev/null; then
+    local pids pid
+    pids=$(lsof -i ":$port" -sTCP:LISTEN -t | sort -u)
+    pid=$(echo "$pids" | head -1)
     local pname
     pname=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
     warn "$name 端口 $port 被占用 (PID $pid - $pname)"
     read -rp "  是否终止该进程?[y/N] " ans
     if [[ "$ans" =~ ^[Yy]$ ]]; then
-      kill -9 "$pid" && log "已终止 PID $pid"
+      while read -r pid; do
+        kill -9 "$pid" && log "已终止 PID $pid"
+      done <<< "$pids"
       sleep 1
+      lsof -i ":$port" -sTCP:LISTEN -t &>/dev/null && error "端口 $port 仍被占用,无法启动 $name"
     else
       error "端口 $port 被占用,无法启动 $name"
     fi
   fi
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/start.sh` around lines 44 - 55, The _check_port logic only
inspects/kills the first PID from lsof (head -1), leaving other listeners and
causing lingering port conflicts; change the implementation in _check_port to
collect all listener PIDs for the port (use lsof -t or equivalent) and iterate
over each PID, resolve its process name via ps -p, prompt once or per-PID as
desired, and attempt to kill each returned PID (with logging on success/failure)
so the port is fully cleared before proceeding; update the warn/log/error calls
in the same function to reflect per-PID results.
web/src/app/(workspace)/app/notebooks/[id]/page.tsx-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor

Avoid duplicate border layers in dark mode.

NotebookWorkspace already applies the same border styling, so this wrapper can create a doubled border visual.

♻️ Proposed fix
-    <div className="flex h-full flex-col dark:border border-border/40">
+    <div className="flex h-full flex-col">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/`(workspace)/app/notebooks/[id]/page.tsx at line 19, The wrapper
div with className "flex h-full flex-col dark:border border-border/40" is adding
the same border that NotebookWorkspace already applies, causing a doubled border
in dark mode; remove the border classes ("dark:border" and "border-border/40")
from that div (keep "flex h-full flex-col") so only NotebookWorkspace controls
the border styling and visual duplication is resolved.
docs-site/en/getting-started.md-99-99 (1)

99-99: ⚠️ Potential issue | 🟡 Minor

Do not list DEBUG as a minimum required variable.

This variable is optional and development-only; current wording in the “Minimum Required Environment Variables” section is misleading.

📝 Proposed doc fix
-| `DEBUG` | Set `true` to skip authentication |
+| `DEBUG` | Optional (local development only): set `true` to skip authentication |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs-site/en/getting-started.md` at line 99, Remove `DEBUG` from the "Minimum
Required Environment Variables" list and update the docs so that `DEBUG` is
documented only in the optional/development settings section (e.g., an "Optional
/ Development Environment Variables" or "Development-only" note). Locate the
occurrence of the `DEBUG` entry in the "Minimum Required Environment Variables"
section and delete or move it, then add a brief sentence in the development
section clarifying that `DEBUG` is optional and intended for local/dev use only
(set to true to skip authentication).
api/app/skills/builtin/mind_map.py-45-47 (1)

45-47: ⚠️ Potential issue | 🟡 Minor

Inconsistent default type in depth argument handling.

The schema defines depth as a string enum ["2", "3"], but line 59 uses an integer default: args.get("depth", 2). If the argument is missing, it defaults to 2 (int), not "2" (string). While int() handles both, the inconsistency may cause confusion.

🔧 Proposed fix for consistency
-        depth = int(args.get("depth", 2))
+        depth = int(args.get("depth", "2"))

Also applies to: 59-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/skills/builtin/mind_map.py` around lines 45 - 47, The depth
argument's schema uses string enum ["2","3"] but the code calls
args.get("depth", 2) returning an int default; update the handler so the default
matches the schema by using args.get("depth", "2") and then coerce with
int(args.get("depth", "2")) where depth is used (or alternatively change the
schema to use integers); specifically adjust the code around the depth retrieval
(the args.get("depth", ...) usage) to return a string default "2" and then
convert to int consistently.
web/src/app/setup/page.tsx-449-450 (1)

449-450: ⚠️ Potential issue | 🟡 Minor

Localize the “Thinking” badge label.

Line 450 hardcodes English text in an otherwise localized flow. Please move this to i18n keys for language consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/setup/page.tsx` around lines 449 - 450, Hardcoded "Thinking"
badge text needs to be replaced with a localized string: update the span that
currently renders "Thinking" to use the app's i18n lookup (e.g., call the
translation function/hook used across the codebase such as t('status.thinking')
or equivalent) and add the new key "status.thinking" to your locale files for
all languages; ensure the component (page.tsx) imports/uses the translation hook
(useTranslation or the project's helper) if not already present and keep the
existing className and markup intact.
web/src/store/use-deep-research-store.ts-225-231 (1)

225-231: ⚠️ Potential issue | 🟡 Minor

Missing error handling for createDeepResearch failure.

If createDeepResearch throws, the store is left in an inconsistent state with isActive: true but no taskId. Consider wrapping the API call and subscription in a try-catch to reset state on failure.

Proposed fix
       async start(query, notebookId, mode) {
         _abortController?.abort();

         set({
           query,
           notebookId,
           mode,
           progress: makeInitialProgress(mode),
           reportTokens: "",
           isActive: true,
           eventIndex: 0,
           taskId: null,
           conversationId: null,
         });

+        try {
           const { taskId, conversationId } = await createDeepResearch(query, {
             notebookId,
             mode,
           });
           set({ taskId, conversationId });

           subscribeTo(taskId, get, set);
+        } catch (err) {
+          set({
+            isActive: false,
+            progress: null,
+            taskId: null,
+            conversationId: null,
+          });
+          throw err; // Re-throw so caller can handle
+        }
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/use-deep-research-store.ts` around lines 225 - 231, Wrap the
createDeepResearch call and subsequent subscribeTo invocation in a try-catch so
failures don’t leave the store inconsistent; call createDeepResearch(...) and
only on success call set({ taskId, conversationId }) and subscribeTo(taskId,
get, set), and in the catch reset the store (e.g. set isActive:false and clear
taskId/conversationId) and surface/log the error. Ensure subscribeTo is never
invoked if createDeepResearch throws and that isActive is only set true after a
successful createDeepResearch/subscribeTo sequence.
web/src/features/notebook/notebook-card.tsx-139-150 (1)

139-150: ⚠️ Potential issue | 🟡 Minor

Potential double router.refresh() call.

In handleSave (line 146), router.refresh() is called after onSaved(). However, the onSaved callback passed from NotebookCard (line 338) also calls router.refresh(). This results in the router being refreshed twice, which is unnecessary and could cause performance issues.

🔧 Proposed fix: Remove duplicate refresh

Either remove the refresh from handleSave:

   async function handleSave() {
     const title = name.trim();
     if (!title || loading) return;
     setLoading(true);
     try {
       await updateNotebook(notebook.id, { title, cover_emoji: iconId });
       onSaved();
-      router.refresh();
     } finally {
       setLoading(false);
     }
   }

Or simplify the onSaved callback:

       <EditNotebookDialog
         open={editOpen}
         notebook={notebook}
         onClose={() => setEditOpen(false)}
-        onSaved={() => { setEditOpen(false); router.refresh(); }}
+        onSaved={() => setEditOpen(false)}
       />

Also applies to: 334-339

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/notebook/notebook-card.tsx` around lines 139 - 150,
handleSave currently calls router.refresh() and the onSaved callback passed into
NotebookCard also calls router.refresh(), causing a duplicate refresh; remove
one of them — either delete the router.refresh() call inside handleSave
(function handleSave) after onSaved(), or change the onSaved callback provided
by NotebookCard to not call router.refresh() so only one refresh remains; update
the code where onSaved is defined/used to ensure a single refresh happens and
keep setLoading/reset logic intact.
web/src/features/chat/chat-view.tsx-598-627 (1)

598-627: ⚠️ Potential issue | 🟡 Minor

Potential duplicate user message on focus return.

When drFocusRequested triggers and drState.query exists, a new synthetic user message is created. If the backend already saved the user message during DR initiation (as mentioned in the completion watcher), this could result in a duplicate when the messages list is later refreshed from the server.

Consider checking if the conversation already has messages before creating a synthetic one, or relying solely on the server-persisted messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/chat/chat-view.tsx` around lines 598 - 627, When
drFocusRequested triggers in the useEffect handling deep research return, avoid
always inserting a synthetic user message from drState.query; instead check the
current conversation messages before injecting to prevent duplicates. In the
effect that uses useDeepResearchStore.getState(), use the existing identifiers
setActiveConvId, saveActiveConversation, setMessages and drState.query to first
inspect the current messages for the target conversation (or the local messages
array) and only create/set the synthetic LocalMessage if the conversation has no
messages or the last message content/role doesn't match drState.query; otherwise
rely on server-persisted messages and skip creating the synthetic user message.
api/app/domains/knowledge/schemas.py-28-34 (1)

28-34: ⚠️ Potential issue | 🟡 Minor

Add default value for optional error field.

In Pydantic v2, str | None without a default makes the field required. Since error is semantically optional (jobs may not have errors), it should default to None.

🐛 Proposed fix
 class JobStatusOut(BaseModel):
     id: UUID
     type: str
     status: str
-    error: str | None
+    error: str | None = None

     model_config = {"from_attributes": True}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/knowledge/schemas.py` around lines 28 - 34, The JobStatusOut
Pydantic model marks error as optional with the type union `str | None` but does
not provide a default, making it required in Pydantic v2; update the
JobStatusOut model by giving the error field a default of None (e.g., set error
to have a default None) so it is truly optional while keeping model_config =
{"from_attributes": True}.
web/src/features/chat/dr-floating-indicator.tsx-62-84 (1)

62-84: ⚠️ Potential issue | 🟡 Minor

Add keyboard handler for accessibility.

The m.div has role="button" and tabIndex={0} but lacks an onKeyDown handler. Keyboard users won't be able to activate it with Enter or Space.

♿ Proposed fix
         onClick={() => {
           if (isChatPage) {
             requestFocus();
           } else {
             router.push("/app/chat");
             requestFocus();
           }
         }}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            if (isChatPage) {
+              requestFocus();
+            } else {
+              router.push("/app/chat");
+              requestFocus();
+            }
+          }
+        }}
         role="button"
         tabIndex={0}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/chat/dr-floating-indicator.tsx` around lines 62 - 84, The
floating indicator (the m.div with role="button" and tabIndex={0}) is missing
keyboard support; add an onKeyDown handler that listens for Enter and Space and
invokes the same activation logic as the onClick (call requestFocus() and
router.push("/app/chat") when needed), ensuring you preventDefault() for Space
to avoid page scrolling and keep the same isChatPage branching; implement the
handler as a named function (e.g., handleActivateKey) near the component so it
can be reused by onClick and onKeyDown.
web/src/features/chat/use-chat-stream.ts-133-140 (1)

133-140: ⚠️ Potential issue | 🟡 Minor

Clear partial reasoning if the stream later fails.

After one or more "reasoning" events, the catch path only replaces content, so the assistant bubble can end up showing stale reasoning next to the error message.

🧹 Suggested fix
     } catch (error) {
       if (isAbortError(error)) return;
       const message = getErrorMessage(error, t("streamFailed"));
       streamLifecycle.fail(message);
       notifyError(message);
       setStreaming(false);
+      const curId = assistantIdRef.current;
       setMessages((prev) =>
-        prev.map((m) => m.id === assistantId ? { ...m, content: message } : m)
+        prev.map((m) =>
+          m.id === curId ? { ...m, content: message, reasoning: undefined } : m
+        )
       );

Also applies to: 171-179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/chat/use-chat-stream.ts` around lines 133 - 140, The
assistant's partial "reasoning" buffer (reasoningContentRef) can remain visible
when the stream later fails because the error-handling path only replaces
message.content; update the catch/error branches that call setMessages to also
clear the assistant message's reasoning field (set reasoning: "" or undefined)
for the current assistant id (use assistantIdRef.current) so stale reasoning is
removed; apply the same change to both places handling stream failure where
setMessages is used (refer to reasoningContentRef, assistantIdRef, and the
setMessages mapping logic).
api/app/services/conversation_service.py-325-344 (1)

325-344: ⚠️ Potential issue | 🟡 Minor

Nested task also needs reference storage.

The asyncio.create_task(self._sync_diary_safe(today)) on line 341 has the same issue as flagged above—store the task reference to prevent potential garbage collection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/conversation_service.py` around lines 325 - 344, In
_maybe_flush_diary, the asyncio.create_task(self._sync_diary_safe(today)) call
can be garbage-collected because its reference isn't retained; instead assign
the created task to a variable and store it on the instance (e.g.,
self._pending_tasks or self._background_tasks) so the task reference is kept
alive, and optionally add a done callback to remove the task from the collection
when complete; update the code around asyncio.create_task in the
_maybe_flush_diary method to use this pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 495b423f-37d3-4cc5-99a1-7845523b92b6

📥 Commits

Reviewing files that changed from the base of the PR and between a7a3d71 and a2c10b6.

⛔ Files ignored due to path filters (26)
  • api/app/__pycache__/config.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/main.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/models.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/ai/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/auth/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/config/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/conversation/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/feedback/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge_graph/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/memory/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/setup/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/skill/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/source/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/llm.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/storage.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/__pycache__/base.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/compare_sources.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/deep_read.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/mind_map.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/scheduled_task.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/search_knowledge.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/summarize.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/update_memory_doc.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/workers/__pycache__/tasks.cpython-312.pyc is excluded by !**/*.pyc
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (125)
  • README.md
  • README.zh-CN.md
  • api/alembic/versions/025_drop_clerk_id.py
  • api/alembic/versions/026_add_research_tasks.py
  • api/alembic/versions/027_research_task_conversation.py
  • api/app/agents/core/__init__.py
  • api/app/agents/core/brain.py
  • api/app/agents/core/engine.py
  • api/app/agents/core/instructions.py
  • api/app/agents/core/react_agent.py
  • api/app/agents/core/state.py
  • api/app/agents/core/tools.py
  • api/app/agents/kg/__init__.py
  • api/app/agents/kg/knowledge_graph.py
  • api/app/agents/memory.py
  • api/app/agents/memory/__init__.py
  • api/app/agents/memory/extraction.py
  • api/app/agents/memory/file_storage.py
  • api/app/agents/memory/notebook.py
  • api/app/agents/memory/retrieval.py
  • api/app/agents/rag/__init__.py
  • api/app/agents/rag/ingestion.py
  • api/app/agents/rag/retrieval.py
  • api/app/agents/research/__init__.py
  • api/app/agents/research/deep_research.py
  • api/app/agents/research/evaluation.py
  • api/app/agents/research/reflection.py
  • api/app/agents/research/task_manager.py
  • api/app/agents/writing/__init__.py
  • api/app/agents/writing/composer.py
  • api/app/agents/writing/ghost_text.py
  • api/app/agents/writing/scene_detector.py
  • api/app/auth.py
  • api/app/config.py
  • api/app/database.py
  • api/app/domains/ai/router.py
  • api/app/domains/ai/routers/__init__.py
  • api/app/domains/ai/routers/insights.py
  • api/app/domains/ai/routers/knowledge.py
  • api/app/domains/ai/routers/research.py
  • api/app/domains/ai/routers/suggestions.py
  • api/app/domains/ai/routers/writing.py
  • api/app/domains/ai/schemas.py
  • api/app/domains/auth/router.py
  • api/app/domains/auth/schemas.py
  • api/app/domains/config/router.py
  • api/app/domains/config/schemas.py
  • api/app/domains/conversation/router.py
  • api/app/domains/feedback/router.py
  • api/app/domains/feedback/schemas.py
  • api/app/domains/knowledge/router.py
  • api/app/domains/knowledge/schemas.py
  • api/app/domains/knowledge_graph/router.py
  • api/app/domains/knowledge_graph/schemas.py
  • api/app/domains/memory/router.py
  • api/app/domains/memory/schemas.py
  • api/app/domains/notebook/schemas.py
  • api/app/domains/setup/router.py
  • api/app/domains/setup/schemas.py
  • api/app/domains/skill/router.py
  • api/app/domains/skill/schemas.py
  • api/app/domains/source/router.py
  • api/app/logging_config.py
  • api/app/main.py
  • api/app/models.py
  • api/app/providers/llm.py
  • api/app/providers/openai_provider.py
  • api/app/schemas/response.py
  • api/app/services/__init__.py
  • api/app/services/conversation_service.py
  • api/app/services/source_service.py
  • api/app/skills/base.py
  • api/app/skills/builtin/compare_sources.py
  • api/app/skills/builtin/deep_read.py
  • api/app/skills/builtin/mind_map.py
  • api/app/skills/builtin/scheduled_task.py
  • api/app/skills/builtin/search_knowledge.py
  • api/app/skills/builtin/summarize.py
  • api/app/skills/builtin/update_memory_doc.py
  • api/app/workers/tasks.py
  • docs-site/en/getting-started.md
  • docs-site/zh/getting-started.md
  • lyra
  • scripts/start.sh
  • web/README.md
  • web/messages/en.json
  • web/messages/zh.json
  • web/package.json
  • web/src/app/(auth)/login/page.tsx
  • web/src/app/(marketing)/notebooks/[id]/page.tsx
  • web/src/app/(marketing)/page.tsx
  • web/src/app/(workspace)/app/notebooks/[id]/page.tsx
  • web/src/app/(workspace)/app/page.tsx
  • web/src/app/(workspace)/app/settings/page.tsx
  • web/src/app/(workspace)/layout.tsx
  • web/src/app/setup/page.tsx
  • web/src/components/chat-input/chat-input.tsx
  • web/src/components/layout/app-shell.tsx
  • web/src/components/settings/settings-primitives.tsx
  • web/src/features/auth/auth-provider.tsx
  • web/src/features/chat/chat-message-bubble.tsx
  • web/src/features/chat/chat-view.tsx
  • web/src/features/chat/choice-cards.tsx
  • web/src/features/chat/dr-floating-indicator.tsx
  • web/src/features/chat/dr-learning-card.tsx
  • web/src/features/chat/dr-mode-toggle.tsx
  • web/src/features/chat/dr-types.ts
  • web/src/features/chat/use-chat-stream.ts
  • web/src/features/chat/use-deep-research.ts
  • web/src/features/knowledge/knowledge-view.tsx
  • web/src/features/notebook/notebook-card.tsx
  • web/src/features/notebook/notebook-icons.tsx
  • web/src/features/notebook/notebook-workspace.tsx
  • web/src/features/notebook/notebooks-view.tsx
  • web/src/features/tasks/tasks-view.tsx
  • web/src/lib/api-routes.ts
  • web/src/lib/constants.ts
  • web/src/lib/http-client.ts
  • web/src/lib/request-error.ts
  • web/src/middleware.ts
  • web/src/schemas/setup-schemas.ts
  • web/src/services/ai-service.ts
  • web/src/services/auth-service.ts
  • web/src/services/notebook-service.ts
  • web/src/store/use-deep-research-store.ts
💤 Files with no reviewable changes (5)
  • README.zh-CN.md
  • web/package.json
  • README.md
  • api/app/auth.py
  • api/app/agents/memory.py

Comment on lines +25 to +26
sa.Column("mode", sa.String(20), server_default="quick"),
sa.Column("status", sa.String(20), server_default="running"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make core task-state columns non-nullable.

Line 25, Line 26, and Line 31 currently permit explicit NULL despite having defaults. That can create invalid task rows (status/mode/created_at missing) and break queue/state filters.

🧱 Proposed migration hardening
-        sa.Column("mode", sa.String(20), server_default="quick"),
-        sa.Column("status", sa.String(20), server_default="running"),
+        sa.Column("mode", sa.String(20), server_default="quick", nullable=False),
+        sa.Column("status", sa.String(20), server_default="running", nullable=False),
@@
-        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
+        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),

Also applies to: 31-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/alembic/versions/026_add_research_tasks.py` around lines 25 - 26, The
migration currently defines sa.Column("mode", sa.String(20),
server_default="quick"), sa.Column("status", sa.String(20),
server_default="running"), and sa.Column("created_at", sa.DateTime(),
server_default=sa.func.now()) but leaves them nullable; update these column
definitions in 026_add_research_tasks.py to enforce non-nullability by setting
nullable=False while retaining the server_default values so existing rows get
defaults; ensure any Alembic operations that create or alter these columns (the
sa.Column("mode"...), sa.Column("status"...), and sa.Column("created_at"...))
are updated accordingly and that the migration will run without trying to set
NULL defaults.

Comment on lines +17 to +29
def get_client() -> AsyncOpenAI:
"""Return the underlying AsyncOpenAI client from the active provider.

Escape hatch for scenarios that need the raw client (e.g. LangGraph,
streaming with custom parameters). Prefer ``chat`` / ``chat_stream``
for normal usage.
"""
from app.config import settings

return AsyncOpenAI(
api_key=settings.openai_api_key,
base_url=settings.openai_base_url or None,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

get_client() bypasses the selected provider.

This helper always constructs AsyncOpenAI from the openai_* settings, so callers using it ignore llm_provider and will break as soon as Anthropic is the active provider. Either route this through provider_factory, or fail fast for non-OpenAI-compatible providers instead of exposing it as the active provider’s escape hatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/providers/llm.py` around lines 17 - 29, get_client() currently
instantiates AsyncOpenAI directly which ignores the configured llm_provider;
update get_client to obtain the active provider via provider_factory (using
settings.llm_provider) and return its underlying async client, or if the active
provider does not expose an OpenAI-compatible raw client, raise a clear error
(e.g., NotImplementedError) so callers fail fast; reference get_client,
provider_factory, llm_provider, and AsyncOpenAI when making the change.

Comment on lines +275 to +288
def _dispatch_post_chat_tasks(
self,
conversation_id: UUID,
scene: str,
user_memories: list[dict],
) -> None:
asyncio.create_task(self._extract_memories_safe(conversation_id))
asyncio.create_task(self._reflect_safe(conversation_id, scene, user_memories))
asyncio.create_task(self._compress_safe(conversation_id))
asyncio.create_task(self._maybe_flush_diary(conversation_id))

from app.config import settings
if settings.memory_evaluation_sample_rate > 0 and random.random() < settings.memory_evaluation_sample_rate:
asyncio.create_task(self._evaluate_safe(conversation_id))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Store references to background tasks to prevent premature garbage collection.

asyncio.create_task() returns a task object that should be stored. Without a reference, tasks may be garbage collected before completion, especially under memory pressure. This can cause silent failures in memory extraction, reflection, compression, and diary flushing.

🛠️ Proposed fix using a task set
+    _background_tasks: set[asyncio.Task] = set()
+
     def _dispatch_post_chat_tasks(
         self,
         conversation_id: UUID,
         scene: str,
         user_memories: list[dict],
     ) -> None:
-        asyncio.create_task(self._extract_memories_safe(conversation_id))
-        asyncio.create_task(self._reflect_safe(conversation_id, scene, user_memories))
-        asyncio.create_task(self._compress_safe(conversation_id))
-        asyncio.create_task(self._maybe_flush_diary(conversation_id))
+        for coro in [
+            self._extract_memories_safe(conversation_id),
+            self._reflect_safe(conversation_id, scene, user_memories),
+            self._compress_safe(conversation_id),
+            self._maybe_flush_diary(conversation_id),
+        ]:
+            task = asyncio.create_task(coro)
+            self._background_tasks.add(task)
+            task.add_done_callback(self._background_tasks.discard)

         from app.config import settings
         if settings.memory_evaluation_sample_rate > 0 and random.random() < settings.memory_evaluation_sample_rate:
-            asyncio.create_task(self._evaluate_safe(conversation_id))
+            task = asyncio.create_task(self._evaluate_safe(conversation_id))
+            self._background_tasks.add(task)
+            task.add_done_callback(self._background_tasks.discard)

Note: The random.random() usage on line 287 is appropriate for probabilistic sampling—it's not being used for cryptographic purposes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _dispatch_post_chat_tasks(
self,
conversation_id: UUID,
scene: str,
user_memories: list[dict],
) -> None:
asyncio.create_task(self._extract_memories_safe(conversation_id))
asyncio.create_task(self._reflect_safe(conversation_id, scene, user_memories))
asyncio.create_task(self._compress_safe(conversation_id))
asyncio.create_task(self._maybe_flush_diary(conversation_id))
from app.config import settings
if settings.memory_evaluation_sample_rate > 0 and random.random() < settings.memory_evaluation_sample_rate:
asyncio.create_task(self._evaluate_safe(conversation_id))
_background_tasks: set[asyncio.Task] = set()
def _dispatch_post_chat_tasks(
self,
conversation_id: UUID,
scene: str,
user_memories: list[dict],
) -> None:
for coro in [
self._extract_memories_safe(conversation_id),
self._reflect_safe(conversation_id, scene, user_memories),
self._compress_safe(conversation_id),
self._maybe_flush_diary(conversation_id),
]:
task = asyncio.create_task(coro)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
from app.config import settings
if settings.memory_evaluation_sample_rate > 0 and random.random() < settings.memory_evaluation_sample_rate:
task = asyncio.create_task(self._evaluate_safe(conversation_id))
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 281-281: Store a reference to the return value of asyncio.create_task

(RUF006)


[warning] 282-282: Store a reference to the return value of asyncio.create_task

(RUF006)


[warning] 283-283: Store a reference to the return value of asyncio.create_task

(RUF006)


[warning] 284-284: Store a reference to the return value of asyncio.create_task

(RUF006)


[error] 287-287: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


[warning] 288-288: Store a reference to the return value of asyncio.create_task

(RUF006)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/conversation_service.py` around lines 275 - 288,
_dispatch_post_chat_tasks currently fires background work with
asyncio.create_task(...) but drops the Task objects, risking premature GC;
capture and retain them on the instance (e.g., add to a self._background_tasks
set) when creating tasks for _extract_memories_safe, _reflect_safe,
_compress_safe, _maybe_flush_diary and _evaluate_safe, and register an
add_done_callback to remove each task from the set when finished; ensure
self._background_tasks is initialized (e.g., in __init__) so these tasks are
referenced until completion.

Comment on lines +105 to +116
def _dispatch_refresh_summary(self, notebook_id: UUID) -> None:
asyncio.create_task(self._refresh_summary_safe(notebook_id))

async def _refresh_summary_safe(self, notebook_id: UUID) -> None:
from app.agents.memory import refresh_notebook_summary
from app.database import AsyncSessionLocal
try:
async with AsyncSessionLocal() as session:
await refresh_notebook_summary(notebook_id, session)
await session.commit()
except Exception as exc:
logger.warning("Failed to refresh notebook summary after source deletion: %s", exc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fire-and-forget task may be garbage collected before completion.

The task created by asyncio.create_task without storing a reference can be garbage collected before it completes. Python's asyncio documentation explicitly warns about this behavior.

Consider storing task references in a set to prevent premature GC:

🔧 Proposed fix to prevent task garbage collection
+_background_tasks: set = set()
+
 def _dispatch_refresh_summary(self, notebook_id: UUID) -> None:
-    asyncio.create_task(self._refresh_summary_safe(notebook_id))
+    task = asyncio.create_task(self._refresh_summary_safe(notebook_id))
+    _background_tasks.add(task)
+    task.add_done_callback(_background_tasks.discard)
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 106-106: Store a reference to the return value of asyncio.create_task

(RUF006)


[warning] 115-115: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/source_service.py` around lines 105 - 116, The
fire-and-forget task created in _dispatch_refresh_summary via
asyncio.create_task(self._refresh_summary_safe(...)) can be garbage-collected
before completion; update the service to keep a strong reference (e.g.,
self._refresh_tasks = set()) and add each created task to that set in
_dispatch_refresh_summary, attach a done-callback that removes the task from
self._refresh_tasks (and logs exceptions if task.exception() is set), and leave
_refresh_summary_safe unchanged; this ensures tasks started by
_dispatch_refresh_summary are retained until finished and cleaned up when done.

Comment on lines +53 to +58
@worker_process_init.connect
def _on_worker_init(**_kwargs):
"""Runs in each forked worker process: load DB config (API keys etc.)."""
from app.logging_config import setup_logging
setup_logging(debug=settings.debug)
_load_db_settings_sync()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make DB-backed settings preload best-effort.

_load_db_settings_sync() now runs unguarded during worker process init. A transient DB outage here can stop the worker from booting at all, which stalls queued retries and beat-dispatched jobs. Catch/log and continue with env settings, or move this refresh out of the startup path.

Suggested change
 `@worker_process_init.connect`
 def _on_worker_init(**_kwargs):
     """Runs in each forked worker process: load DB config (API keys etc.)."""
+    import logging
+
     from app.logging_config import setup_logging
     setup_logging(debug=settings.debug)
-    _load_db_settings_sync()
+    try:
+        _load_db_settings_sync()
+    except Exception:
+        logging.getLogger(__name__).exception(
+            "Failed to preload DB-backed settings; continuing with environment defaults"
+        )

Also applies to: 61-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/workers/tasks.py` around lines 53 - 58, The worker init handler
_on_worker_init calls _load_db_settings_sync() unguarded so a transient DB
outage can crash worker startup; make the DB-backed settings preload best-effort
by wrapping the _load_db_settings_sync() call in a try/except that logs the
exception (using process/logger from app.logging_config) and continues without
raising, falling back to environment settings; apply the same pattern to the
other startup path(s) mentioned (the similar handler around lines 61-80) so
refreshes do not block worker boot or retries.

Comment on lines +156 to +253
// ── MermaidBlock ──────────────────────────────────────────────────────────────

const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mermaidReady: Promise<any> | null = null;

function loadMermaid() {
if (!mermaidReady) {
mermaidReady = import(/* webpackIgnore: true */ MERMAID_CDN).then((m) => {
const mermaid = m.default;
mermaid.initialize({
startOnLoad: false,
theme: "dark",
themeVariables: {
primaryColor: "#6366f1",
primaryTextColor: "#e2e8f0",
primaryBorderColor: "#4f46e5",
lineColor: "#64748b",
secondaryColor: "#1e293b",
tertiaryColor: "#0f172a",
fontFamily: "inherit",
fontSize: "13px",
},
});
return mermaid;
});
}
return mermaidReady;
}

function MermaidBlock({ code }: { code: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const uniqueId = useId().replace(/:/g, "_");
const [error, setError] = useState<string | null>(null);
const [svgHtml, setSvgHtml] = useState<string | null>(null);
const [zoomed, setZoomed] = useState(false);

useEffect(() => {
let cancelled = false;
loadMermaid()
.then(async (mermaid) => {
if (cancelled) return;
try {
const { svg } = await mermaid.render(`mermaid${uniqueId}`, code.trim());
if (!cancelled) {
setSvgHtml(svg);
if (containerRef.current) containerRef.current.innerHTML = svg;
}
} catch {
if (!cancelled) setError("Diagram render failed");
}
})
.catch(() => {
if (!cancelled) setError("Failed to load Mermaid");
});
return () => { cancelled = true; };
}, [code, uniqueId]);

if (error) {
return (
<pre className="my-2 overflow-x-auto rounded-xl bg-accent/60 p-3 font-mono text-xs leading-5">
<code>{code}</code>
</pre>
);
}

return (
<>
<div
ref={containerRef}
onClick={() => svgHtml && setZoomed(true)}
className="group my-2 flex cursor-zoom-in justify-center overflow-x-auto rounded-xl bg-accent/30 p-4 transition-colors hover:bg-accent/50 [&_svg]:max-w-full"
/>
<AnimatePresence>
{zoomed && svgHtml && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="fixed inset-0 z-[999] flex cursor-zoom-out items-center justify-center bg-black/70 backdrop-blur-sm"
onClick={() => setZoomed(false)}
>
<m.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="max-h-[90vh] max-w-[90vw] overflow-auto rounded-2xl bg-card/95 p-6 shadow-2xl ring-1 ring-white/10 [&_svg]:max-h-[80vh] [&_svg]:max-w-full"
onClick={(e) => e.stopPropagation()}
dangerouslySetInnerHTML={{ __html: svgHtml }}
/>
</m.div>
)}
</AnimatePresence>
</>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Mermaid.js sanitize SVG output to prevent XSS attacks?

💡 Result:

No, Mermaid.js does not fully sanitize its SVG output to prevent all XSS attacks, though it implements measures like DOMPurify and security levels to mitigate risks. Recent CVEs (e.g., CVE-2025-54881, CVE-2025-54880) demonstrate that vulnerabilities allowing XSS via unsanitized user input in labels and iconText existed in versions before 11.10.0, where malicious HTML was injected into SVG elements and executed via innerHTML sinks during rendering. Official docs confirm default "strict" securityLevel encodes HTML tags in text and disables click handlers, with DOMPurify used by default (configurable). However, apps using mermaid.render and inserting raw SVG via dangerouslySetInnerHTML remain vulnerable if user-controlled diagrams bypass protections (e.g., via %%{init: {"securityLevel": "loose"}}%%). Always use latest versions, strict config, and sanitize/escape SVG before DOM insertion.

Citations:


Sanitize SVG output before rendering with dangerouslySetInnerHTML.

The dangerouslySetInnerHTML at line 246 poses a real XSS risk. While Mermaid implements security measures, recent CVEs (CVE-2025-54881, CVE-2025-54880) demonstrate that versions before 11.10.0 are vulnerable to XSS via unsanitized user input in labels and diagram syntax (e.g., iconText fields). Since the input code originates from message content, attackers could inject malicious diagram syntax to execute arbitrary scripts.

Mermaid's default "strict" securityLevel provides some protection but is insufficient when paired with dangerouslySetInnerHTML, which bypasses React's built-in XSS defenses. Additional measures required:

  • Upgrade Mermaid to ≥11.10.0 and explicitly configure securityLevel: "strict"
  • Sanitize the SVG output using DOMPurify before passing to dangerouslySetInnerHTML, or alternatively, use innerHTML with proper escaping on a detached container
🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 245-245: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/chat/chat-message-bubble.tsx` around lines 156 - 253, The
MermaidBlock is rendering untrusted SVG via dangerouslySetInnerHTML and must be
hardened: upgrade the imported MERMAID_CDN to a version >=11.10.0, set
mermaid.initialize({ ..., securityLevel: "strict" }) inside loadMermaid, and
sanitize any SVG before it’s injected (where MermaidBlock uses
dangerouslySetInnerHTML and containerRef.innerHTML) by passing the generated svg
through a sanitizer like DOMPurify (or sanitizing on a detached DOM) and only
then calling setSvgHtml / innerHTML; keep the rest of the flow (loadMermaid,
mermaid.render, svgHtml, containerRef) intact.

Comment on lines +37 to +69
// OpenAI
{ value: "o3", label: "o3", group: "OpenAI", thinking: true },
{ value: "o3-mini", label: "o3 Mini", group: "OpenAI", thinking: true },
{ value: "o4-mini", label: "o4 Mini", group: "OpenAI", thinking: true },
{ value: "gpt-5.4", label: "GPT-5.4", group: "OpenAI" },
{ value: "gpt-5.4-pro", label: "GPT-5.4 Pro", group: "OpenAI" },
{ value: "gpt-5.2-instant", label: "GPT-5.2 Instant", group: "OpenAI" },
{ value: "gpt-4o", label: "GPT-4o", group: "OpenAI" },
{ value: "gpt-4o-mini", label: "GPT-4o Mini", group: "OpenAI" },
// Anthropic
{ value: "claude-opus-4-6", label: "Claude Opus 4.6", group: "Anthropic" },
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", group: "Anthropic" },
{ value: "claude-sonnet-4-5-20250514", label: "Claude Sonnet 4.5", group: "Anthropic", thinking: true },
{ value: "claude-haiku-4-5", label: "Claude Haiku 4.5", group: "Anthropic" },
// DeepSeek
{ value: "deepseek-chat", label: "DeepSeek V3", group: "DeepSeek" },
{ value: "deepseek-reasoner", label: "DeepSeek R1", group: "DeepSeek", thinking: true },
// Google
{ value: "gemini-2.5-pro-preview-05-06", label: "Gemini 2.5 Pro", group: "Google", thinking: true },
{ value: "gemini-2.5-flash-preview-05-20",label: "Gemini 2.5 Flash", group: "Google", thinking: true },
{ value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", group: "Google" },
{ value: "gemini-3.1-flash-lite-preview", label: "Gemini 3.1 Flash Lite", group: "Google" },
// Qwen
{ value: "qwen-max", label: "Qwen Max", group: "Qwen" },
{ value: "qwen-plus", label: "Qwen Plus", group: "Qwen" },
{ value: "qwen-turbo", label: "Qwen Turbo", group: "Qwen" },
{ value: "qwq-plus", label: "QwQ Plus", group: "Qwen", thinking: true },
// Meta
{ value: "llama-4-maverick", label: "Llama 4 Maverick", group: "Meta" },
{ value: "llama-4-scout", label: "Llama 4 Scout", group: "Meta" },
// Mistral
{ value: "mistral-large-latest", label: "Mistral Large", group: "Mistral" },
{ value: "mistral-small-latest", label: "Mistral Small", group: "Mistral" },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What are the current OpenAI model names available via API in 2026?

💡 Result:

The current OpenAI models available via API as of March 2026, based on official documentation from OpenAI's model catalog pages, include the following key models across categories (note: the full list is dynamic and can be retrieved via the /v1/models endpoint; these are the prominently featured ones): Frontier models: - gpt-5.4 - gpt-5.4 pro - gpt-5.4 mini - gpt-5.4 nano - gpt-5 mini Other GPT models: - GPT-4.1 - GPT-5.2 - GPT-5.1 - GPT-5.2 pro - GPT-5 pro Image models: - GPT Image 1.5 (likely gpt-image-1.5) - chatgpt-image-latest - gpt-image-1-mini Video models: - Sora 2 - Sora 2 Pro Realtime & audio models: - gpt-realtime-1.5 - gpt-realtime - gpt-realtime-mini - gpt-audio-1.5 Open-weight models: - gpt-oss-120b - gpt-oss-20b Specialized mentions: - o3 (possibly o3-mini or similar reasoning model) - o4-mini-deep-research To get the complete, real-time list, call GET https://api.openai.com/v1/models with your API key.

Citations:


🌐 Web query:

What are the current Anthropic Claude API model names available in 2026?

💡 Result:

The current Anthropic Claude API models available as of March 18, 2026, include both the latest recommended models and older still-available ones. Note that claude-3-haiku-20240307 is deprecated and scheduled for retirement on April 20, 2026. Latest models (recommended): - claude-opus-4-6 - claude-sonnet-4-6 - claude-haiku-4-5-20251001 (alias: claude-haiku-4-5) Still available (consider migrating): - claude-sonnet-4-5-20250929 (alias: claude-sonnet-4-5) - claude-opus-4-5-20251101 (alias: claude-opus-4-5) - claude-opus-4-1-20250805 (alias: claude-opus-4-1) - claude-sonnet-4-20250514 (alias: claude-sonnet-4-0) - claude-opus-4-20250514 (alias: claude-opus-4-0) - claude-3-haiku-20240307 (deprecated) These can be queried programmatically via the /v1/models endpoint.

Citations:


🌐 Web query:

What are the current Google Gemini API model names available in 2026?

💡 Result:

The current Google Gemini API model names available as of March 2026, based on official documentation, include both stable and preview models. Stable models are recommended for production, while previews offer cutting-edge features but may change. Stable models: - gemini-2.5-pro - gemini-2.5-flash - gemini-2.5-flash-lite Preview models (actively promoted and available via API): - gemini-3.1-pro-preview - gemini-3.1-pro-preview-customtools - gemini-3-flash (or gemini-3.0-flash-preview) - gemini-2.5-flash-live (preview) - gemini-3.1-flash-lite (preview) Deprecated models to migrate from: - gemini-2.0-flash - gemini-2.0-flash-lite - gemini-3-pro (shut down) To get the exact list programmatically, use the Gemini API's models.list endpoint: GET https://generativelanguage.googleapis.com/v1beta/models?key=YOUR_API_KEY. Model names follow patterns like gemini-X.Y-flash, gemini-X.Y-pro, with suffixes -preview-MM-YYYY for previews or -latest aliases.

Citations:


🌐 Web query:

What are the current DeepSeek API model names available in 2026?

💡 Result:

The current DeepSeek API models available as of March 2026 are deepseek-chat and deepseek-reasoner, both based on DeepSeek-V3.2. deepseek-chat is the non-thinking mode for general tasks (128K context, supports JSON output, tool calls, FIM beta), while deepseek-reasoner is the thinking mode for advanced reasoning (128K context, supports JSON output and tool calls, higher max output). These are the primary models listed in official documentation and used in API examples. To get the real-time list, call GET /models on https://api.deepseek.com.

Citations:


🌐 Web query:

What are the current Qwen API model names available in 2026?

💡 Result:

The current Qwen API models available through Alibaba Cloud Model Studio (DashScope API) in 2026 include the following primary commercial and specialized models: Commercial general-purpose LLMs: - qwen-max - qwen-plus (upgraded to Qwen3.5, also qwen3.5-plus) - qwen-flash Specialized models: - qwen-vl-plus, qwen-vl-max (vision-language) - qwen3-coder-plus, qwen3-coder-flash (coding) - qwen-math-plus, qwen-math-turbo (math) - qwen-mt-plus, qwen-mt-turbo, qwen-mt-flash (translation) - qwen-doc-turbo (document processing) - qwen-deep-research (research) Open-source variants like qwen3.5-397b-a17b, qwen3, qwen2.5 are also accessible via API. Variants with -latest or dated snapshots (e.g., qwen-plus-latest, qwen-turbo-2024-09-19) point to the most recent versions. Access requires an Alibaba Cloud API key via DashScope endpoints, with OpenAI-compatible interfaces available.

Citations:


🌐 Web query:

What are the current Meta Llama API model names available in 2026?

💡 Result:

The current Meta Llama API (at llama.meta.com or llama.developer.meta.com, currently in preview/waitlist as of 2026) supports the latest Llama models, primarily from the Llama 4 and Llama 3 families. Official documentation highlights availability of: - Llama 4 Maverick (natively multimodal, e.g., meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8 in partner APIs) - Llama 4 Scout (natively multimodal, single H100 efficient, 10M context) Additional models mentioned as available via Llama API include previously unreleased Llama 3.3 8B, alongside Llama 3.1 (8B, 70B, 405B), Llama 3.2 (1B, 3B, 11B Vision, 90B Vision), and Llama 3.3 70B. Model IDs follow patterns like "llama-4-maverick", "llama-4-scout", or Hugging Face-style "meta-llama/Llama-4-Maverick-17B-128E-Instruct". Exact list can be queried via the API's /v1/models endpoint. Llama API is OpenAI-compatible and supports chat completions, image understanding, tool calling, etc. Partners like Together AI and AWS Bedrock host these with specific IDs.

Citations:


🌐 Web query:

What are the current Mistral API model names available in 2026?

💡 Result:

The current Mistral API models available as of March 2026, based on official Mistral AI documentation, include the following key models accessible via their La Plateforme API (use /v1/models endpoint for user-specific list). Featured and frontier models: - mistral-large-3 (or mistral-large-latest): State-of-the-art open-weight general-purpose multimodal model (v25.12). - devstral-2 (or devstral-latest): Frontier code agents model for software engineering tasks. - mistral-medium-3.1 (or mistral-medium-latest): Frontier-class multimodal model (v25.08, released Aug 2025). - mistral-small-4 (or mistral-small-latest): Hybrid instruct, reasoning, coding model (v26.03). - mistral-small-3.2: Small model update (v25.06, June 2025). - ministral-3-14b (or ministral-latest): Text and vision capabilities (v25.12). - magistral-medium-1.2 (or magistral-medium-latest): Multimodal reasoning model (v25.09). - magistral-small-1.2 (or magistral-small-latest): Small reasoning model with vision (v25.09). Specialized models: - mistral-moderation-latest: Moderation with 128k context. - ocr-3 (or similar): OCR for Document AI. - voxtral-mini-transcribe-realtime: Audio transcription. - mistral-embed: Embeddings. - Others: devstral-small-2, leanstral, mistral-small-creative, pixtral-large, mistral-nemo-12b, mistral-large-2.1. Legacy/deprecated models (some retired by early 2026, e.g., older magistral 1.1 versions retired Nov 2025): Use alternatives like *-latest aliases. "Latest" aliases point to current versions (e.g., mistral-small-latest -> mistral-small-4 v26.03). All premier models are API-available unless specified open-only; confirm via API key's /v1/models list.

Citations:


Update model identifiers to match current provider APIs.

Several model identifiers do not match current provider API documentation as of March 2026:

  • Anthropic: claude-sonnet-4-5-20250514 should be claude-sonnet-4-5-20250929 (latest snapshot per Anthropic docs)
  • Google: gemini-2.5-pro-preview-05-06 and gemini-2.5-flash-preview-05-20 should use stable versions (gemini-2.5-pro and gemini-2.5-flash); the preview-dated IDs appear outdated
  • Qwen: qwen-turbo and qwq-plus are not listed in current Qwen API documentation; verify these exist or use confirmed models (qwen-max, qwen-plus, qwen-flash)
  • OpenAI: gpt-5.2-instant, gpt-4o, and gpt-4o-mini do not appear in current OpenAI API documentation; confirm availability or remove

Confirmed correct:

  • OpenAI: o3, o3-mini, gpt-5.4, gpt-5.4-pro
  • Anthropic: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5
  • Google: gemini-3.1-pro-preview, gemini-3.1-flash-lite-preview
  • DeepSeek, Meta, Mistral: all listed models confirmed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/constants.ts` around lines 37 - 69, The model identifier list
contains outdated or invalid provider model IDs; update the constants array
entries to match current provider APIs by: replace Anthropic
"claude-sonnet-4-5-20250514" with "claude-sonnet-4-5-20250929"; replace Google
"gemini-2.5-pro-preview-05-06" and "gemini-2.5-flash-preview-05-20" with
"gemini-2.5-pro" and "gemini-2.5-flash"; verify and remove or replace OpenAI
entries "gpt-5.2-instant", "gpt-4o", and "gpt-4o-mini" if not supported (keep
only confirmed OpenAI ones: "o3", "o3-mini", "gpt-5.4", "gpt-5.4-pro"); for
Qwen, ensure only supported IDs ("qwen-max", "qwen-plus", "qwen-flash") remain
and remove "qwen-turbo" and "qwq-plus" if they are invalid; update the array
entries by editing the constant that contains these objects (look for the
objects with value keys matching the listed model strings) so labels and group
fields remain consistent with the new values.

Comment on lines +171 to +193
} catch {
// aborted or network error; non-fatal for background tasks
} finally {
if (_abortController === ac) _abortController = null;

// If the abort signal fired (e.g. page refresh or manual cancel),
// do NOT mark progress as "done" — the backend task is still running.
// Just set isActive to false so the persist layer keeps taskId for reconnect.
if (ac.signal.aborted) {
set({ isActive: false });
return;
}

const current = get();
if (current.progress && current.progress.status !== "done") {
set({
progress: { ...current.progress, status: "done" },
isActive: false,
});
} else {
set({ isActive: false });
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsafe return inside finally block."It is generally a bad idea to use control flow statements (return, throw, break, continue) in the finally block because they can override the effect of previously executed control flow statements." The return on line 181 inside the finally block can suppress exceptions or override other return values, making the control flow confusing.

In this case, if subscribeDeepResearch throws an error that isn't caught (the catch block is empty), the return in finally would silently swallow that exception. Restructure to check the abort condition before the cleanup logic instead.

Proposed fix
   try {
     await subscribeDeepResearch(
       taskId,
       (event) => {
         eventIdx++;
         const current = get();
         if (!current.progress) return;
         const next = processEvent(event, current.progress, reportTokens);
         set({ progress: next, reportTokens: reportTokens.value, eventIndex: eventIdx });
       },
       ac.signal,
       fromIndex,
     );
-  } catch {
-    // aborted or network error; non-fatal for background tasks
+  } catch (err) {
+    // If aborted, we'll handle state in the finally block
+    // Only log unexpected errors
+    if (!ac.signal.aborted && err instanceof Error) {
+      console.warn("Deep research subscription error:", err.message);
+    }
   } finally {
     if (_abortController === ac) _abortController = null;
-
-    // If the abort signal fired (e.g. page refresh or manual cancel),
-    // do NOT mark progress as "done" — the backend task is still running.
-    // Just set isActive to false so the persist layer keeps taskId for reconnect.
-    if (ac.signal.aborted) {
-      set({ isActive: false });
-      return;
-    }
-
-    const current = get();
-    if (current.progress && current.progress.status !== "done") {
-      set({
-        progress: { ...current.progress, status: "done" },
-        isActive: false,
-      });
-    } else {
-      set({ isActive: false });
-    }
   }
+
+  // Handle state updates after try/catch/finally completes
+  // If the abort signal fired, keep taskId for reconnect
+  if (ac.signal.aborted) {
+    set({ isActive: false });
+    return;
+  }
+
+  const current = get();
+  if (current.progress && current.progress.status !== "done") {
+    set({
+      progress: { ...current.progress, status: "done" },
+      isActive: false,
+    });
+  } else {
+    set({ isActive: false });
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
// aborted or network error; non-fatal for background tasks
} finally {
if (_abortController === ac) _abortController = null;
// If the abort signal fired (e.g. page refresh or manual cancel),
// do NOT mark progress as "done" — the backend task is still running.
// Just set isActive to false so the persist layer keeps taskId for reconnect.
if (ac.signal.aborted) {
set({ isActive: false });
return;
}
const current = get();
if (current.progress && current.progress.status !== "done") {
set({
progress: { ...current.progress, status: "done" },
isActive: false,
});
} else {
set({ isActive: false });
}
}
try {
await subscribeDeepResearch(
taskId,
(event) => {
eventIdx++;
const current = get();
if (!current.progress) return;
const next = processEvent(event, current.progress, reportTokens);
set({ progress: next, reportTokens: reportTokens.value, eventIndex: eventIdx });
},
ac.signal,
fromIndex,
);
} catch (err) {
// If aborted, we'll handle state in the finally block
// Only log unexpected errors
if (!ac.signal.aborted && err instanceof Error) {
console.warn("Deep research subscription error:", err.message);
}
} finally {
if (_abortController === ac) _abortController = null;
}
// Handle state updates after try/catch/finally completes
// If the abort signal fired, keep taskId for reconnect
if (ac.signal.aborted) {
set({ isActive: false });
return;
}
const current = get();
if (current.progress && current.progress.status !== "done") {
set({
progress: { ...current.progress, status: "done" },
isActive: false,
});
} else {
set({ isActive: false });
}
}
🧰 Tools
🪛 Biome (2.4.7)

[error] 181-181: Unsafe usage of 'return'.

(lint/correctness/noUnsafeFinally)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/use-deep-research-store.ts` around lines 171 - 193, The finally
block contains a return when ac.signal.aborted which can swallow errors;
refactor so no control-flow (return) happens inside finally. Capture the abort
state into a local variable (e.g. const wasAborted = ac.signal.aborted) before
entering the finally/cleanup, or set a local flag inside the try/catch, then in
the finally use that flag to decide behavior without returning from inside
finally. Update logic around _abortController, ac, get(), and set() so you only
early-return from the outer function (not inside finally) when wasAborted is
true, and otherwise perform the progress/status updates and set isActive
consistently.

LinMoQC added 2 commits March 19, 2026 09:48
- 后端:Message 增加 reasoning 字段,migration 028;conversation 路由与 conversation_service 调整
- 前端:ChatToolbar 抽离为 chat-toolbar.tsx,chat-tools 工具函数,chat-view/use-deep-research/store 与 home-qa 联调
- i18n 与 chat-api schema 更新

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (2)
api/app/services/conversation_service.py (1)

32-35: ⚠️ Potential issue | 🟠 Major

Retain spawned background tasks until they complete.

These asyncio.create_task(...) calls still drop their Task objects, including the diary sync path. Keep them in an instance set and discard them in a done callback so they stay referenced for the full run.

Suggested fix
 class ConversationService:
     """Conversation domain business logic."""
 
     def __init__(self, db: AsyncSession, user_id: UUID):
         self.db = db
         self.user_id = user_id
+        self._background_tasks: set[asyncio.Task[None]] = set()
@@
-        asyncio.create_task(self._extract_memories_safe(conversation_id))
-        asyncio.create_task(self._reflect_safe(conversation_id, scene, user_memories))
-        asyncio.create_task(self._compress_safe(conversation_id))
-        asyncio.create_task(self._maybe_flush_diary(conversation_id))
+        for coro in (
+            self._extract_memories_safe(conversation_id),
+            self._reflect_safe(conversation_id, scene, user_memories),
+            self._compress_safe(conversation_id),
+            self._maybe_flush_diary(conversation_id),
+        ):
+            task = asyncio.create_task(coro)
+            self._background_tasks.add(task)
+            task.add_done_callback(self._background_tasks.discard)
@@
         from app.config import settings
         if settings.memory_evaluation_sample_rate > 0 and random.random() < settings.memory_evaluation_sample_rate:
-            asyncio.create_task(self._evaluate_safe(conversation_id))
+            task = asyncio.create_task(self._evaluate_safe(conversation_id))
+            self._background_tasks.add(task)
+            task.add_done_callback(self._background_tasks.discard)
@@
-                    asyncio.create_task(self._sync_diary_safe(today))
+                    task = asyncio.create_task(self._sync_diary_safe(today))
+                    self._background_tasks.add(task)
+                    task.add_done_callback(self._background_tasks.discard)

Also applies to: 292-299, 349-352

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/conversation_service.py` around lines 32 - 35, The created
background tasks are being dropped immediately; initialize an instance set
(e.g., self._tasks = set()) in ConversationService.__init__ and, at every
asyncio.create_task(...) call (including the diary sync path), add the returned
Task to that set and attach a done callback to remove it
(task.add_done_callback(self._tasks.discard)) so the Task stays referenced until
completion; ensure all create_task sites use this pattern so spawned tasks are
retained and cleaned up when finished.
web/src/store/use-deep-research-store.ts (1)

175-193: ⚠️ Potential issue | 🟠 Major

Remove control flow from finally.

The returns here are redundant and still trip lint/correctness/noUnsafeFinally. Keep finally cleanup-only so it can't mask future throws/returns, and this should also clear the current Biome error.

🧹 Minimal cleanup
   } finally {
     if (_abortController === ac) _abortController = null;
-
-    // If the abort signal fired (e.g. page refresh or manual cancel),
-    // do NOT mark progress as "done" — the backend task is still running.
-    // Just set isActive to false so the persist layer keeps taskId for reconnect.
-    if (ac.signal.aborted) {
-      set({ isActive: false });
-      return;
-    }
-
-    // Non-abort exits can happen on transient SSE/network interruptions.
-    // Only treat as completed when we actually received a "done" event.
-    if (sawDoneEvent) {
-      set({ isActive: false });
-      return;
-    }
     set({ isActive: false });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/use-deep-research-store.ts` around lines 175 - 193, The finally
block currently performs control flow via multiple returns (checking
_abortController, ac.signal.aborted and sawDoneEvent) which triggers
lint/correctness/noUnsafeFinally; change the finally to be cleanup-only: remove
all return statements and only perform state cleanup (clear _abortController
when _abortController === ac and call set({ isActive: false }) as needed) so no
early returns occur inside the finally. Keep the logic that differentiates abort
vs non-abort and sawDoneEvent by recording any flags (e.g., examine
ac.signal.aborted and sawDoneEvent into local variables before the try/finally
or set them earlier) and let the surrounding function handle any post-finally
branching/returns instead of returning inside finally; ensure references to
_abortController, ac, ac.signal.aborted, sawDoneEvent and set remain consistent.
🧹 Nitpick comments (8)
api/app/agents/ingestion.py (1)

1-3: LGTM — clean backward-compatibility shim.

The wildcard re-export with appropriate noqa suppression is a valid pattern for maintaining legacy import paths during a refactor. The docstring clearly communicates the module's purpose.

Optional: If the goal is to eventually retire this legacy path, consider emitting a DeprecationWarning so consumers are encouraged to migrate:

💡 Optional deprecation warning
 """Backward-compatibility shim for legacy import path `app.agents.ingestion`."""
+
+import warnings
+
+warnings.warn(
+    "Importing from 'app.agents.ingestion' is deprecated. "
+    "Use 'app.agents.rag.ingestion' instead.",
+    DeprecationWarning,
+    stacklevel=2,
+)

 from app.agents.rag.ingestion import *  # noqa: F401,F403

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/ingestion.py` around lines 1 - 3, Add a runtime deprecation
notice to the shim module app.agents.ingestion by emitting a warnings.warn call
at module import time (right after the module docstring and before or after the
existing from app.agents.rag.ingestion import *), using the DeprecationWarning
category and a short message that the legacy import path is deprecated and
points consumers to app.agents.rag.ingestion; ensure the call is at module level
so it runs on import and use the warnings module (warnings.warn,
DeprecationWarning) to avoid altering existing behavior.
web/src/components/chat-input/chat-toolbar.tsx (3)

253-267: Unnecessary fragment wrapper around single element.

The fragment <>...</> at lines 253-267 wraps only a single <button>. Fragments are only needed when returning multiple siblings. This pattern also appears at lines 269-336 and 338-425.

♻️ Remove unnecessary fragments
               {isThinkingModel && (
-                <>
                   <button
                     type="button"
                     ...
                   </button>
-                </>
               )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chat-input/chat-toolbar.tsx` around lines 253 - 267,
Remove the unnecessary React fragment wrapping the single <button> in the
ChatToolbar component: when rendering the thinking-mode block (guarded by
isThinkingModel) delete the surrounding <>...</> so the button is returned
directly; update the same pattern in the other similar blocks in this file (the
other places that wrap single elements with fragments) — locate the JSX guarded
by isThinkingModel (and the blocks that use primaryItemClass, onToggleThinking,
closeMenu, thinkingEnabled, and Lightbulb) and remove the extraneous fragments
so only the single element is rendered.

136-138: Potential infinite re-render if onMenuOpenChange is not memoized.

If the parent passes an inline arrow function for onMenuOpenChange, this effect will fire on every render, potentially causing a render loop or excessive calls. Consider either:

  1. Documenting that the callback should be stable (memoized with useCallback)
  2. Using a ref to store the callback to avoid the dependency
♻️ Ref-based approach to stabilize callback
+  const onMenuOpenChangeRef = useRef(onMenuOpenChange);
+  useEffect(() => {
+    onMenuOpenChangeRef.current = onMenuOpenChange;
+  });
+
   useEffect(() => {
-    onMenuOpenChange?.(menuOpen);
-  }, [menuOpen, onMenuOpenChange]);
+    onMenuOpenChangeRef.current?.(menuOpen);
+  }, [menuOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chat-input/chat-toolbar.tsx` around lines 136 - 138, The
effect in chat-toolbar.tsx using useEffect(() => onMenuOpenChange?.(menuOpen),
[menuOpen, onMenuOpenChange]) can trigger repeated renders if onMenuOpenChange
is passed inline; change to a ref-stabilized callback: store the latest
onMenuOpenChange in a ref (e.g., callbackRef.current = onMenuOpenChange in an
effect) and then have the effect that watches only menuOpen call
callbackRef.current?.(menuOpen); alternatively document that onMenuOpenChange
must be memoized with useCallback — update references around useEffect,
onMenuOpenChange, and menuOpen accordingly.

140-197: Consider extracting repeated outside-click logic into a custom hook.

There are four nearly identical useEffect blocks handling outside-click dismissal for menuRef, drRef, toolsRef, and notebooksRef. A small custom hook like useClickOutside(ref, isOpen, onClose) would reduce duplication and improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chat-input/chat-toolbar.tsx` around lines 140 - 197,
Duplicate outside-click useEffect logic across the menu, dr, tools, and
notebooks blocks should be extracted into a small reusable hook; create a hook
like useClickOutside(ref, isOpen, onClose) that attaches/removes the "mousedown"
listener only when isOpen is true and calls onClose when clicks occur outside
(use the same MouseEvent -> Node check used in the current handlers), then
replace the four useEffect blocks by calling useClickOutside(menuRef, menuOpen,
() => setMenuOpen(false)), useClickOutside(drRef, drDropdownOpen, () =>
setDrDropdownOpen(false)), useClickOutside(toolsRef, toolsMenuOpen, () =>
setToolsMenuOpen(false)), and useClickOutside(notebooksRef, notebooksMenuOpen,
() => setNotebooksMenuOpen(false)); keep the existing resize logic and panel
layout updates (getSecondaryPanelLayout, setToolsPanelLayout,
setNotebooksPanelLayout) in their respective effects or combine them with the
new hook only if you also support an optional resize handler parameter to
preserve current behavior and proper cleanup.
api/app/agents/instructions.py (1)

1-3: Stabilize the shim's public export surface by defining __all__.

The core module api/app/agents/core/instructions.py does not define __all__, and this shim re-exports its contents with a bare wildcard import. This creates an implicit, unversioned API surface that could drift if new symbols are added to the core module. For a backward-compatibility layer, pinning an explicit __all__ ensures the legacy API contract remains intentional.

The core module currently exposes 7 public symbols: CallLLMInstruction, CallToolsInstruction, CallRAGInstruction, StreamAnswerInstruction, CompressContextInstruction, RequestHumanApprovalInstruction, and FinishInstruction. Add an explicit __all__ to this shim to mirror that surface:

♻️ Suggested change
 """Backward-compatibility shim for legacy import path `app.agents.instructions`."""
 
+import app.agents.core.instructions as _instructions
+
 from app.agents.core.instructions import *  # noqa: F401,F403
+__all__ = getattr(_instructions, "__all__", [name for name in globals() if not name.startswith("_")])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/instructions.py` around lines 1 - 3, The shim currently
re-exports everything from app.agents.core.instructions via a bare wildcard
which creates an unstable public surface; fix it by defining an explicit __all__
in this file that pins the legacy API to the seven public symbols:
CallLLMInstruction, CallToolsInstruction, CallRAGInstruction,
StreamAnswerInstruction, CompressContextInstruction,
RequestHumanApprovalInstruction, and FinishInstruction (keep the existing from
app.agents.core.instructions import * but immediately add __all__ = [...the
seven names as strings...] to stabilize the exported surface).
api/app/domains/conversation/router.py (1)

27-28: Add ownership regression coverage at the new service boundary.

The router no longer scopes notebooks/conversations itself; every read/write/stream/delete path now trusts ConversationService to enforce current_user.id. A small set of cross-user tests here would catch a serious authorization regression early.

Also applies to: 39-40, 54-55, 66-67, 81-84, 94-103, 116-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/conversation/router.py` around lines 27 - 28, The router now
delegates ownership enforcement to ConversationService which creates a
regression risk; add unit/integration tests that exercise each router endpoint
(list_by_notebook, read, create/append/stream, delete) to assert cross-user
access is denied when the current_user.id does not match the
notebook/conversation owner and allowed when it does, targeting the
ConversationService boundary (instantiate ConversationService(db,
current_user.id) or mock it) and calling the router handlers used on lines
around the listed ranges to verify authorization is enforced and appropriate
error/status responses are returned.
web/src/features/chat/use-deep-research.ts (1)

213-216: Don't keep a silent no-op setter in the hook contract.

setDrProgress is still typed as a real Dispatch, but it never updates anything. That makes stale callers fail quietly. Either remove it once callers are migrated, or make the deprecated path noisy in development so misuse is visible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/features/chat/use-deep-research.ts` around lines 213 - 216,
setDrProgress is currently a typed Dispatch but implemented as a silent no-op,
which can hide stale-caller bugs; update the useDeepResearch hook so this
deprecated setter is either removed from the public contract or replaced with a
noisy dev-only setter that warns (or throws in strict mode) when called. Locate
the symbol setDrProgress inside the useDeepResearch hook and either (a) remove
it and update callers, or (b) implement it to call the real state setter or to
console.warn with contextual info (including function name and call-site hint)
when process.env.NODE_ENV !== 'production', so misuse becomes visible during
development.
web/src/store/use-deep-research-store.ts (1)

260-270: Either resume from eventIndex or delete the cursor.

The store tracks eventIndex as a reconnect cursor, but reconnect() always resets state and resubscribes from 0. That means every reconnect still replays the whole stream. Either keep enough partial state to resume from the stored index, or remove the cursor/comment so the API matches the behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/use-deep-research-store.ts` around lines 260 - 270, The code
resets eventIndex to 0 and always calls subscribeTo(taskId, get, set, 0), which
prevents resuming; change reconnect() so it preserves and uses the stored
cursor: set eventIndex to status.eventIndex (or read the existing store
eventIndex if present) instead of always 0 and pass that value into subscribeTo
(i.e., subscribeTo(taskId, get, set, eventIndex)). Update the set call that
creates initial state (where eventIndex is currently hardcoded) to use the
preserved cursor, and ensure makeInitialProgress(status.mode) remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/app/agents/research/task_manager.py`:
- Around line 65-70: The in-memory _buffers dict (and its use in
get_buffer/TaskBuffer) is single-worker only; replace it with a shared store and
optional pub/sub so buffers are visible across workers: remove the module-level
_buffers and implement a Redis (or other shared datastore)-backed buffer
registry used by get_buffer(TaskBuffer) and any create/delete/update_buffer
functions, persist TaskBuffer state (serialize/deserialize) and publish changes
on a channel so other workers can subscribe and update local caches;
alternatively, if you cannot introduce shared storage, document and enforce
sticky routing for endpoints that create/use TaskBuffer (e.g., POST
/ai/deep-research and GET /events) so both requests always route to the same
worker. Ensure you update function signatures and any callers of get_buffer and
buffer-mutating functions to use the shared-store accessors.

In `@api/app/domains/ai/routers/research.py`:
- Around line 134-165: The completed-task replay branch (inside the buf is None
and task.status == "done" block) ignores the client's resume offset, causing
duplicate events on reconnect; update the replay_done() generator to accept and
honor the ?from=N offset (read the request's `from` query param) and skip
yielding events until the stream position reaches that offset when emitting
plan, learning, writing, report_complete, done, and deliverable events; locate
the replay logic in replay_done, use task.timeline_json and any existing event
ordering logic to compute each event's sequence index, advance/seek past events
lower than `from`, then yield only remaining events before sending the final
"data: [DONE]\n\n" via StreamingResponse.
- Around line 24-25: The ResearchTask DB row is left as status="running" while
execution is launched with an untracked asyncio.create_task(), so on restart the
row is orphaned; fix by keeping a reference to the started Task in an in-memory
registry (e.g., a module-level dict like running_tasks keyed by ResearchTask.id)
instead of fire-and-forget, attach a done callback to the Task that updates the
ResearchTask DB row to a terminal status and removes the Task from
running_tasks, and ensure creation flow (where ResearchTask is committed as
status="running") stores the returned Task into running_tasks; for durability
beyond restarts migrate execution to a queue, but for the minimal fix modify
places that call asyncio.create_task(...) to save the Task and register a
callback that calls the existing DB update logic for ResearchTask.

In `@api/app/services/conversation_service.py`:
- Around line 276-282: The current fallback selects messages ordered ascending
and limits to 20, returning the oldest 20; instead, change the query in
conversation history retrieval to order by Message.created_at.desc() (newest
first) with .limit(20), then take result.scalars().all(), reverse that list to
restore chronological order, and finally return [{"role": m.role, "content":
m.content} for m in reversed_list]; update the code around the select(Message)
... .order_by(...) and the result.scalars().all() handling so the agent sees the
most recent window of messages in chronological order.
- Around line 228-241: The flushed assistant Message is not yet visible to new
AsyncSessionLocal sessions because the outer transaction isn't committed before
_dispatch_post_chat_tasks spawns background tasks; change the flow to await
self.db.commit() (or otherwise commit the outer transaction/context that
includes self.db.add/await self.db.flush) before calling
_dispatch_post_chat_tasks so background workers see the new message, and update
the asyncio.create_task usages referenced in _dispatch_post_chat_tasks (and the
calls at the noted lines) to capture and manage the returned Task objects (store
them in variables, append to a list, or return them) instead of dropping the
handles so task lifetimes are explicit.

In `@web/src/components/home/home-qa.tsx`:
- Line 108: Replace the hardcoded Chinese label clearNotebookLabel="清除笔记本限制" in
the HomeQA component with a translated string (e.g.,
clearNotebookLabel={t('clearNotebookFilter')}) using the existing i18n helper
(t) or useTranslation hook used elsewhere in this component; then add the
"clearNotebookFilter" key to the locale message files used by the app with
values "Clear notebook filter" (English) and "清除笔记本限制" (Chinese) so the label is
served from translations rather than hardcoded text.

In `@web/src/store/use-deep-research-store.ts`:
- Around line 210-232: start() sets optimistic active state before awaiting
createDeepResearch(), so if createDeepResearch() rejects the store remains
stale; wrap the call to createDeepResearch(query, { notebookId, mode }) in a
try/catch and on error rollback the optimistic fields (reset isActive to false,
clear taskId, conversationId, revert progress to null or previous state from
get(), and ensure eventIndex/reportTokens are cleared) and rethrow or handle the
error; keep subscribeTo(taskId, get, set) only after successful creation and
setting of taskId/conversationId so you never subscribe when creation fails.

---

Duplicate comments:
In `@api/app/services/conversation_service.py`:
- Around line 32-35: The created background tasks are being dropped immediately;
initialize an instance set (e.g., self._tasks = set()) in
ConversationService.__init__ and, at every asyncio.create_task(...) call
(including the diary sync path), add the returned Task to that set and attach a
done callback to remove it (task.add_done_callback(self._tasks.discard)) so the
Task stays referenced until completion; ensure all create_task sites use this
pattern so spawned tasks are retained and cleaned up when finished.

In `@web/src/store/use-deep-research-store.ts`:
- Around line 175-193: The finally block currently performs control flow via
multiple returns (checking _abortController, ac.signal.aborted and sawDoneEvent)
which triggers lint/correctness/noUnsafeFinally; change the finally to be
cleanup-only: remove all return statements and only perform state cleanup (clear
_abortController when _abortController === ac and call set({ isActive: false })
as needed) so no early returns occur inside the finally. Keep the logic that
differentiates abort vs non-abort and sawDoneEvent by recording any flags (e.g.,
examine ac.signal.aborted and sawDoneEvent into local variables before the
try/finally or set them earlier) and let the surrounding function handle any
post-finally branching/returns instead of returning inside finally; ensure
references to _abortController, ac, ac.signal.aborted, sawDoneEvent and set
remain consistent.

---

Nitpick comments:
In `@api/app/agents/ingestion.py`:
- Around line 1-3: Add a runtime deprecation notice to the shim module
app.agents.ingestion by emitting a warnings.warn call at module import time
(right after the module docstring and before or after the existing from
app.agents.rag.ingestion import *), using the DeprecationWarning category and a
short message that the legacy import path is deprecated and points consumers to
app.agents.rag.ingestion; ensure the call is at module level so it runs on
import and use the warnings module (warnings.warn, DeprecationWarning) to avoid
altering existing behavior.

In `@api/app/agents/instructions.py`:
- Around line 1-3: The shim currently re-exports everything from
app.agents.core.instructions via a bare wildcard which creates an unstable
public surface; fix it by defining an explicit __all__ in this file that pins
the legacy API to the seven public symbols: CallLLMInstruction,
CallToolsInstruction, CallRAGInstruction, StreamAnswerInstruction,
CompressContextInstruction, RequestHumanApprovalInstruction, and
FinishInstruction (keep the existing from app.agents.core.instructions import *
but immediately add __all__ = [...the seven names as strings...] to stabilize
the exported surface).

In `@api/app/domains/conversation/router.py`:
- Around line 27-28: The router now delegates ownership enforcement to
ConversationService which creates a regression risk; add unit/integration tests
that exercise each router endpoint (list_by_notebook, read,
create/append/stream, delete) to assert cross-user access is denied when the
current_user.id does not match the notebook/conversation owner and allowed when
it does, targeting the ConversationService boundary (instantiate
ConversationService(db, current_user.id) or mock it) and calling the router
handlers used on lines around the listed ranges to verify authorization is
enforced and appropriate error/status responses are returned.

In `@web/src/components/chat-input/chat-toolbar.tsx`:
- Around line 253-267: Remove the unnecessary React fragment wrapping the single
<button> in the ChatToolbar component: when rendering the thinking-mode block
(guarded by isThinkingModel) delete the surrounding <>...</> so the button is
returned directly; update the same pattern in the other similar blocks in this
file (the other places that wrap single elements with fragments) — locate the
JSX guarded by isThinkingModel (and the blocks that use primaryItemClass,
onToggleThinking, closeMenu, thinkingEnabled, and Lightbulb) and remove the
extraneous fragments so only the single element is rendered.
- Around line 136-138: The effect in chat-toolbar.tsx using useEffect(() =>
onMenuOpenChange?.(menuOpen), [menuOpen, onMenuOpenChange]) can trigger repeated
renders if onMenuOpenChange is passed inline; change to a ref-stabilized
callback: store the latest onMenuOpenChange in a ref (e.g., callbackRef.current
= onMenuOpenChange in an effect) and then have the effect that watches only
menuOpen call callbackRef.current?.(menuOpen); alternatively document that
onMenuOpenChange must be memoized with useCallback — update references around
useEffect, onMenuOpenChange, and menuOpen accordingly.
- Around line 140-197: Duplicate outside-click useEffect logic across the menu,
dr, tools, and notebooks blocks should be extracted into a small reusable hook;
create a hook like useClickOutside(ref, isOpen, onClose) that attaches/removes
the "mousedown" listener only when isOpen is true and calls onClose when clicks
occur outside (use the same MouseEvent -> Node check used in the current
handlers), then replace the four useEffect blocks by calling
useClickOutside(menuRef, menuOpen, () => setMenuOpen(false)),
useClickOutside(drRef, drDropdownOpen, () => setDrDropdownOpen(false)),
useClickOutside(toolsRef, toolsMenuOpen, () => setToolsMenuOpen(false)), and
useClickOutside(notebooksRef, notebooksMenuOpen, () =>
setNotebooksMenuOpen(false)); keep the existing resize logic and panel layout
updates (getSecondaryPanelLayout, setToolsPanelLayout, setNotebooksPanelLayout)
in their respective effects or combine them with the new hook only if you also
support an optional resize handler parameter to preserve current behavior and
proper cleanup.

In `@web/src/features/chat/use-deep-research.ts`:
- Around line 213-216: setDrProgress is currently a typed Dispatch but
implemented as a silent no-op, which can hide stale-caller bugs; update the
useDeepResearch hook so this deprecated setter is either removed from the public
contract or replaced with a noisy dev-only setter that warns (or throws in
strict mode) when called. Locate the symbol setDrProgress inside the
useDeepResearch hook and either (a) remove it and update callers, or (b)
implement it to call the real state setter or to console.warn with contextual
info (including function name and call-site hint) when process.env.NODE_ENV !==
'production', so misuse becomes visible during development.

In `@web/src/store/use-deep-research-store.ts`:
- Around line 260-270: The code resets eventIndex to 0 and always calls
subscribeTo(taskId, get, set, 0), which prevents resuming; change reconnect() so
it preserves and uses the stored cursor: set eventIndex to status.eventIndex (or
read the existing store eventIndex if present) instead of always 0 and pass that
value into subscribeTo (i.e., subscribeTo(taskId, get, set, eventIndex)). Update
the set call that creates initial state (where eventIndex is currently
hardcoded) to use the preserved cursor, and ensure
makeInitialProgress(status.mode) remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8913edfe-2fc4-453d-b3de-cffcb50a544c

📥 Commits

Reviewing files that changed from the base of the PR and between a2c10b6 and 8bdccfd.

📒 Files selected for processing (36)
  • api/alembic/versions/028_message_reasoning.py
  • api/app/agents/brain.py
  • api/app/agents/composer.py
  • api/app/agents/deep_research.py
  • api/app/agents/engine.py
  • api/app/agents/evaluation.py
  • api/app/agents/file_memory.py
  • api/app/agents/ingestion.py
  • api/app/agents/instructions.py
  • api/app/agents/knowledge_graph.py
  • api/app/agents/memory_extraction.py
  • api/app/agents/memory_notebook.py
  • api/app/agents/memory_retrieval.py
  • api/app/agents/react_agent.py
  • api/app/agents/reflection.py
  • api/app/agents/research/task_manager.py
  • api/app/agents/retrieval.py
  • api/app/agents/scene_detector.py
  • api/app/agents/state.py
  • api/app/agents/tools.py
  • api/app/domains/ai/routers/research.py
  • api/app/domains/conversation/router.py
  • api/app/domains/conversation/schemas.py
  • api/app/models.py
  • api/app/services/conversation_service.py
  • web/messages/en.json
  • web/messages/zh.json
  • web/src/components/chat-input/chat-toolbar.tsx
  • web/src/components/chat-input/index.ts
  • web/src/components/home/home-qa.tsx
  • web/src/features/chat/chat-helpers.tsx
  • web/src/features/chat/chat-view.tsx
  • web/src/features/chat/use-deep-research.ts
  • web/src/lib/chat-tools.ts
  • web/src/schemas/chat-api.ts
  • web/src/store/use-deep-research-store.ts
✅ Files skipped from review due to trivial changes (6)
  • web/src/components/chat-input/index.ts
  • web/src/features/chat/chat-helpers.tsx
  • api/app/domains/conversation/schemas.py
  • api/alembic/versions/028_message_reasoning.py
  • web/src/features/chat/chat-view.tsx
  • web/messages/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/app/models.py

Comment on lines +65 to +70
# Module-level registry of active task buffers
_buffers: dict[str, TaskBuffer] = {}


def get_buffer(task_id: str) -> TaskBuffer | None:
return _buffers.get(task_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move the task buffer out of process memory.

_buffers only exists inside one API worker. If POST /ai/deep-research lands on worker A and GET /events lands on worker B, the second request will see no buffer and treat an active task as expired. Back this with shared storage/pub-sub, or explicitly require sticky single-worker routing.

Also applies to: 85-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/task_manager.py` around lines 65 - 70, The in-memory
_buffers dict (and its use in get_buffer/TaskBuffer) is single-worker only;
replace it with a shared store and optional pub/sub so buffers are visible
across workers: remove the module-level _buffers and implement a Redis (or other
shared datastore)-backed buffer registry used by get_buffer(TaskBuffer) and any
create/delete/update_buffer functions, persist TaskBuffer state
(serialize/deserialize) and publish changes on a channel so other workers can
subscribe and update local caches; alternatively, if you cannot introduce shared
storage, document and enforce sticky routing for endpoints that create/use
TaskBuffer (e.g., POST /ai/deep-research and GET /events) so both requests
always route to the same worker. Ensure you update function signatures and any
callers of get_buffer and buffer-mutating functions to use the shared-store
accessors.

Comment on lines +24 to +25
router = APIRouter()
logger = logging.getLogger(__name__)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file structure
find . -name "research.py" -type f | head -5

Repository: LinMoQC/LyraNote

Length of output: 100


🏁 Script executed:

# Get an overview of the repository structure
git ls-files | grep -E "(routers|research)" | head -20

Repository: LinMoQC/LyraNote

Length of output: 898


🏁 Script executed:

# Check if the file exists and its size
wc -l api/app/domains/ai/routers/research.py 2>/dev/null || echo "File not found at expected path"

Repository: LinMoQC/LyraNote

Length of output: 102


🏁 Script executed:

# Read the entire file to understand the structure
cat -n api/app/domains/ai/routers/research.py

Repository: LinMoQC/LyraNote

Length of output: 8652


🏁 Script executed:

# Check the task_manager module to understand task lifecycle
cat -n api/app/agents/research/task_manager.py

Repository: LinMoQC/LyraNote

Length of output: 7862


🏁 Script executed:

# Search for ResearchTask status updates to understand lifecycle
rg "status\s*=" api/app/agents/research/ -A 2 -B 2

Repository: LinMoQC/LyraNote

Length of output: 929


🏁 Script executed:

# Verify the exact lines mentioned "Also applies to: 70-80, 89-101"
sed -n '70,80p; 89,101p' api/app/domains/ai/routers/research.py | cat -n

Repository: LinMoQC/LyraNote

Length of output: 922


Retain asyncio.Task references to prevent orphaned "running" rows on restart.

The ResearchTask row is committed as status="running" (line 70), then execution is launched with an untracked fire-and-forget asyncio.create_task() (line 89). On process restart/reload, this background coroutine is abandoned, leaving the row permanently running with no worker to finish it.

The 10-minute buffer window allows client reconnects, but the database row remains orphaned indefinitely. At minimum, retain the Task object; if this feature must survive full process restarts, migrate execution to a durable queue.

Minimal fix for task retention
 router = APIRouter()
 logger = logging.getLogger(__name__)
+_research_tasks: set[asyncio.Task[None]] = set()
@@
-    asyncio.create_task(
+    research_task = asyncio.create_task(
         run_research_task(
             task_id=str(task_id),
             query=body.query,
             notebook_id=str(notebook.id),
             conversation_id=str(conv.id),
             user_id=str(current_user.id),
             mode=body.mode,
             model=settings.llm_model,
             tavily_api_key=settings.tavily_api_key or None,
             user_memories=user_memories,
         )
     )
+    _research_tasks.add(research_task)
+    research_task.add_done_callback(_research_tasks.discard)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/ai/routers/research.py` around lines 24 - 25, The
ResearchTask DB row is left as status="running" while execution is launched with
an untracked asyncio.create_task(), so on restart the row is orphaned; fix by
keeping a reference to the started Task in an in-memory registry (e.g., a
module-level dict like running_tasks keyed by ResearchTask.id) instead of
fire-and-forget, attach a done callback to the Task that updates the
ResearchTask DB row to a terminal status and removes the Task from
running_tasks, and ensure creation flow (where ResearchTask is committed as
status="running") stores the returned Task into running_tasks; for durability
beyond restarts migrate execution to a queue, but for the minimal fix modify
places that call asyncio.create_task(...) to save the Task and register a
callback that calls the existing DB update logic for ResearchTask.

Comment on lines +134 to +165
if buf is None and task.status == "done":
async def replay_done():
if task.timeline_json:
tl = task.timeline_json
plan_event = {
"type": "plan",
"data": {
"sub_questions": tl.get("subQuestions", []),
"research_goal": tl.get("researchGoal"),
"evaluation_criteria": tl.get("evaluationCriteria"),
"report_title": tl.get("reportTitle"),
},
}
yield f"data: {json.dumps(plan_event, ensure_ascii=False)}\n\n"

for learning in tl.get("learnings", []):
yield f"data: {json.dumps({'type': 'learning', 'data': learning}, ensure_ascii=False)}\n\n"

yield f"data: {json.dumps({'type': 'writing', 'data': {}}, ensure_ascii=False)}\n\n"

if task.report:
yield f"data: {json.dumps({'type': 'report_complete', 'data': {'report': task.report}}, ensure_ascii=False)}\n\n"

done_cites = tl.get("doneCitations", [])
yield f"data: {json.dumps({'type': 'done', 'data': {'citations': done_cites}}, ensure_ascii=False)}\n\n"

if tl.get("deliverable"):
yield f"data: {json.dumps({'type': 'deliverable', 'data': tl['deliverable']}, ensure_ascii=False)}\n\n"

yield "data: [DONE]\n\n"

return StreamingResponse(replay_done(), media_type="text/event-stream")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Apply from= to the completed-task replay branch.

When the live buffer has already been cleaned up, this branch always replays from the start. A reconnect with ?from=N will duplicate the earlier plan/learning/report events and can rebuild the UI twice.

One way to honor the resume offset
     if buf is None and task.status == "done":
         async def replay_done():
+            events: list[dict] = []
             if task.timeline_json:
                 tl = task.timeline_json
                 plan_event = {
                     "type": "plan",
                     "data": {
@@
                         "evaluation_criteria": tl.get("evaluationCriteria"),
                         "report_title": tl.get("reportTitle"),
                     },
                 }
-                yield f"data: {json.dumps(plan_event, ensure_ascii=False)}\n\n"
+                events.append(plan_event)
 
                 for learning in tl.get("learnings", []):
-                    yield f"data: {json.dumps({'type': 'learning', 'data': learning}, ensure_ascii=False)}\n\n"
+                    events.append({"type": "learning", "data": learning})
 
-                yield f"data: {json.dumps({'type': 'writing', 'data': {}}, ensure_ascii=False)}\n\n"
+                events.append({"type": "writing", "data": {}})
 
                 if task.report:
-                    yield f"data: {json.dumps({'type': 'report_complete', 'data': {'report': task.report}}, ensure_ascii=False)}\n\n"
+                    events.append({"type": "report_complete", "data": {"report": task.report}})
 
                 done_cites = tl.get("doneCitations", [])
-                yield f"data: {json.dumps({'type': 'done', 'data': {'citations': done_cites}}, ensure_ascii=False)}\n\n"
+                events.append({"type": "done", "data": {"citations": done_cites}})
 
                 if tl.get("deliverable"):
-                    yield f"data: {json.dumps({'type': 'deliverable', 'data': tl['deliverable']}, ensure_ascii=False)}\n\n"
+                    events.append({"type": "deliverable", "data": tl["deliverable"]})
+
+            for event in events[from_index:]:
+                yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
 
             yield "data: [DONE]\n\n"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/domains/ai/routers/research.py` around lines 134 - 165, The
completed-task replay branch (inside the buf is None and task.status == "done"
block) ignores the client's resume offset, causing duplicate events on
reconnect; update the replay_done() generator to accept and honor the ?from=N
offset (read the request's `from` query param) and skip yielding events until
the stream position reaches that offset when emitting plan, learning, writing,
report_complete, done, and deliverable events; locate the replay logic in
replay_done, use task.timeline_json and any existing event ordering logic to
compute each event's sequence index, advance/seek past events lower than `from`,
then yield only remaining events before sending the final "data: [DONE]\n\n" via
StreamingResponse.

Comment on lines 63 to +67
async def send_message(
conversation_id: UUID, body: MessageCreate, db: DbDep, current_user: CurrentUser
):
"""Non-streaming: save user message and return full assistant reply."""
conv = await _get_owned_conv(db, conversation_id, current_user.id)

user_msg = Message(
conversation_id=conversation_id,
role="user",
content=body.content,
)
db.add(user_msg)
await db.flush()

history = await _load_history(db, conversation_id)

from app.agents.retrieval import retrieve_chunks
from app.agents.composer import compose_answer
from app.agents.memory import get_user_memories, get_notebook_summary

user_memories = await get_user_memories(current_user.id, db)
notebook_summary = await get_notebook_summary(conv.notebook_id, db)
chunks = await retrieve_chunks(body.content, str(conv.notebook_id), db)
answer, citations = await compose_answer(
body.content, chunks, history,
user_memories=user_memories,
notebook_summary=notebook_summary,
)

assistant_msg = Message(
conversation_id=conversation_id,
role="assistant",
content=answer,
citations=citations,
)
db.add(assistant_msg)
await db.flush()
await db.refresh(assistant_msg)
return success(assistant_msg)
svc = ConversationService(db, current_user.id)
return success(await svc.send_message(conversation_id, body.content))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't accept fields this handler drops.

Line 64 reuses MessageCreate, but this handler only forwards body.content. Since the same schema exposes global_search, tool_hint, attachment_ids, and attachments_meta for /messages/stream, the plain /messages endpoint currently accepts fields it silently ignores. Either persist the supported extras here or use a narrower request model.

Comment on lines +228 to +241
assistant_msg = Message(
conversation_id=conversation_id,
role="assistant",
content=content_text,
reasoning=reasoning_text,
citations=citations,
agent_steps=agent_steps or None,
)
self.db.add(assistant_msg)
await self.db.flush()

self._dispatch_post_chat_tasks(
conversation_id, scene, user_memories
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "conversation_service.py" | head -20

Repository: LinMoQC/LyraNote

Length of output: 102


🏁 Script executed:

wc -l ./api/app/services/conversation_service.py

Repository: LinMoQC/LyraNote

Length of output: 106


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '220,250p'

Repository: LinMoQC/LyraNote

Length of output: 1510


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '1,50p'

Repository: LinMoQC/LyraNote

Length of output: 1789


🏁 Script executed:

rg -n "_dispatch_post_chat_tasks" ./api/app/services/conversation_service.py -A 20

Repository: LinMoQC/LyraNote

Length of output: 2062


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '286,330p'

Repository: LinMoQC/LyraNote

Length of output: 2432


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '130,170p'

Repository: LinMoQC/LyraNote

Length of output: 1685


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '220,250p'

Repository: LinMoQC/LyraNote

Length of output: 1510


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '136,250p'

Repository: LinMoQC/LyraNote

Length of output: 5070


🏁 Script executed:

rg -n "stream_agent" . --type py -B 3 -A 3 | head -60

Repository: LinMoQC/LyraNote

Length of output: 1080


🏁 Script executed:

cat -n ./api/app/domains/conversation/router.py | sed -n '80,110p'

Repository: LinMoQC/LyraNote

Length of output: 1252


🏁 Script executed:

cat -n ./api/app/database.py | head -80

Repository: LinMoQC/LyraNote

Length of output: 980


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '301,340p'

Repository: LinMoQC/LyraNote

Length of output: 2125


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '336,360p'

Repository: LinMoQC/LyraNote

Length of output: 1478


🏁 Script executed:

cat -n ./api/app/services/conversation_service.py | sed -n '251,285p'

Repository: LinMoQC/LyraNote

Length of output: 1711


Commit the outer transaction before dispatching background tasks.

The assistant message is flushed but not yet committed when _dispatch_post_chat_tasks() spawns background tasks. Since background tasks open fresh AsyncSessionLocal() sessions, they cannot see the newly flushed message until the outer transaction commits—which doesn't happen until after the response stream finishes. This causes background jobs to operate on stale conversation state.

Additionally, all asyncio.create_task() calls at lines 292–295, 299, and 352 drop their returned task handles, leaving task lifetimes implicitly managed.

Either commit before dispatch, or defer task spawning to code that runs after the outer transaction commits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/conversation_service.py` around lines 228 - 241, The flushed
assistant Message is not yet visible to new AsyncSessionLocal sessions because
the outer transaction isn't committed before _dispatch_post_chat_tasks spawns
background tasks; change the flow to await self.db.commit() (or otherwise commit
the outer transaction/context that includes self.db.add/await self.db.flush)
before calling _dispatch_post_chat_tasks so background workers see the new
message, and update the asyncio.create_task usages referenced in
_dispatch_post_chat_tasks (and the calls at the noted lines) to capture and
manage the returned Task objects (store them in variables, append to a list, or
return them) instead of dropping the handles so task lifetimes are explicit.

Comment on lines +276 to +282
result = await self.db.execute(
select(Message)
.where(Message.conversation_id == conversation_id)
.order_by(Message.created_at.asc(), Message.role.desc())
.limit(20)
)
return [{"role": m.role, "content": m.content} for m in result.scalars().all()]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Load the most recent history window here.

This fallback orders ascending and then limits to 20, so once a conversation exceeds 20 messages the agent sees the oldest turns instead of the latest context. Query the newest slice and reverse it before returning.

Suggested fix
         result = await self.db.execute(
             select(Message)
             .where(Message.conversation_id == conversation_id)
-            .order_by(Message.created_at.asc(), Message.role.desc())
-            .limit(20)
+            .order_by(Message.created_at.desc(), Message.role.asc())
+            .limit(RAW_HISTORY_WINDOW)
         )
-        return [{"role": m.role, "content": m.content} for m in result.scalars().all()]
+        recent = list(reversed(result.scalars().all()))
+        return [{"role": m.role, "content": m.content} for m in recent]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/services/conversation_service.py` around lines 276 - 282, The current
fallback selects messages ordered ascending and limits to 20, returning the
oldest 20; instead, change the query in conversation history retrieval to order
by Message.created_at.desc() (newest first) with .limit(20), then take
result.scalars().all(), reverse that list to restore chronological order, and
finally return [{"role": m.role, "content": m.content} for m in reversed_list];
update the code around the select(Message) ... .order_by(...) and the
result.scalars().all() handling so the agent sees the most recent window of
messages in chronological order.

onNotebookSelect={setSelectedNotebook}
notebookLabel={th("notebook")}
notebookEmptyLabel={tn("empty")}
clearNotebookLabel="清除笔记本限制"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded Chinese string breaks i18n.

"清除笔记本限制" should use a translation key for consistency with the rest of the internationalized UI. The chat.json files should have this string defined.

♻️ Suggested fix
-        clearNotebookLabel="清除笔记本限制"
+        clearNotebookLabel={t("clearNotebookFilter")}

Then add the corresponding keys to web/messages/en.json and web/messages/zh.json:

"clearNotebookFilter": "Clear notebook filter"  // en.json
"clearNotebookFilter": "清除笔记本限制"  // zh.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
clearNotebookLabel="清除笔记本限制"
clearNotebookLabel={t("clearNotebookFilter")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/home/home-qa.tsx` at line 108, Replace the hardcoded
Chinese label clearNotebookLabel="清除笔记本限制" in the HomeQA component with a
translated string (e.g., clearNotebookLabel={t('clearNotebookFilter')}) using
the existing i18n helper (t) or useTranslation hook used elsewhere in this
component; then add the "clearNotebookFilter" key to the locale message files
used by the app with values "Clear notebook filter" (English) and "清除笔记本限制"
(Chinese) so the label is served from translations rather than hardcoded text.

Comment on lines +210 to +232
async start(query, notebookId, mode) {
_abortController?.abort();

set({
query,
notebookId,
mode,
progress: makeInitialProgress(mode),
reportTokens: "",
isActive: true,
eventIndex: 0,
taskId: null,
conversationId: null,
});

const { taskId, conversationId } = await createDeepResearch(query, {
notebookId,
mode,
});
set({ taskId, conversationId });

subscribeTo(taskId, get, set);
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Rollback optimistic state if task creation fails.

start() marks the store active before createDeepResearch() runs, but a rejected create leaves isActive: true and an initial progress with no taskId. The hook catches the error, so this stale store state is what keeps the UI looking like a live task that can never reconnect.

🛠️ Suggested rollback
       async start(query, notebookId, mode) {
         _abortController?.abort();
 
         set({
           query,
           notebookId,
           mode,
           progress: makeInitialProgress(mode),
           reportTokens: "",
           isActive: true,
           eventIndex: 0,
           taskId: null,
           conversationId: null,
         });
 
-        const { taskId, conversationId } = await createDeepResearch(query, {
-          notebookId,
-          mode,
-        });
-        set({ taskId, conversationId });
-
-        subscribeTo(taskId, get, set);
+        try {
+          const { taskId, conversationId } = await createDeepResearch(query, {
+            notebookId,
+            mode,
+          });
+          set({ taskId, conversationId });
+          void subscribeTo(taskId, get, set);
+        } catch (error) {
+          set({
+            taskId: null,
+            conversationId: null,
+            progress: null,
+            reportTokens: "",
+            isActive: false,
+            eventIndex: 0,
+          });
+          throw error;
+        }
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/store/use-deep-research-store.ts` around lines 210 - 232, start()
sets optimistic active state before awaiting createDeepResearch(), so if
createDeepResearch() rejects the store remains stale; wrap the call to
createDeepResearch(query, { notebookId, mode }) in a try/catch and on error
rollback the optimistic fields (reset isActive to false, clear taskId,
conversationId, revert progress to null or previous state from get(), and ensure
eventIndex/reportTokens are cleared) and rethrow or handle the error; keep
subscribeTo(taskId, get, set) only after successful creation and setting of
taskId/conversationId so you never subscribe when creation fails.

LinMoQC added 2 commits March 19, 2026 11:03
- 空字符串返回 ("", "") 而非 ("(无内容)", "")
- valid JSON 有 counterpoint 但无 finding 时,finding 回退到 raw 文本
- valid JSON finding 字段值不再截断到 max_chars
- 绝对 fallback 返回 "" 而非 "(提取失败)"

Made-with: Cursor
这些文件虽在 .gitignore 中,但历史上已被追踪。
统一 git rm --cached 清理,后续不再入库。

Made-with: Cursor
LinMoQC added 2 commits March 19, 2026 11:08
覆盖 5 条提取路径:
- Path 1: 完整 JSON parse(有/无 counterpoint、缺 finding、超长值不截断)
- Path 2: 嵌入式 JSON(prose 中、markdown fence 包裹)
- Path 3: 严格正则(转义引号)
- Path 5: 原始文本回退(空串、纯文本、截断边界)
- EdgeCases: 返回类型、数字转字符串、多行、max_chars=0

Made-with: Cursor
当输入是残缺 JSON(以 {"finding" 开头但无法解析)时,
fallback 路径应剥去 JSON 脚手架后返回剩余文本,
而不是原样返回带 {"finding" 前缀的字符串。

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
api/app/agents/research/deep_research.py (6)

34-34: Unused constant MAX_QUERIES_PER_DIM.

This constant is defined but never referenced—cfg.queries_per_dim from ModeConfig is used instead. Consider removing it to avoid confusion.

Proposed fix
 RAG_THRESHOLD = 0.50
-MAX_QUERIES_PER_DIM = 2     # cap queries per dimension to avoid runaway cost
 LEARNING_MAX_CHARS = 200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` at line 34, Remove the unused
constant MAX_QUERIES_PER_DIM: locate its definition and delete it, and ensure
all code paths use cfg.queries_per_dim from ModeConfig (already in use) so no
other changes are needed; if any future references are intended, replace them
with cfg.queries_per_dim to avoid duplication and confusion.

227-236: Rename ambiguous variable l to learning.

The single-letter l is easily confused with 1 or I. Static analysis (E741) flags this for readability.

Proposed fix
 def compute_evidence_strength(learnings: list[dict]) -> str:
     """Aggregate evidence strength across all learnings."""
-    total = sum(len(l.get("citations", [])) for l in learnings)
-    has_web = any(c.get("type") == "web" for l in learnings for c in l.get("citations", []))
-    has_internal = any(c.get("type") == "internal" for l in learnings for c in l.get("citations", []))
+    total = sum(len(learning.get("citations", [])) for learning in learnings)
+    has_web = any(c.get("type") == "web" for learning in learnings for c in learning.get("citations", []))
+    has_internal = any(c.get("type") == "internal" for learning in learnings for c in learning.get("citations", []))
     if total >= 6 and has_web and has_internal:
         return "high"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` around lines 227 - 236, The
variable name `l` in compute_evidence_strength is ambiguous and flagged by
static analysis; rename it to a clearer identifier like `learning` wherever used
(in the generator expressions for total, has_web, and has_internal) to improve
readability and satisfy E741, ensuring all references to the old `l` within the
function body are updated consistently.

555-555: Same ambiguous l variable issue.

Consistent with the earlier recommendation, rename l to learning for readability.

Proposed fix
-    all_citations_count = sum(len(l.get("citations", [])) for l in learnings)
+    all_citations_count = sum(len(learning.get("citations", [])) for learning in learnings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` at line 555, The generator
expression computing all_citations_count uses an ambiguous loop variable `l`;
change it to a descriptive name (e.g., `learning`) so the expression becomes
sum(len(learning.get("citations", [])) for learning in learnings) — update the
variable in the expression that assigns to all_citations_count and any identical
short-variable uses nearby (referencing the all_citations_count assignment and
the learnings iterable).

487-494: Additional l variable usage to rename.

This comprehension in _synthesize_report also uses l.

Proposed fix
     learnings_text = "\n\n".join(
-        f"[{DIMENSION_LABELS.get(l.get('dimension', 'concept'), '研究')}] "
-        f"查询:{l['sub_question']}\n"
-        f"发现 [{l.get('evidence_grade', 'weak')}]:{l['content']}"
-        + (f"\n反例/风险:{l['counterpoint']}" if l.get("counterpoint") else "")
-        for l in learnings
-        if l.get("content") and l.get("content") != "未找到相关信息"
+        f"[{DIMENSION_LABELS.get(learning.get('dimension', 'concept'), '研究')}] "
+        f"查询:{learning['sub_question']}\n"
+        f"发现 [{learning.get('evidence_grade', 'weak')}]:{learning['content']}"
+        + (f"\n反例/风险:{learning['counterpoint']}" if learning.get("counterpoint") else "")
+        for learning in learnings
+        if learning.get("content") and learning.get("content") != "未找到相关信息"
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` around lines 487 - 494, The list
comprehension in _synthesize_report uses the single-letter variable name "l"
which duplicates other uses; rename the loop variable to something descriptive
(e.g., "learning" or "entry") and update all references inside the comprehension
(learning.get(...), learning['sub_question'], learning['content'],
learning.get('counterpoint'), etc.) so the comprehension building learnings_text
uses the new variable name consistently and avoids shadowing/duplicate usage.

241-242: Misleading function name: web_search_sync is actually async.

The _sync suffix suggests synchronous execution, but this is an async def function using httpx.AsyncClient. Consider renaming to web_search or web_search_async for clarity.

Proposed fix
-async def web_search_sync(query: str, tavily_api_key: str, max_results: int = 4) -> list[dict]:
-    """Call Tavily synchronously. Does NOT persist sources to DB."""
+async def web_search(query: str, tavily_api_key: str, max_results: int = 4) -> list[dict]:
+    """Call Tavily asynchronously. Does NOT persist sources to DB."""

Also update the call site at line 421:

-            web_results = await web_search_sync(query, tavily_api_key, max_results=max_web_results)
+            web_results = await web_search(query, tavily_api_key, max_results=max_web_results)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` around lines 241 - 242, The
function web_search_sync is misleading because it's declared async and uses
httpx.AsyncClient; rename the function (e.g., to web_search_async) and update
its docstring accordingly, then update every call site and any
imports/references that use web_search_sync to the new name (search for
web_search_sync and references to httpx.AsyncClient in this module to locate
usages), keeping the same signature and behavior to avoid breaking callers.

686-686: Rename l to learning here as well.

Proposed fix
-        all_citations = [c for l in state["learnings"] for c in l.get("citations", [])]
+        all_citations = [c for learning in state["learnings"] for c in learning.get("citations", [])]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` at line 686, The list comprehension
that builds all_citations uses a terse loop variable name `l`; rename it to
`learning` for clarity by changing the comprehension `all_citations = [c for l
in state["learnings"] for c in l.get("citations", [])]` to use `learning` (i.e.,
iterate `for learning in state["learnings"]` and call `learning.get("citations",
[])`) — update any nearby uses in the same expression to match.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/app/agents/research/deep_research.py`:
- Around line 437-438: Replace the silent "except Exception: pass" in
api/app/agents/research/deep_research.py with an exception handler that logs the
failure and exception details (e.g., change to "except Exception as e:" and call
the appropriate logger such as logger.warning(..., exc_info=True) or
self.logger.exception(...)) so web search errors (rate limits, auth, config) are
recorded for diagnosis while keeping execution flow intact.

---

Nitpick comments:
In `@api/app/agents/research/deep_research.py`:
- Line 34: Remove the unused constant MAX_QUERIES_PER_DIM: locate its definition
and delete it, and ensure all code paths use cfg.queries_per_dim from ModeConfig
(already in use) so no other changes are needed; if any future references are
intended, replace them with cfg.queries_per_dim to avoid duplication and
confusion.
- Around line 227-236: The variable name `l` in compute_evidence_strength is
ambiguous and flagged by static analysis; rename it to a clearer identifier like
`learning` wherever used (in the generator expressions for total, has_web, and
has_internal) to improve readability and satisfy E741, ensuring all references
to the old `l` within the function body are updated consistently.
- Line 555: The generator expression computing all_citations_count uses an
ambiguous loop variable `l`; change it to a descriptive name (e.g., `learning`)
so the expression becomes sum(len(learning.get("citations", [])) for learning in
learnings) — update the variable in the expression that assigns to
all_citations_count and any identical short-variable uses nearby (referencing
the all_citations_count assignment and the learnings iterable).
- Around line 487-494: The list comprehension in _synthesize_report uses the
single-letter variable name "l" which duplicates other uses; rename the loop
variable to something descriptive (e.g., "learning" or "entry") and update all
references inside the comprehension (learning.get(...),
learning['sub_question'], learning['content'], learning.get('counterpoint'),
etc.) so the comprehension building learnings_text uses the new variable name
consistently and avoids shadowing/duplicate usage.
- Around line 241-242: The function web_search_sync is misleading because it's
declared async and uses httpx.AsyncClient; rename the function (e.g., to
web_search_async) and update its docstring accordingly, then update every call
site and any imports/references that use web_search_sync to the new name (search
for web_search_sync and references to httpx.AsyncClient in this module to locate
usages), keeping the same signature and behavior to avoid breaking callers.
- Line 686: The list comprehension that builds all_citations uses a terse loop
variable name `l`; rename it to `learning` for clarity by changing the
comprehension `all_citations = [c for l in state["learnings"] for c in
l.get("citations", [])]` to use `learning` (i.e., iterate `for learning in
state["learnings"]` and call `learning.get("citations", [])`) — update any
nearby uses in the same expression to match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de4955d2-cdfb-45ba-be60-045f76be1767

📥 Commits

Reviewing files that changed from the base of the PR and between 8bdccfd and 06b485b.

⛔ Files ignored due to path filters (121)
  • api/alembic/__pycache__/env.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/001_initial_schema.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/002_memory_tables.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/003_notebook_is_global.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/004_memory_v2.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/005_restore_user_memory_unique_index.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/006_skills.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/007_conversation_summary.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/008_source_storage_key.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/009_message_agent_steps.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/010_single_user_auth.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/011_user_avatar.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/012_clerk_id_nullable.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/013_memory_doc.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/014_chunks_fts_index.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/015_oauth_ids.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/016_oauth_unbound.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/017_memory_embedding_and_eval.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/018_message_feedbacks.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/019_message_attachments.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/020_note_as_source.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/021_scheduled_tasks.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/022_notebook_public.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/023_knowledge_graph.cpython-312.pyc is excluded by !**/*.pyc
  • api/alembic/versions/__pycache__/bcd2ff1941ab_add_note_word_count.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/auth.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/config.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/database.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/dependencies.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/exceptions.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/main.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/__pycache__/models.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/composer.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/deep_research.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/evaluation.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/file_memory.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/ingestion.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/knowledge_graph.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/memory.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/memory_extraction.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/memory_notebook.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/memory_retrieval.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/react_agent.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/reflection.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/retrieval.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/scene_detector.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/tools.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/agents/__pycache__/writing.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/ai/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/ai/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/artifact/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/artifact/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/artifact/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/auth/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/config/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/config/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/conversation/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/conversation/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/conversation/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/feedback/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge_graph/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/knowledge_graph/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/memory/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/memory/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/note/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/note/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/note/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/notebook/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/notebook/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/notebook/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/public/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/public/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/public/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/setup/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/skill/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/skill/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/source/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/source/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/source/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/task/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/task/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/task/__pycache__/schemas.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/upload/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/domains/upload/__pycache__/router.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/email.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/embedding.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/llm.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/perplexity.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/storage.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/providers/__pycache__/tavily.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/schemas/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/schemas/__pycache__/response.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/__pycache__/base.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/__pycache__/registry.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/compare_sources.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/create_note.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/deep_read.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/mind_map.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/scheduled_task.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/search_knowledge.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/summarize.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/update_memory_doc.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/update_preference.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/skills/builtin/__pycache__/web_search.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/utils/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/utils/__pycache__/cron.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/utils/__pycache__/markdown_email.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/workers/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/app/workers/__pycache__/tasks.cpython-312.pyc is excluded by !**/*.pyc
  • api/scripts/__pycache__/__init__.cpython-312.pyc is excluded by !**/*.pyc
  • api/scripts/__pycache__/backfill_memory_embeddings.cpython-312.pyc is excluded by !**/*.pyc
  • api/scripts/__pycache__/delete_duplicate_user.cpython-312.pyc is excluded by !**/*.pyc
  • api/scripts/__pycache__/reset_all_data.cpython-312.pyc is excluded by !**/*.pyc
📒 Files selected for processing (1)
  • api/app/agents/research/deep_research.py

Comment on lines +437 to +438
except Exception:
pass
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent exception swallowing hides web search failures.

The except Exception: pass block silently discards all errors from web search, including API rate limits, authentication failures, or configuration issues. At minimum, log a warning so operators can diagnose issues.

Proposed fix
-        except Exception:
-            pass
+        except Exception as exc:
+            _extract_log.warning("Web search failed for %r: %s", query, exc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception:
pass
except Exception as exc:
_extract_log.warning("Web search failed for %r: %s", query, exc)
🧰 Tools
🪛 Ruff (0.15.6)

[error] 437-438: try-except-pass detected, consider logging the exception

(S110)


[warning] 437-437: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/app/agents/research/deep_research.py` around lines 437 - 438, Replace the
silent "except Exception: pass" in api/app/agents/research/deep_research.py with
an exception handler that logs the failure and exception details (e.g., change
to "except Exception as e:" and call the appropriate logger such as
logger.warning(..., exc_info=True) or self.logger.exception(...)) so web search
errors (rate limits, auth, config) are recorded for diagnosis while keeping
execution flow intact.

目录结构:
- tests/unit/        纯函数,无 IO(毫秒级)
- tests/integration/ 服务层,直接调 DB,无 HTTP
- tests/e2e/         完整 ASGI client

迁移:
- test_auth / test_health / test_notebook / test_upload → e2e/
- test_deep_research_extract_finding → unit/

新增:
- unit/test_deep_research_utils.py    _strip_fences / _try_json_dict / grade_evidence / compute_evidence_strength
- unit/test_response_utils.py         success() / fail() / not_configured() / ApiResponse
- integration/test_conversation_service.py  ConversationService CRUD + 消息持久化
- e2e/test_conversation.py            对话端点 + MessageSave + 消息列表

Made-with: Cursor
- unit: grade_evidence 单条 web → "weak"(非 "medium"),修正测试名和断言
- e2e: conversation 创建端点返回 201 非 200,helper 和测试改为 in (200, 201)
- integration: list_messages 顺序在同毫秒内不稳定,改为验证内容包含而非 ID 顺序

Made-with: Cursor
@LinMoQC LinMoQC merged commit 3e0f108 into main Mar 19, 2026
4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 27, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant