XuRuuuy a9c8f79dfe
fix(gateway): resolve the agent factory off the event loop (#6206)
* fix(gateway): resolve the agent factory off the event loop

start_run() called resolve_agent_factory() inline, which lazily imports
the full lead-agent assembly stack (middlewares, authz, MCP, jsonschema;
measured at ~8s cold on Windows, multi-second on slower filesystems).
start_run is the single choke point every run-creation path flows
through, so the first run after Gateway startup blocked the event loop
for the whole import, stalling every other request, stream and channel
on the same loop - the loop-stall family issue #5172 fixed for agent
construction itself (the checkpoint-state path already resolves through
the assembly pool).

Resolve the factory via asyncio.to_thread: the first call pays one
worker-thread hop for the import, later runs hit the cached module.
Regression test pins the contract with a probe module whose module-level
__getattr__ records the thread that performed the from-import.

* fix(gateway): isolate factory resolution in the assembly pool

Move import regressions into the strict blocking-I/O suite and cover default-executor isolation, request context, and failure-before-admission ordering.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-06 07:52:47 +08:00

4.5 KiB

Goal Objective Validation

goal_objective.py owns the dependency-free normalized 4000-character objective validation shared by runtime and scheduled-task capability/repository admission. runtime.goal re-exports the helper for compatibility. Validation does not replace the saved original text; blank or over-limit text is rejected before persistence.

Message Text Extraction

message_content_to_text takes raw message content and treats None as empty text so downstream empty-result, error-detail, and archive-skip fallbacks work. Keep its other conversions unchanged, including newline-separated list blocks and literal strings such as "None". Do not replace it with message_to_text, which takes a whole message and uses different list/mapping semantics. Regression coverage lives in tests/test_utils_messages.py, tests/test_subagent_executor.py, and tests/test_task_continuity.py.

llm_text.py::strip_think_blocks scans opening and closing tags forward so repeated unclosed <think> prefixes in model output do not rescan the same suffix. Keep its case-insensitive tag handling, optional whitespace before the closing >, and the truncate_unclosed behavior. Regression coverage lives in tests/test_utils_llm_text.py.

Agent / Tool Assembly Off-Load

start_run also resolves the factory through run_assembly before admission: cold imports cannot occupy the loop or its default pool, and import errors cannot admit a run. tests/blocking_io/test_agent_factory_resolve_offloop.py pins offload, executor isolation, request context, and failure ordering.

Tool and agent assembly re-enters get_available_tools() and may block on MCP discovery, so the four async assembly entry points — run_agent's agent_factory call, task_tool, durable batch _execute_item, and abuild_checkpoint_state_accessor — dispatch through deerflow.utils.assembly_io.run_assembly, a dedicated ContextVar-preserving bounded executor (DEER_FLOW_ASSEMBLY_WORKERS, default 8) rather than the loop's default executor; a hung MCP server therefore parks an assembly worker instead of queueing unrelated default-executor work, and the pool logs a warning when pending assemblies exceed the worker count. tests/blocking_io/test_tool_assembly_offloop.py pins all four offloads plus the ContextVar propagation.

Uploaded Document Summaries

file_outline.py reads outlines and fallback previews as utf-8-sig so an optional leading UTF-8 BOM cannot hide a first-line heading or code fence, or occupy a preview line. Preserve physical line numbers, embedded U+FEFF characters, and the original file bytes.

All outline heading styles skip root-level indented code (four leading spaces or a tab after up to three spaces), including PDF-style single and split bold headings. Keep legitimate bold headings with up to three spaces and the shared conversion-companion path working. Regression coverage lives in tests/test_file_outline_indented_bold.py.

Split-bold headings reject numeric/punctuation/currency-only blocks after the section number, including parentheses, signs, leading whitespace, and $€£¥. Apply the check to the second through fourth blocks; preserve the four-block limit, punctuated titles containing text, and non-ASCII titles. Coverage lives in tests/test_file_outline_split_bold.py.

Active Content MIME Types

text_detection.py::_is_active_content_mime_type is the shared download-safety boundary for artifacts and project documents. Keep platform aliases such as Windows' image/svg aligned with their standard active type (image/svg+xml), and preserve the generic +xml rule.

Conversion Worker Ownership

convert_file_to_markdown drains large-file conversion workers across caller cancellation before returning control to source-file cleanup. Keep conversion inside await_drained, including when the worker fails or cancellation repeats; upload ingestion owns a temporary source that it removes in finally. Coverage lives in tests/test_file_conversion_cancellation.py.

Host Path Portability

host_paths.py rejects Windows device names for host-visible creation paths on every platform, including the COM/LPT aliases with superscript ¹, ² and ³. The console aliases CONIN$ and CONOUT$ are reserved too; match them case-insensitively before the first dot, without rejecting longer ordinary names such as CONIN$notes.txt. Do not normalize arbitrary Unicode digits into device numbers: names such as COM⁴.txt and COM¹notes.txt are ordinary portable names. Read/removal callers retain their existing portability exemptions.