deer-flow/CHANGELOG.md
Hyeonsang Cho f9f3127dc1
fix(uploads): delete the requested upload, not a symlink's target (#5547)
* fix(uploads): delete the requested upload, not a symlink's target

delete_file_safe resolved the requested path before unlinking it. The
uploads directory is writable from local and AIO sandboxes, so a
symlink planted under an upload name was followed: deleting alias.pdf
removed the victim.pdf it pointed to, and the companion cleanup then
removed victim.md, while the link itself survived and the call reported
"Deleted alias.pdf". A link resolving outside the directory was already
refused by the traversal check, so the damage stayed inside the
thread's uploads.

The function now checks and unlinks the requested entry itself and
treats a symlink as not found, the same way list_files_in_dir already
hides it. unlink() never follows the final component, so a file swapped
for a link between the check and the unlink removes only the link.
Tests cover the helper, the Gateway DELETE route, and
DeerFlowClient.delete_upload.

* docs(changelog): note upload delete symlink fix (#5547)
2026-09-18 19:56:09 +08:00

270 KiB
Raw Blame History

Changelog

All notable changes to DeerFlow are documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased]

This section accumulates work toward the 2.1.0 milestone (milestone 2). This release closes that milestone with 765 merged pull requests.

⚠ Breaking changes

  • gateway: Request trace ids are now issued unconditionally, and every Gateway HTTP response carries an X-Trace-Id header. Previously both were gated behind logging.enhance.enabled, which now controls log output only — whether records carry a trace_id field, and in which format. The header cannot be turned off; installations running the default enabled: false will start seeing it after upgrading. Scheduled tasks, MCP task notification runs, IM channel messages, and the embedded DeerFlowClient bind an id per unit of work, so the id also reaches the run record, the checkpoint metadata, and Langfuse traces that previously had none. A deerflow_trace_id supplied in a run request's metadata or config.context is now ignored and overwritten so the response header, the logs, and the persisted run cannot disagree — send the X-Trace-Id request header to pin a correlation id across services. logging remains restart-required. No config keys were added or removed. (#5119)
  • skills: Sandboxes now reserve /mnt/skills for managed enabled-only projections. DEER_FLOW_HOST_SKILLS_PATH and SKILLS_HOST_PATH are no longer used; Docker/AIO and hostPath deployments derive projection paths from DEER_FLOW_HOST_BASE_DIR. E2B operator mounts targeting /mnt/skills or any child path are skipped with a warning so they cannot shadow the managed projection; move extra E2B content to a different container path. User projections re-read global enable state from disk so toggles propagate across Gateway workers on the next sandbox acquire. Existing E2B sandboxes retain their creation-time snapshot until they are recreated. PVC-backed provisioner deployments still mount the operator-supplied PVC snapshot directly, so disabled-skill filesystem isolation does not apply in PVC mode until dynamic PVC materialization is implemented. (#4178)
  • sandbox: E2B now enforces sandbox.replicas as a process-local capacity limit. The default wait policy waits for acquire_timeout, then fails the agent turn. DeerFlow does not retry the turn automatically. Use burst with burst_limit to permit bounded extra VMs. The reject policy can remove one warm VM before it returns a capacity error. (#4391)
  • skills: A directory containing SKILL.md is now a runtime package boundary. Nested SKILL.md files inside that package are supporting data and are no longer registered as independent skills; unusual custom layouts must move independently loadable skills under a namespace directory without its own SKILL.md. (#4098)
  • memory: The memory system is now pluggable (memory.manager_class selects a backend; default deermem is self-contained). DeerMem-private settings moved from the top level of memory: into memory.backend_config, and the /memory/config response (and client.get_memory_config()) changed shape. (#4122)
  • memory: /memory/config and client.get_memory_config() no longer return flat DeerMem fields (storage_path, max_facts, debounce_seconds, token_counting, guaranteed_*, staleness_*, ...). They return {enabled, mode, injection_enabled, manager_class, backend_config} where backend_config is an opaque dict the active backend self-interprets. Memory data responses (/memory, /memory/status data) are unchanged. External API/SDK clients reading the old flat fields must read backend_config instead. (#4122)
  • memory: Custom memory.storage_class moved: the old default path deerflow.agents.memory.storage.FileMemoryStorage no longer exists (now deerflow.agents.memory.backends.deermem.deermem.core.storage.FileMemoryStorage). Custom MemoryStorage subclasses must accept config in __init__ (was no-arg). A broken/old storage_class logs an error and falls back to FileMemoryStorage (won't crash) -- update the path + signature to restore it. (#4122)
  • memory: storage_path semantics changed from a FILE path to a root DIRECTORY. Pre-abstraction, an absolute storage_path was the shared memory file (opting out of per-user isolation) and a relative value was the global file under the data base_dir. Now storage_path (absolute or relative) is the root directory; per-user memory lives at {storage_path}/users/{uid}/memory.json. An upgrade keeping the old default storage_path: memory.json (a relative file name) would orphan per-user memory or hit NotADirectoryError on save, so the legacy migration drops file-style storage_path values (ending in .json) with a warning and the factory raises if storage_path resolves to an existing file. Set memory.backend_config.storage_path to a directory for a custom root. (#4122)
  • memory: memory.mode: tool with a backend that does not implement search() now fails fast at Gateway startup with a ValueError from the MemoryManager invariant, instead of starting successfully and silently returning empty results on every memory_search call. Both shipping backends implement search() (DeerMem retrieves; noop returns []), so this only affects a custom backend that onboards without overriding search(). It is intentional -- silent empties are worse than a loud startup error. Fix: switch to mode: middleware or override search() (and set supports_search=True). (#4324)
  • config: database.checkpoint_delta_snapshot_frequency moved to database.checkpoint_delta.snapshot_frequency and its default changed from 1000 to 10. A legacy top-level value is still honored with a deprecation warning and mapped onto the nested key (an explicitly set nested key wins). Deployments that relied on the old default now snapshot 100x more often in delta mode -- set database.checkpoint_delta.snapshot_frequency: 1000 explicitly to keep the previous cadence. (#4516)
  • docker: The published entry port now binds to loopback (127.0.0.1) by default in both compose files, matching the documented local-trust deployment model. Deployments that relied on the old 0.0.0.0 binding must set BIND_HOST to expose the stack on other interfaces. (#4618)

Added

Scheduler

  • scheduler: Scheduled tasks accept interval (schedule_spec.every_seconds) in addition to once and cron. Cadence is UTC now + N with no missed-beat catch-up. N is at least scheduler.min_once_delay_seconds (default 60s) and at most 30 days. (#5291)
  • scheduled-tasks: Upcoming cron occurrences can be inspected before a task is saved. POST /api/scheduled-tasks/preview-cron takes a five-field cron expression, a timezone, a count of 110 (default 5), and an optional aware reference time, and returns the normalized cron, the effective UTC reference, and the occurrences in both UTC and local time. It runs the scheduler's own calculator in a worker thread, so DST handling matches what a saved task will actually do, and it reserves nothing and touches no task, thread, or run store; invalid input and date-range failures return 422. (#5381)
  • scheduled-tasks: Run history can be filtered server-side by occurrence status. Finding a rare failure used to mean downloading every history page and filtering in the client, since a failure behind many newer successes could sit on any page; GET /api/scheduled-tasks/{task_id}/runs now accepts ?status= with queued, launching, running, success, failed, skipped, and interrupted, applying the predicate before pagination so ?status=failed&limit=50&offset=0 returns the first page of real failures. Unknown values, including the parent task's own completed, return 422, empty results return [], and the unfiltered array response is unchanged. (#5384)
  • scheduled-tasks: The scheduled-task detail pane can page backwards through execution records. It showed only the latest 50 even though the endpoint already supports limit and offset, so anything older was unreachable from the UI. Newer, Older, and Latest navigation now walk the history with localized page, loading, error, and retry states; each request fetches one row past the page to decide whether an older page exists rather than inventing a total count, a short final page disables Older, switching tasks resets to the latest page and cancels the obsolete request so a late result cannot replace the new task's history, and only the latest page polls. (#5363)

Authentication

  • auth: Personal access tokens (PAT) for programmatic API access: POST/GET/DELETE /api/v1/auth/pats manage tokens (shown once, stored as SHA-256 digests); a default-deny route policy admits only the thread/run lifecycle routes, narrowed further by the token's threads/runs scopes, and any request dimension that carries cancel capability (?action=, multitask_strategy) additionally requires runs:cancel. (#5041)
  • auth: Login throttling parameters are configurable via auth.local.max_login_attempts (default 5, min 2) and auth.local.lockout_seconds (default 300), resolved live so a config reload applies on the next login without a Gateway restart — the unblock path for deployments behind corporate proxies/NAT where many users share one egress IP. Defaults are unchanged. (#5110)
  • settings: Account preferences survive a cleared browser or a move to another device. The notification toggle, default model, conversation mode, and reasoning effort are stored as independent (user_id, key) rows behind session-authenticated GET/PATCH /api/v1/auth/preferences, hydrated from the server before first paint, with a tab-local outbox and capped-backoff retry so a failed write is not silently dropped; null resets a field. Only those four allowlisted fields are transmitted — device notification permission, display settings, and thread-specific model overrides stay local, and an automatic composer fallback is never uploaded as an explicit account preference. Existing unscoped local settings are not migrated, since they carry no known owner, so users reselect these four once after upgrading. (#5397)

Agents & runtime

  • scheduler: Scheduled tasks can pin assistant_id to lead_agent (the default) or a custom agent the owner already has. Unknown or malformed names return 422. The workspace create/edit form exposes the same choice. (#5286, #5288)
  • gateway: GET /api/threads/{thread_id}/runs/page walks thread run history with a (created_at, run_id) keyset cursor ({data, has_more, next_before_created_at, next_before_run_id}). GET /api/threads/{thread_id}/runs still returns a bare array of the newest 100 runs so LangGraph SDK clients keep working. (#5282, #5283)
  • middleware: New TokenBudgetMiddleware enforces a per-run token budget, shared additively across the lead agent and subagents. (#3412)
  • middleware: Structured tool-result metadata and a tool-progress state machine give the runtime first-class visibility into multi-step tool flows. (#3601)
  • context: Record the effective memory identity per run and persist durable context (system messages, memory, and tool state) across summarization, emitting it as structured runtime metadata so compaction no longer drops it. (#3556, #3887, #3906)
  • runtime: Goal continuations let a run resume toward a goal across multiple agent turns, with continuation_count tracked and capped. (#3858)
  • subagents: A system-maintained delegation ledger prevents redundant re-delegation of an in-flight task, and a total delegation cap bounds fan-out per run. (#3877, #4115)
  • subagents: Persist and display subagent step history in the thread. (#3845)
  • tools: Structured synopses replace raw oversized tool output in previews. (#3377)
  • files: Deterministic read-before-write version gate for file tools prevents clobbering concurrent edits. (#3912)
  • gateway: Cache-aware cost accounting attributes token costs to cached vs. uncached paths; a Redis stream bridge enables distributed event streaming; and manual context compaction is exposed to the user. (#3920, #3191, #3969)
  • gateway: The stream-bridge heartbeat interval is configurable via stream_bridge.heartbeat_interval_seconds (default 15s), so deployments behind aggressive proxy idle timeouts can tune SSE, /wait, and internal subscribers together. (#5017)
  • runtime: Dual-mode checkpoint storage with LangGraph DeltaChannel cuts thread storage from O(N²) to near-linear for long research/coding runs. (#4292)
  • runtime: Delta-mode checkpoint history cache (memory/redis) with O(1) incremental composition, configured via database.checkpoint_cache. (#4638)
  • agent: Config-declared lead-agent middlewares let deployments add custom AgentMiddleware classes without patching the runtime chain. (#3964)
  • agents: Per-agent model and generation settings (temperature, max_tokens, thinking_enabled, reasoning_effort) override the shared model profile. (#4347)
  • runtime: Record terminal artifact-delivery receipts so runs expected to present_files no longer report success when delivery fails. (#4365)
  • uploads: Lazy-load historical files via a list_uploaded_files tool instead of injecting the full manifest. (#4174)
  • scheduler: scheduler.recursion_limit in config.yaml sets the LangGraph super-step cap for scheduled runs (default 1000, matching the web UI's interactive budget, clamped by max_recursion_limit). (#4848)
  • runtime: Every tool call now carries a runtime-stamped, tamper-evident tool receipt, and a bounded receipt ledger is injected into the model context so agents can cite execution evidence in their reports. Enabled by default via the new verification config section. (#4659)
  • subagents: Subagent delegations are now verifiable, layering RFC #4651: every subagent's report contract requires citing tool receipts (e.g. [r3 write_file]) and attaching a verifiable handle to each deliverable, the lead agent cross-checks those citations against the subagent's actual execution record, and acceptance_criteria on a task delegation are checked deterministically parent-side (file existence/non-emptiness, recorded test-command exit status) with anything undecidable reported UNVERIFIED instead of silently passed. (#5076, #5090, #5109)
  • clarification: Human-input (clarification) cards support structured form fields, so an agent can request exactly the input it needs instead of free text only. (#4406)
  • subagents: Built-in subagents now receive the current-date context anchor, so delegated tasks involving relative dates behave like tasks the lead agent handles directly. (#4797)
  • subagents: A Settings page manages a deployment-level Subagent catalog (admin-managed worker definitions alongside built-in and config.yaml ones), and Custom Agents can restrict delegation to an explicit worker allowlist enforced at both prompt and execution time. (#4887)
  • subagents: Subagent concurrency is now governed by one process-wide capacity controller, and an opt-in batch_task tool runs large collections of independent items as durable, resumable SQL-backed batches with leases, bounded retries, pause/resume/cancel, and a chat panel for tracking progress. (#4998)
  • agents: The current-date context injected into lead- and subagent prompts honors the optional DEER_FLOW_DATE_TIMEZONE env var (IANA name, e.g. Asia/Shanghai), so users in non-UTC deployments are no longer told the wrong "today" around midnight; unset keeps server-local behavior. (#5154)
  • subagents: Delegated subagents can discover files uploaded in earlier turns: the parent run's validated uploaded_files boundary seeds the subagent's graph state, making list_uploaded_files eligible for normal tool-policy filtering (durable batch_task workers keep it disabled). (#5170)
  • agents: The read-before-write gate now elides the dead payload of a blocked write_file / str_replace call (content, old_str, new_str) from model-bound requests. A blocked call never ran and must be re-issued after a re-read, so the original arguments only cost context; stored history, receipts, and the run journal keep them. Blocked results are paired with call occurrences (tool-call ids may repeat across turns), and a request whose history was rewritten drops OpenAI resp_ response ids so use_previous_response_id chaining cannot resume the original server-side history. Controlled by read_before_write.elide_blocked_payloads (default on) and read_before_write.elide_min_chars (default 2000). (#5329)
  • agents: ToolOutputBudgetMiddleware now also elides the content of a successful write_file call from model-bound requests once the same path was read or modified again later in the conversation. After a successful write the file on disk is the source of truth, and the read-before-write gate forces a read_file before the next modification, so the historical copy was redundant with that read and long report-writing runs carried every section twice. The newest tool_output.keep_recent_writes successful writes (default
    1. always stay visible, str_replace payloads are never touched, and stored history, receipts, and the run journal keep the original arguments. Controlled by tool_output.elide_superseded_writes (default on) and tool_output.superseded_write_min_chars (default 2000). (#5374)
  • agents: Custom Agent config.yaml accepts memory_enabled: false to run an agent as a stateless execution worker; omission defaults to true. The opt-out removes only that agent from the memory lifecycle — recalled-memory injection, passive capture, the memory_search / memory_add / memory_update / memory_delete tools, and tool-mode memory guidance are all suppressed, and both automatic summarization and manual /compact skip the durable memory flush. Switching an existing agent off clears only the frozen, server-tagged memory reminders from checkpoint state: date reminders, real user messages, and untagged lookalikes survive, and the global memory setting is unchanged for every other agent. (#5167)
  • runtime: ToolProgressMiddleware's interventions become auditable in the durable run-event stream. A new middleware:tool_progress event is persisted for each effective warn, block, recover, or later-invocation reset phase transition; previously those replanning hints, tool-state promotions, and short-circuits disappeared into process logs, so a persisted run could not show whether the model recovered on its own or the runtime guard intervened. Only bounded decision metadata is recorded — status and recovery vocabularies derive from the canonical tool_result_meta schema, and tool arguments, result content, prompts, and content-derived hashes never enter the event. Subagent and other recorder attribution keys are server-owned at both the Gateway and embedded-worker boundaries, so a caller cannot forge durable attribution. Events appear only when ToolProgress is enabled, which remains off by default. (#5214)
  • subagents: Durable batch_task items accept the same optional acceptance_criteria that ordinary task delegations already take, so a batch worker can no longer claim a missing deliverable exists and still land as a succeeded item with no recorded check. Criteria are normalized before persistence (20 usable entries, 500 characters after neutralization; empty becomes null) and preserved across lease recovery, and a completed execution reuses the existing deterministic checker against owner-scoped thread files and recorded test-command evidence. Item queries and JSONL exports gain nullable acceptance_criteria and a separately validated acceptance_verdict (holds, does not hold, or UNVERIFIED): a completed item with a report, a missing CSV, and an unsupported quality claim stays succeeded with those three leaves and is not retried automatically. Items without criteria and legacy rows report no verdict. (#5289)
  • subagents: task(context_mode="snapshot") lets a delegation carry a dispatch-time copy of the retained parent conversation, instead of the delegated prompt alone. Snapshot mode captures conversation content and summary_text after delegation validation and before child setup, so a rejected delegation serializes nothing and later parent edits never reach the child. It is rendered as a separate historical HumanMessage ahead of the task, keeping plain text, text blocks, historical tool-call descriptions with their matching results, and serializable media input; unserializable media becomes an explicit omission notice, while parent system messages, hidden framework state (injected memory and todo state included), reasoning blocks, execution metadata, and pending tool calls are excluded. The child keeps its own role, model, tools, and skill restrictions, and parent tool frames never enter its execution history, so parent actions cannot populate child receipts, execution steps, or bash evidence. context_mode="isolated" remains the default, and batch_task items still require self-contained prompts. (#5367)
  • context: Opt-in task notes and compacted-history recall, gated by task_continuity.enabled (default false; the config schema gains the section with disabled defaults). Enabling it exposes task_note, history_search, and history_read to standard lead agents and DeerFlowClient under the existing authorization and skill policies, with both sync and async tool execution. task_note keeps short checkpointed notes of constraints, decisions, failed attempts, and next steps in a shared state channel that enforces notebook limits and report shape on every write, rendered as escaped historical data in the durable-context human message. Automatic and manual compaction additionally preserve bounded message and tool text — including genuine hidden clarification-card answers — into a user/thread-local SQLite FTS5 archive outside the sandbox mount, which history_search and history_read then query; malformed checkpoint metadata is reported as unavailable instead of aborting a model call or compaction, and a storage failure leaves ordinary compaction working. This is lexical recall within one task: it does not resume runs, replicate archives across hosts, or create cross-thread memory, and it adds no embedding dependency. (#5382)
  • gateway: Gateway runs no longer hardcode a recursion limit of 100. A deployment whose normal tasks need longer agent loops had to make every API caller supply a request-level override, and a caller that did not hit GraphRecursionError on otherwise valid work. A hot-reloaded top-level recursion_limit now supplies the default when a request omits it (100, for backward compatibility), an explicit request value still wins, an invalid request value falls back to the configured default, and max_recursion_limit caps both configured and client-supplied values. (#5390)
  • gateway: A Gateway run can be asked to read a specified earlier conversation. Opt-in read_conversation plus a conversation_references run field (up to three thread ids or same-origin chat URLs) grant the lead agent a model-facing read of a previous conversation while it continues the current task; ordinary message text never confers access, and the grant is bound to that run's references, the run's runs:read permission, and ownership of each source. Reads reuse the existing transcript pagination and visibility rules, returning bounded user and assistant text with source ids, continuation cursors, and truncation or unavailability notices while excluding hidden context, reasoning blocks, and raw tool results. The reader is never persisted in configuration and is released when the run ends, so resume and replay need the references again; bootstrap, subagent, and ordinary embedded paths do not receive it. Text is capped at 4,000 characters per message and 20,000 per page. (#5399)
  • gateway: Conversation references can be sent by SDK clients and detected by them. The LangGraph JS SDK's RunsClient.stream drops unknown top-level fields, so a browser client could not send the conversation_references field at all, and it had no way to tell whether the reader was enabled — leaving no way to hide an entry point where it was off. Run create, stream, and wait requests now accept context.conversation_references with the same bounds (at most three, each 12048 characters) and the same error locations, lifted into the canonical field before validation and removed from context so it never reaches the run context or the checkpointed configurable; sending both spellings returns 422, and a copy left in body.config still grants nothing. GET /api/features reports conversation_references: {enabled, max_references}, where enabled follows the configured tool list so a config.yaml change takes effect without a restart. (#5463)
  • tools: list_uploaded_files accepts optional query (a case-insensitive filename substring) and extensions ("pdf" or ".PDF", both meaning .pdf), and both filters run before the 20-file truncation that lazy-loading history already applied. Previously the cap hit the unfiltered mtime-sorted list, so "analyze those PDFs I uploaded before" in a thread full of newer screenshots pushed the PDFs into omitted_summary and left the agent with no path to hand read_file; total_count, truncated, and omitted_summary now describe the filtered set. Omitting both parameters keeps today's behavior, the two filters combine with AND, blank or invalid values count as no filter, and a filtered miss returns No uploaded files matched the given filters.. (#5341)
  • agents: Custom Agents showed their ASCII-only storage identifier in the gallery, chat header and welcome page, so an owner could not label an agent in Chinese or any other non-ASCII script. Each agent can now carry an optional display_name — whitespace-trimmed, at most 100 characters — on its config document and through the create/update/read/list APIs; omitting the field on an update preserves it, and null or a blank value clears it. The label is edited from the agent gallery's settings button, counts Unicode code points against a visible budget, and is rejected when it holds control characters, invisible formatting characters, or nothing but marks, separators or format characters — ordinary multilingual text and ZWJ emoji stay allowed. Paths, URLs, the runtime agent_name, ownership and React identity keep using the stable identifier, so nothing needs migrating, and an invalid stored value is ignored on read rather than breaking list/detail/bootstrap. (#5324)

Memory

  • memory: Memory consolidation synthesizes fragmented facts, and a staleness review prunes silently-outdated facts using LLM-assigned per-fact expected_valid_days / staleFactsToExtend. (#3996, #3860, #4143)
  • memory: Guaranteed injection of correction facts (with graceful fallback) so user corrections always reach the model. (#3592)
  • memory: Slim the pluggable MemoryManager interface for backend onboarding - new backends no longer implement unused abstract methods, and DeerMem-specific hook injection moves out of the shared factory. (#4326)
  • memory: Incremental agent-scoped Markdown fact storage isolates per-agent facts and updates a single fact without rewriting or reindexing the whole collection. (#4279)
  • memory: Memory message processing adds a conversation watermark, trivial-turn filtering, and a durable queue so extraction no longer re-feeds the full conversation every turn. (#4447)
  • memory: A built-in FTS5/BM25 retrieval adapter provides full-text search over stored memories without an external retrieval service. (#4360)
  • memory: New pluggable memory backends: OpenViking and mem0 over HTTP, plus Honcho as a user-model memory provider. (#4509, #4528, #4730)
  • memory: A hybrid fact eviction policy blends multiple signals when deciding which stored facts to drop as memory fills. (#4789)
  • memory: Opt-in write-side guard against storing paraphrases of a fact that is already remembered, so repeated extraction no longer consumes memory capacity and prompt tokens with duplicates. The write-side gate is memory.backend_config.fact_dedup_enabled (default false), with memory.backend_config.fact_dedup_similarity_threshold (default 0.7, range 0.51.0). Within the current user/agent scope a new same-category fact is compared against existing ones with bounded token-Jaccard similarity over Latin words and Chinese bigrams — a lexical heuristic, not semantic equivalence detection. A merge keeps the existing fact's id, content, and creation time, raises confidence to the higher of the two, and refreshes source only when confidence increased; the facts_merged_dedup counter records how many merges happened. Paired correction replacements bypass near-dedup, removal proposals exclude their own targets from matching, and no confirmation signal is fabricated — reinforcement still requires a genuine human message. (#5254)

Skills

  • skills: The built-in image-generation skill can use OpenAI-compatible Images APIs for generation and reference-image editing, with configurable endpoint, model, size, and output format. (#5389)
  • skills: Native SkillScan (phase 1) statically analyzes skill packages at load, and describe_skill enables deferred discovery so the model fetches a skill's schema on demand instead of loading all skills up front. (#3033, #3775)
  • skills: Per-user custom skill isolation with sandbox mounting. (#3889)
  • skills: The skill list reopens after a skill is selected, so several skills can be attached in a row. (#4639)
  • skills: Install local .skill archives directly from the Skills settings page, reusing the existing per-user installer and security scan. (#5039)
  • skills: Podcast-generation Volcengine voices are configurable per speaker gender, with trimmed blank-safe defaults. (#5156)
  • skills: Custom skills can be exported from Settings, disabled ones included, so what you back up, move between installations, or send a teammate is the version actually installed rather than the original upload. A bilingual preview lists package size, paginated file paths, declared requirements, and actionable blockers, and the two new admin-only manifest and download endpoints bind that preview to a revision digest — a package changed in between returns 409 instead of a stale ZIP. Supporting files, empty directories, and normalized executable permissions survive the round trip, while links, special files, nonportable paths, nested skill roots, and executable binaries are rejected. Export copies raw saved files and is not a redaction step: it neither scans for secrets nor runs skill scripts, so credentials and account settings must still be configured separately. (#5332)
  • skills: Deferred skill discovery ranks candidates instead of matching the model's whole free-text query as one regular expression. With skills.deferred_discovery: true the agent sees only skill names and calls describe_skill to choose what to load, but a multi-term intent had to match contiguously: chart visualization never found chart-visualization, and analyze Python never found a skill described as "Analyze data with Python, pandas, jupyter" — so the agent could not load the matching workflow or its active tool policy. Lookup now normalizes Unicode, case, and name separators and scores candidates by literal intent-term coverage, with name matches outranking description-only matches at equal coverage and catalog order breaking ties so results stay reproducible; model-generated queries are bounded to 256 characters and 16 unique terms. select: exact selection and +required name filtering are unchanged, and no embeddings, model calls, or telemetry are involved. (#5369)

Models & integrations

  • community: New web search/fetch engines - GroundRoute, Crawl4AI (web_fetch), and a fastCRW provider - plus a Browserless web_capture screenshot tool and Brave image_search. (#3675, #3821, #3585, #3881, #3866)
  • mcp: Per-server tool_call_timeout for MCP tool calls, and routing hints that guide the model to the right server. (#3843, #4004)
  • mcp: Add an official OpenViking /mcp example that exposes the native tool set through DeerFlow's generic MCP client. (#4745)
  • community: Agentic browser control as a first-class thread capability - Playwright-backed browser sessions the agent operates while the user observes or takes over from the workspace. (#4187)
  • community: Lark/Feishu CLI integration bundles the runtime install, the official lark-* skill pack, and an interactive auth flow so the integration is no longer environment-dependent. (#3971)
  • integrations: Lark/Feishu app credentials can be switched per user from Settings > Integrations: new App ID/Secret values are validated before anything is committed, and the previous OAuth token is revoked after a successful switch. (#4703)
  • acp: MiniMax Code (mcode acp) is supported and documented as a native external coding agent, and ACP thought chunks are no longer concatenated into tool results. (#4846)
  • models: A Z.AI GLM-5.3-Flash profile keeps thinking permanently enabled and stops generic reasoning-effort forwarding, since the model rejects disabled thinking and only accepts its own effort values. (#5074)
  • community: New web search providers - Serply (with news and scholar verticals) and Tencent Cloud WSA - plus native recency filters (day/week/month/year) shared across DDGS, Brave, Tavily, and SearXNG. (#5023, #5057, #5099)
  • community: New Sofya web_search and web_fetch provider - search results carry the content of each page, capped per result so a default search stays inline. (#5239)
  • knowledge: Opt-in read-only RAGFlow retrieval exposes a knowledge_search(query) agent tool over configured RAGFlow datasets, with a dataset-ID allowlist and credential/dataset-id redaction on error paths. (#4955)
  • knowledge: Opt-in read-only LightRAG retrieval is an alternative knowledge_search(query) provider for the same knowledge group — operators pick RAGFlow or LightRAG by which entry is configured. It queries LightRAG's structured /query/data endpoint (no LLM generation), formats the ranked chunks as citation-numbered text, redacts the optional X-API-Key on every model-visible path, and maps server error messages to actionable tool errors. (#5209)
  • models: A per-user favorites layer keeps frequently used models reachable without changing the selected or default one. The compact two-line model list is now an anchored dropdown in both the main chat and Side Chat, with an inline star per row; favorites sort into the first group without selecting a model or closing the picker, and persist per signed-in user with cross-tab synchronization and restoration of temporarily unavailable entries. The existing search field is removed as a deliberate simplification, with no separate management mode. (#5441)
  • models: Bounded concurrency does not bound request rate: several concurrent runs could still exhaust a provider's requests-per-minute allowance, and nothing paced the demand before dispatch, so a 429 was the first signal anything was wrong. A new optional models[].request_admission block admits model calls through a process-local queue, taking requests_per_minute (required), an optional group (defaults to the model config name), max_wait_seconds (default 300) and max_queue_size (default 256); at 60 RPM admissions land at least a second apart, and idle periods accumulate no burst credit. One bounded FIFO is shared across synchronous callers, independent asyncio loops and every model instance in an explicit group, whose policies must be identical — a conflicting setting fails construction and needs a restart. Cancellation and timeout drop a waiter without consuming an admission, and a full queue fails before dispatch. The limiter attaches through the LangChain BaseChatModel hook, so agent models, the invoke and stream paths and factory-created auxiliary models are all covered, and the factory strips the policy from provider kwargs and sets the exposed SDK max_retries to zero so retries cannot silently bypass the hook — existing agent middleware retries re-enter admission, while non-middleware callers lose SDK retries. Budgets are per process, so an operator must partition an account allowance across workers, replicas and other clients; the limiter counts requests, not tokens, and unconfigured models behave exactly as before. (#5432)

MCP

  • mcp: A durable task runtime for MCP: long-running tool tasks survive Gateway restarts through a durable driver, and their progress and completion notifications surface in the chat UI. (#4665, #4690, #4833)
  • mcp: Shared MCP servers can inject per-user credentials: a single server entry authenticates each DeerFlow user with their own header value, unmapped users are denied by default, and stored credentials are masked in Gateway API responses. (#4868)
  • mcp: Per-server tool_name_prefix option lets servers that already namespace their own tools keep their original tool names; the default behavior is unchanged. (#4624)
  • mcp: Settings > Tools can add, edit, and delete MCP servers through targeted Gateway endpoints, with a copy-paste JSON workflow that preserves advanced fields and masked secret placeholders. (#5022)
  • mcp: Shared HTTP/SSE servers can map request-scoped secrets to headers via headers_from_context: callers supply per-request values in config.context.secrets, the config stores only key names, and missing values deny by default. (#5010)
  • mcp: An optional parallel-search server entry (https://search.parallel.ai/mcp, HTTP, no auth by default) ships in extensions_config.example.json, disabled by default; enabling it exposes parallel-search_web_search and parallel-search_web_fetch, with optional Bearer authentication documented. (#5028, #5501)

Channels

  • channels: Expose the IM channel_user_id to sandbox commands as DEERFLOW_CHANNEL_USER_ID. (#3926)
  • channels: Queue rapid same-thread messages and preserve topic-card previews across batches. (#3988)
  • channels: Inbound webhook deduplication moves to Postgres, so several Gateway pods can serve the same IM channel without double-processing events. (#4210)
  • channels: DingTalk inbound messages support file and image attachments. (#4423)
  • channels: New Buzz (Nostr) channel connector, including the frontend experience for the channel. (#4649, #4727)
  • channels: /agent list and /agent use <name> IM commands let a conversation switch to the owner's Custom Agents: the selection is persisted in thread metadata (restored after restart) and wins over stale channel defaults, IM-created threads route through the same agent when opened in the web UI, and /agent is reserved across the slash-skill parser, frontend, and TUI so no skill can shadow the command. (#5168)

Auth & guardrails

  • auth: Generic OIDC/SSO authentication with Keycloak support. (#3506)
  • guardrails: Authenticated runtime context is exposed in GuardrailRequest, and security interventions are persisted as run events. (#3665, #3837)
  • auth: "Keep me signed in" login option with a centralized session-cookie policy (persistent Secure cookies on HTTPS, session cookies on public HTTP). (#4255)
  • auth: Deployments can close local self-registration to restrict new accounts to SSO/OIDC provisioning. (#4311)
  • authz: Built-in RBAC authorization provider with a unified factory, plus tool-authorization enforcement at both assembly (tools removed before the model sees them) and runtime (denied calls blocked). (#4260, #4370)
  • authz: Gateway route permissions are derived from the configured AuthorizationProvider rather than a fixed table. (#4439)
  • authz: Model authorization is enforced at Gateway routes and again in the agent runtime, and sandbox:execute is checked when a sandbox is acquired - users can no longer reach models or sandboxes they are not authorized for. (#4540, #4911)
  • authz: GET /auth/me now surfaces the caller's effective route permissions (RFC #4063 Phase 4), read from the AuthContext the auth middleware already stamps on every authenticated request — no extra provider evaluations — so the frontend can hide actions the caller's role cannot perform. (#5228)
  • authz: A role denied threads:delete or runs:cancel was still shown the thread-row Delete menu item, the sidecar delete button and an enabled composer stop button, so the UI offered an action the Gateway's @require_permission guard would only reject. The frontend now consumes the effective permissions GET /auth/me reports and hides the delete affordances, and disables the stop button while streaming with an aria-label/title naming the permission boundary. Enforcement is unchanged — the Gateway guards remain the single decision point — and an absent, null or not-yet-loaded permission list is treated as permissive, so a mixed old-backend/new-frontend deploy can never hide an action the caller can still perform. (#5294)

Sandbox & provisioner

  • sandbox: New E2B and BoxLite (micro-VM) sandbox providers; BoxLite ships with a warm pool. (#3883, #3940, #3951)
  • provisioner: ClusterIP Services and scoped per-skill PVC mounts, plus a configurable sandbox container port. (#4016, #3928)
  • sandbox: New cloud sandbox providers: Tenki and OpenSandbox. (#4382, #4877)
  • sandbox: An optional lark-cli credential broker sidecar (K8s provisioner mode) keeps Lark app secrets and OAuth tokens out of the sandbox filesystem entirely - the sandbox sees only a shim that forwards commands to a loopback broker in the pod. Off by default. (#4501)
  • sandbox: The E2B mount-upload wall-clock deadline is configurable via mount_upload_deadline_seconds (default 120s). (#4876)
  • sandbox: An E2B sandbox carries a structured MountUploadResult (truncated, reason, upload totals) after creation — preserved across warm-pool reclaim within the process — so mount truncation by a resource limit is observable in code instead of only in Gateway logs. (#4884)
  • sandbox: Opt-in controlled egress for local Docker AIO sandboxes: sandbox.network.mode supports isolated (per-sandbox internal bridge with no outbound route) and allowlist (the same bridge plus a domain-allowlist HTTP(S) policy sidecar that resolves destinations itself and rejects IP literals and ECH); denied public domains can be approved through the Human Input card (temporary grant or allow-for-this-sandbox), non-interactive runs fail closed, and the sandbox API is no longer published directly in restricted modes. Requires Docker Engine 28+; open remains the default. (#5152)

Extensions & plugins

  • extensions: An out-of-tree Python extension system: extensions can contribute middleware, task-lifecycle and system-model observers, Gateway services, and HTTP routers, and are managed with deerflow extensions install/enable/disable/remove. (#4636, #4684, #4780)
  • extensions: Extensions can observe what the agent did - message provenance, middleware policy declarations, agent-assembly fingerprints, context-compaction records, guardrail decisions, and the MCP origin of a tool. deerflow-extension-api moves to 0.2.0; extensions written against 0.1 are refused at startup with an install hint. (#4863)
  • extensions: An extensions.middlewares entry may be {class, kwargs} in addition to the existing module.path:ClassName string, and kwargs is passed to the constructor. Operator-managed middleware that needs a threshold, a header name, or any other argument no longer has to ship a hardcoded subclass just to set one value. String entries still construct with no arguments, blank class paths are rejected at config validation instead of failing later at agent creation, and unknown fields such as apply_to are refused. (#5312)
  • extensions: deerflow extensions upgrade SOURCE (also exposed as make extension-upgrade SOURCE=...) replaces a managed local snapshot, or re-pins a requirement already in the extensions group, and adopts the existing plugins: record so its private config and required survive. Moving to a newer pin used to mean remove then install, and remove deletes the whole record — secrets included — while an already-snapshotted local directory could not be reinstalled at all without editing the managed copy by hand. Plain install is unchanged and still refuses an existing local snapshot, upgrading a source that is not installed fails closed with an install hint, and a failed upgrade restores the previous snapshot even when a concurrent pyproject.toml / uv.lock edit blocks dependency-file rollback. (#5347)
  • extensions: A new optional RunEvidenceReader contract lets a Gateway extension learn what changed since it last looked, instead of reconciling whole threads: a host-owned monotonic cursor drives durable changed-run discovery, alongside scoped per-run event paging and authoritative status reads. Cursors are opaque, versioned, scope-bound, replay-safe, and durable for database-backed stores; paging orders by (change_seq, run_id) rather than inferring global order from thread-scoped event sequences or timestamps; progress snapshots and lease heartbeats do not advance the clock; and deletions emit no tombstone, so consumers poll status and treat a missing run as absent. Existing extensions are unaffected because ExtensionRuntimeDeps.run_evidence_reader defaults to None, and a store that cannot serve a page fails explicitly rather than reporting a misleadingly empty one. The production reader is app-scoped with global cross-user visibility for trusted operator extensions; it redacts event metadata but leaves content unchanged. (#5405)

Persistence

  • persistence: A custom PostgreSQL schema can be selected via postgres_schema; ORM, LangGraph checkpointer, and store tables are all created there, and the schema is created automatically at startup. (#3442)

Frontend

  • frontend: Branching support for assistant turns and side conversations for quoted follow-ups. (#3950, #3934)
  • frontend: Regenerate the latest answer. (#3637)
  • frontend: Citation-sources evidence panel, workspace change review for agent runs, and a visualized ask_clarification card. (#3907, #3945, #3956)
  • frontend: Voice dictation, prompt-history recall with arrow keys, composer input polishing, and a "(thought for N seconds)" thinking-duration chip. (#4036, #3718, #3986, #3627)
  • frontend: Feature-gate the agents UI behind the agents_api flag, and persist AI turn duration in backend and UI. (#3769, #3663)
  • frontend: Render slash-skill activations as inline chips. (#3981)
  • frontend: Localized AI-assistance disclaimer. (#4374)
  • frontend: Pin recent chats. (#4442)
  • frontend: Validate /goal objective length in the composer. (#4337)
  • frontend: Real-time context window usage is shown as a conversation grows. (#3183)
  • frontend: The latest user turn can be edited and rerun in place. (#4377)
  • frontend: Replies can be typed and sent while a clarification card is pending. (#4530)
  • suggestions: The number of follow-up suggestions is configurable via suggestions.max_suggestions (default 3). (#4533)
  • artifacts: Text artifacts can be edited inline in the artifact panel. (#4596)
  • artifacts: Markdown artifacts open rendered in a new-window reader (with "View source" and "Download" fallbacks), and all files presented in a run can be downloaded as one zip archive derived from the run's delivery receipt. (#5056, #5117)
  • frontend: Browser Live is available in Custom Agent chats. (#4719)
  • frontend: A conversation outline navigates long chats: past 5 user turns, a compact side menu lists the conversation's questions and jumps between them, tracking the current section. (#5025)
  • frontend: Scheduled tasks can be duplicated into an editable draft that carries over the configuration but not the run history. (#5064)
  • threads: Branched conversations get distinguishing titles (automatic Title (2), Title (3) sibling numbering) and the recent-chats list shows parent-child lineage with tree connectors. (#4983)
  • frontend: Chats can be archived and restored: an Archive sidebar action with an Undo toast, Recent chats / Archived tabs above search, and per-chat restore controls; SQL and Memory stores apply the archive filter before pagination while messages, files, links, pin state, and the current URL are preserved. (#5236)
  • projects: Project workspaces organize chats (Projects MVP Phase 1): a sidebar Projects section with flat/grouped list modes, a project detail page with a paginated thread list, project-scoped new chats whose threads are pre-created with their project so a run can never land outside it, branch membership inheritance, move-between-projects, and project create/rename/archive/restore/delete. (#5265)
  • artifacts: Completed CSV/TSV artifacts preview as bounded tables (up to 200 rows × 50 columns, 50 rows per page, sticky headers, optional first-row header) parsed off the main thread over the existing 1 MiB range loader — literal strings, leading zeros, and multiline quoted cells survive, long cells open in a copyable dialog, and source view remains one toggle away. (#5284)
  • title: Attachment-only first turns get a real title instead of the generic New Conversation, so file-first conversations are distinguishable and searchable. When the first turn carries one validated attachment, the cleaned filename becomes the local title; several attachments produce a count such as 2 files uploaded; user-authored text still wins as the title source, and New Conversation remains the fallback when no valid filename is available. The local title is returned before the configured title-model path, so an attachment-only turn no longer pays for an LLM call. Filenames are read only from the uploaded_files state the upload middleware populated — client metadata is not trusted — and are sanitized: control characters and excess whitespace stripped, ordinary Unicode and percent signs preserved, long names truncated while retaining the extension. This supersedes the New Conversation fallback the earlier <current_uploads> title fix deliberately kept. (#5304)
  • projects: Projects MVP Phase 2 makes a project do its job rather than just name a folder. Project instructions — previously stored, editable, and read by nothing — now reach the model on every member-thread run as a bounded, user-role <project> block in that run's request only, never the system prompt or persisted history, with renames taking effect on the next run and oversized text rejected at write time rather than truncated. A per-project document shelf gains a Documents tab (upload, list with provenance, preview, download, move-to-trash, content-hash dedup) that the agent reads on demand through list_project_documents / read_project_document — registered only in project runs, text only — plus promotion in both directions: Save to project copies any thread file onto the shelf with provenance, and Attach to thread ingests a shelf document back into a conversation through the normal upload pipeline. A read-only conversation-files view aggregates member threads' uploads and outputs, and project deletion finally has a trash tier. (#5443)
  • frontend: Tool details for generic and MCP calls are inspectable in debug mode. Those steps previously showed only a label even though the browser already receives their inputs and results, making it hard to see what was passed between tools. Token Usage → Debug now carries a collapsed Tool details panel — including for calls with no token statistics — showing tool name, call ID, input, and either the original result or an explicit error, with copy actions and English/Chinese labels. Content is formatted only when expanded, and text length, nesting, and visited values are bounded so a large payload cannot stall the panel. (#5309)
  • frontend: Conversations can be referenced from the composer. A "Reference a conversation" button sits beside the attachment button, rendered only while /api/features reports the capability, and opens a picker over the same recent-conversation list the sidebar uses — never offering the current conversation, and capped at the reported max_references (3 today), where rows past the cap are disabled and selected ones stay clickable to remove. Attached conversations appear as removable chips in the composer and as read-only chips linking back to the source in the transcript. References are per message: not saved with a draft, cleared on send or thread change, and regenerating or editing a turn runs without them unless re-attached. (#5465)
  • frontend: Capability management moves out of Settings into a dedicated Capability Center in the workspace sidebar. MCP servers, connected-app authorization, and skills sat alongside account and appearance preferences, which made them hard to find and made the settings dialog hard to scan; they now live on one page, with Plugins combining MCP management and Lark/Feishu install and authorization, and Skills providing Built-in, Community, My skills, and All skills views with searchable cards, enable switches, file import, creation, and custom-skill export. Settings keeps its seven remaining sections, API contracts and permissions are unchanged, and the old ?settings=tools|integrations|skills entry points are removed. (#5468)

Observability & tooling

  • observability: Trace-id correlation with enhanced logging and agent observability via Monocle. (#3902, #4024)
  • tooling: A Hermes-like terminal workbench (deerflow CLI) backed by DeerFlowClient, plus a redacted community support-bundle generator. (#3760, #3886)
  • setup: The setup wizard now asks whether OpenAI-compatible gateway models support thinking, and a Volcengine Coding Plan quick-setup path was added. (#3428, #4141)
  • tui: clear command. (#4306)
  • tui: The TUI supports a transparent terminal background. (#4631)
  • gateway: New GET /health/ready readiness probe runs a bounded database SELECT 1 and returns 503 while the database is unreachable (200 not_configured for the memory backend); /health stays pure liveness, and the production compose healthcheck now gates on readiness. (#5166)
  • observability: Deferred tool promotions — both routing-hint auto-promotion and explicit tool_search — persist as privacy-minimal middleware:tool_promotion run events (tool names, source, count, agent attribution; no queries, schemas, or results), observed only after skill-policy filtering so denied schemas are never reported as effective promotions. (#5183)
  • client: Context compaction stores its result in ThreadState.summary_text, outside messages, but the embedded client's values event selected only the title, messages and artifacts — so a consumer outside the Gateway, such as a benchmark runner, had no way to observe the summary through the public event stream. Every embedded values event now carries summary_text, None when absent, forwarding updates, repeats and clears as state snapshots. Message serialization, AI-delta deduplication, tool artifacts and usage accounting are unchanged, and no checkpoint internals are exposed. An initial snapshot can already contain a restored summary, so a changed value alone is not a causal compaction event. (#5249)

Changed

  • frontend performance: Keep the public root and localized docs static; lazy-load closed workspace panels and editor/highlighter dependencies; incrementally derive streamed message state; bound streaming Markdown work; virtualize long message and chat lists; pause offscreen decorative effects; and enforce representative route JS/CSS budgets.
  • browser: Negotiate binary Browser Live JPEG frames, retain the legacy JSON/base64 protocol for older clients, coalesce presentation to the latest frame per refresh, and revoke replaced object URLs.
  • artifacts: Stream regular text artifacts with HTTP byte-range support and limit the initial Web UI preview to 1 MiB until the user explicitly loads the complete file.
  • sandbox: The Helm chart now defaults per-sandbox Services to ClusterIP instead of NodePort, so the code-execution sandbox is reachable only inside the cluster via Service DNS (http://sandbox-<id>-svc.<ns>.svc.cluster.local) and is no longer bound on every node's interfaces - including the externally-reachable ones on GKE/EKS/AKS. Existing chart installs flip NodePort -> ClusterIP on upgrade. To preserve the old reachability (an external probe hitting the 30xxx port, or the Docker-Compose/hybrid path where the gateway is not in K8s), set provisioner.sandboxServiceType: NodePort (with provisioner.nodeHost if needed). The provisioner itself is unchanged (mode-aware since #4016). (#4190)
  • skills: An active restrictive skill must explicitly list task in allowed-tools to delegate to a subagent. Read-only discovery infrastructure (tool_search and describe_skill) remains available, but cannot grant schema visibility or execution for a denied business tool. (#4098)
  • memory: Pre-abstraction top-level memory.* DeerMem fields (storage_path, max_facts, debounce_seconds, model_name, token_counting, staleness_*, consolidation_*, ...) are auto-migrated into backend_config on load with a warning, so an upgrade does NOT silently revert customized settings to defaults (model_name -> backend_config.model.model). Move them under memory.backend_config in config.yaml to silence the warning. (#4122)
  • memory: Added memory.mode (middleware | tool); tool mode registers memory tools (memory_search/add/update/delete) the model calls directly instead of passive per-turn summarization. manager_class resolution is now fail-fast (raises ValueError on an unknown backend instead of silently falling back). (#4023)
  • middleware: Declarative layered middleware builder; ThreadData now runs before Uploads. (#3809)
  • sandbox: The host->virtual output-masking regex now has a single owner, eliminating duplicated pattern compilation. (#4108)
  • docs: AGENTS.md is now the source of truth for agent guidance, imported by CLAUDE.md via @AGENTS.md; module guides refreshed. (#3770)
  • memory: The OpenViking memory backend now uses the official OpenViking adapter; the old trusted-mode auth_mode/account fields are rejected in favor of a credential-bound USER API key. (#4707)
  • gateway: Threads created before the run-event journal have their checkpoint history backfilled as seed events before the first new run, so legacy conversations stay visible and correctly ordered after an upgrade. (#4590)
  • agents: Subagent delegation is now routed by net benefit: the lead agent defaults to direct execution unless parallel latency, specialist capability, or context isolation clearly pays off. (#4384)

Fixed

  • persistence: Heal databases that silently skipped the run-change clock schema. 0023_run_change_seq was inserted ahead of the already-shipped 0023_user_preferences revision, so databases stamped at that revision (or later) treat it as an applied ancestor and never execute it — leaving the run_change_clock table and the runs.change_seq column permanently missing, and the first thread deletion (any run-store change-clock bump) fails with no such table: run_change_clock. The new 0025_repair_run_change_seq revision re-applies the same guarded DDL on upgrade and no-ops on healthy shapes. RunChangeClockRow and UserPreferenceRow are also registered in the ORM model registry so create_all and autogenerate see every table through explicit imports instead of module side effects. Rolling back the repair to 0024_project_documents intentionally leaves the ancestor-owned schema and existing change positions intact; the repair downgrade is a no-op.
  • nginx: Extend the 600-second read timeout to the two remaining locations whose routes wait on the Gateway, both left on nginx's 60-second default by the thread-route fix. Behind the /api/ catch-all, the stateless POST /api/runs/wait blocks on the same run-completion wait and cancels its run when the client disconnects, so an API consumer waiting on a run longer than 60 seconds got a 504 and a cancelled run, and the composer's POST /api/input-polish waits for a one-shot model call. Behind /api/skills, installing a .skill archive runs one LLM security scan per file in it, and a custom-skill edit or rollback runs one more; none of them sets its own timeout, and only the sibling /api/skills/install/upload endpoint had been given the longer timeout, so the same install through POST /api/skills/install failed at 60 seconds. Applied to the Docker, local, and Helm configs. (#5524)
  • nginx: Stop thread routes that wait on a model call from failing at 60 seconds. The browser calls /api/threads/* directly, and that location had no proxy_read_timeout, so nginx's 60-second default applied while /api/langgraph/ allowed 600. A slow /compact returned 504 while Gateway kept going and still saved the compaction, so the UI showed an error for work that had been applied, inviting a retry that compacts it again. /suggestions hit the same limit, and /runs/wait cancelled its run when nginx dropped the connection. The Docker, local, and Helm configs now allow 600 seconds on that location. (#5505)
  • middleware: Stop loop detection from cutting off an agent that pages through a file. read_file calls were keyed by 200-line buckets, so every read shorter than a bucket collapsed onto its neighbours: five sequential 40-line reads hashed identically and tripped the hard stop, ending the run with a forced final answer and stop_reason=loop_capped — on exactly the ranged reads read_file's own truncation notice tells the model to make. The key now uses the exact line window, with an omitted end_line kept open-ended so a bare read and an explicit start_line=1 still share one key. Repeating a single range is still caught at the same threshold, and a read loop that varies its bounds remains covered by the per-tool frequency layer. (#5486)
  • subagents: Give max_turns the meaning operators read it as. It was handed to LangGraph as recursion_limit, which counts super-steps — one per graph node — while create_agent compiles a node for every middleware lifecycle hook, so one turn cost seven to eight steps through the subagent chain and the built-in general-purpose agent's max_turns=150 bought about 18 tool-using turns before failing as turn_capped. Every middleware added to the chain shrank the effective budget again. The executor now scales the configured turn count by the per-turn node count of the chain it actually assembled, so raising max_turns buys the turns it names. No config keys changed; existing max_turns values now grant their full budget, which can make a previously truncated subagent run longer, bounded as before by subagents.timeout_seconds and subagents.token_budget. (#5485)
  • scheduler: Enforce the global max_concurrent_runs budget on SQLite, which previously only held on Postgres. Claiming a queued occurrence counts the executing rows and then promotes one row to launching, and Postgres serializes that pair with an advisory lock. SQLite's deferred transaction reserved the writer only at the promoting UPDATE, so claimants racing on distinct rows — a manual trigger overlapping the poller, or a second Gateway process sharing the database file — all read the same stale count, all passed the budget check, and the configured cap was exceeded. (#5469)
  • sandbox: Stop AIO's glob from reporting an exactly-full result as truncated. Its include_dirs branch returned as soon as it had collected max_results matches, so a listing that held exactly that many — and no more — came back flagged as cut off, and the tool told the model the result was incomplete. That branch already holds the whole listing, so it now looks one match past the cap before deciding, matching the sibling include_dirs=False branch, which has always decided from the full list. This concerns the filtered-match cap only: the raw-output cap parse_remote_search_output owns is a separate limit with its own one-line-past accounting, and the other providers' filtered-match cap is unchanged. (#5449)
  • middleware: Stop a guard that removes tool calls from breaking every later turn of a Claude or OpenAI Responses thread. Token-budget and loop-detection hard stops, subagent-limit truncation, and safety suppression cleared tool_calls but left the provider's own tool-call blocks in the message content. Anthropic and the Responses API resend those blocks, so the next request carried a tool call with no result and the provider rejected it, and a hard stop saved to the checkpoint kept failing on each new message. All guards now remove the matching content blocks through one shared helper, which also keeps a Responses call that clarification retains. (#5447)
  • sandbox: Stop remote glob and grep from reporting "no matches" when their output was cut off. BoxLite, Tenki, E2B, and OpenSandbox cap the search's raw output and then filter it in Python (ignored directories such as node_modules, the pattern or glob scope), but they reported truncated only when max_results was reached. When the capped lines were all filtered out, a search with real matches past the cap came back empty and complete. The search now passes one line beyond its cap so a cut-off result is reported as truncated, and the glob and grep tools say an empty truncated result is incomplete instead of "No matches found". (#5427)
  • sandbox: Stop host paths reaching the model when output joins them with :, as $PATH and $PYTHONPATH do. The matched path ran on through the rest of the list, so every later entry under the same root was left unmasked; extra masking passes recovered one entry each, which hid the leak for short lists. Masking now ends a matched path at :. A symlink inside a mount whose target lies outside every mount is now shown by its mount path instead of the target's host path in command output and glob results. (#5418)
  • sandbox: Stop BoxLite grep from ignoring the directory part of glob. It compared only file names, so src/*.js matched every .js file in the tree. The glob now applies to the path relative to the search root, the same scope as glob() and the other providers. (#5419)
  • models: Stop every Claude model after the first from losing its credential when the Claude Code OAuth token is handed off through CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR. Every ClaudeChatModel instance loaded credentials again, but a descriptor can be drained only once, so the title, summarization, and subagent models — and every later run — had no credential and failed with TypeError: Could not resolve authentication method. The token is now read once per process and reused. (#5411)
  • models: Stop the lead agent from failing to build whenever a model with supports_reasoning_effort: true also gets a reasoning_effort from its profile — at the top level, in when_thinking_enabled or when_thinking_disabled, or from the extra_body.thinking disable path. The regular lead-agent build forwards the requested effort even when unset, so the key reached the provider constructor twice and raised TypeError: got multiple values for keyword argument 'reasoning_effort'. The requested value now layers like per-agent model_settings: it replaces a top-level profile value, an unset request keeps that value, and the thinking-mode settings still decide the final one. Codex keeps its own level check. (#5403)
  • runtime: Stop a keyed run retry from failing with 500 on the SQL run store. HTTP admissions do not pass a user_id; the SQL store stamps the request user on the row, but the process-local run record kept None. A retry with the same Idempotency-Key that reached another Gateway worker, or the same worker after the finished run was cleaned up, compared the two owners, took its own run for another user's, and raised. The same mismatch dropped HTTP runs from owner-scoped history reads and skipped the MCP background_tasks projection for them, so values events for these runs now include background_tasks. RunManager now resolves an omitted owner from the request user the way the SQL store does, so every store records the same owner. (#5401)
  • runtime: Stop a cross-worker idempotent run reuse from permanently blocking the thread on the reusing worker. The reuse registered the hydrated store row as a local run record, but only the owning worker finalizes and cleans up its records, so the copy kept its admission-time pending/running status forever: every later reject admission for that thread on the worker returned 409 until a restart, its run reads kept reporting the stale status, and orphan reconciliation skipped the run if the owner crashed. A cancel sent to that worker also took the local-owner path and marked the owner's still-running row interrupted. The reusing worker now returns a detached store-only handle instead, so cancel follows the non-owner contract. (#5393)
  • skills: Stop writing resolved secrets into extensions_config.json when a skill is toggled. The Gateway skill toggle and DeerFlowClient.update_skill loaded the file through ExtensionsConfig.from_file(), which replaces every $VAR value with the environment value, and wrote that model back — so a "$GITHUB_TOKEN" reference was persisted as the plaintext token and an unset variable was permanently replaced with "". DeerFlowClient.update_mcp_config did the same for every key other than mcpServers. These writers now edit the raw on-disk JSON and validate the candidate the way the runtime loads it, so placeholders and hand-written structure survive; the MCP router shares the same raw loader. Files rewritten by an earlier toggle keep their plaintext values: restore the $VAR references and rotate the exposed credentials. (#5357)
  • gateway: Honor disable_clarification and github_token only for internally-authenticated callers, the way non_interactive already was. Both keys were forwarded from body.context regardless of the caller and were not scrubbed from the free-form body.config that the run config copies verbatim, so any session or PAT caller could set them. disable_clarification is the stronger of the two: ClarificationMiddleware answers every clarification — risk_confirmation included — with "proceed without asking", and SandboxMiddleware reads it as the same non-interactive signal as non_interactive. github_token reached runtime.context, where the bash tool exports it as GH_TOKEN/GITHUB_TOKEN, and a copy smuggled through body.config['configurable'] was persisted in the checkpoint store. The scheduler, IM channels, and the GitHub webhook channel authenticate over the internal request channel and are unaffected. (#5338)
  • artifacts: Keep PUT /api/threads/{id}/artifacts/{path} confined to /mnt/user-data/outputs. The outputs-only guard was a string-prefix check on the raw path, so a percent-encoded .. (outputs/%2e%2e/uploads/x.txt) — which nginx forwards untouched and Starlette decodes — passed it, and the resolver only confines to user-data/, letting a caller overwrite a sibling upload or workspace file in their own thread. Dot segments are now collapsed before the prefix check, and the resolved host path is re-checked against the resolved outputs root so a symlink planted inside outputs/ cannot redirect the write either. The rule now lives in one shared helper that IM-channel attachment delivery uses as well, so the two copies cannot drift. (#5321)
  • gateway: Stop persisting a caller-supplied deerflow_trace_id on the run record. body.metadata reaches both the live run config, which the run worker restamps, and the run record echoed verbatim by the runs API; only the first was covered, so a client could make the most durable surface of a run disagree with the X-Trace-Id and the log lines from the same request. The id is now stamped once at the trust boundary, config.context is closed off the same way, and a thread's own metadata is no longer seeded with the run-scoped id of whichever run created it. (#5119)
  • gateway: Expose X-Trace-Id in Access-Control-Expose-Headers. It is not CORS-safelisted, so split-origin browser clients — the ones that cannot read the Gateway's logs either — could not read the correlation id they are meant to quote in a bug report. (#5119)
  • gateway: Keep X-Trace-Id on unhandled-exception 500s. Starlette's ServerErrorMiddleware emits those through the raw send outside every user middleware, so the 500 for a server bug — the response most in need of correlation — was the only one shipped without the id. TraceMiddleware now sends its own 500 carrying the header before re-raising; the server's exception logging is untouched and mid-stream failures propagate unchanged. This fallback is emitted outside CORSMiddleware and stays CORS-opaque, so split-origin browser clients cannot read the id on this one response — same as the ServerErrorMiddleware 500 it replaces. (#5119)
  • gateway: Strip a forged deerflow_trace_id from the persisted request echo. body.config is stored verbatim as runs.kwargs_json and served back by the runs API, so a forged id in config.metadata or config.context survived on that one surface while every other carried the real id. redact_config_secrets now drops the key from both containers, and build_run_config merges run metadata onto a copy so the server-stamped id can no longer be written through into the caller's request body. (#5119)
  • artifacts: Keep explicit full-file loading scoped to the source thread, so a same-path artifact in another conversation keeps its 1 MiB preview. (#4634)
  • sandbox: SandboxAuditMiddleware no longer blocks ordinary command substitution that only captures output. The rule now judges position instead of matching any $(: x=$(curl url), echo $(curl url), an argument, and a for word list all run normally, while a substitution in command position ($(curl url), after a |/&&/;, behind leading assignments or an env/nohup/time style wrapper, or as an eval/source argument) still blocks because it executes fetched content. An interpreter's code-string flag (bash -c, python -c, perl -e, node -p, php -r, and the <<< here-string) is treated as an execution context wherever it appears, so bash -c "$(curl url)" blocks; source <(curl url) and the backtick spelling of eval/source now block too, neither of which was detected before. An unquoted newline separates statements like ;, so echo hi followed by a new line starting $(curl url) blocks as well, while heredoc bodies are consumed as data — writing a file whose content happens to start a line with $(curl url) is not a command. Variable expansions whose name merely starts with a risky executable ($shell, $bashrc, $python_version) and lookalike binaries (shellcheck, shasum) are no longer false positives. (#4611, #4623)
  • mcp: Isolate Settings > Tools enable/disable updates to one MCP server, so an unrelated disallowed stdio command no longer blocks every switch; allow disabling a disallowed target while still rejecting its re-enable, preserve the raw extensions config, honor the MCP-spec transport alias when enabling SSE/HTTP servers, surface backend validation details in the UI, and atomically replace the shared config for MCP, skill, and embedded-client updates so interrupted writes cannot leave it truncated. (#4574, #4577)
  • runtime: Thread metadata now switches to running only after the run passes the startup barrier, so pending-cancelled runs no longer briefly project running; clients may observe the prior thread status during worker startup. (#4450)
  • runtime: Re-check orphan candidates through an atomic, lease-aware takeover claim so a successful heartbeat after the scan keeps the run active and only one reconciler reports recovery. (#4424, #4434)
  • skills: Apply allowed-tools only to slash-activated or actually loaded lead-agent skills, preventing passive enabled skills and evaluation fixtures from removing MCP, web, file, and delegation tools from every run. (#4095, #4098, #4192)
  • models: Honor api_base on every BaseChatOpenAI subclass (VllmChatModel, MindIEChatModel, PatchedChatMiMo, PatchedChatStepFun, PatchedChatMiniMax), not just ChatOpenAI / PatchedChatOpenAI. Those five previously dropped the configured endpoint silently and then failed every request with an opaque unexpected keyword argument 'api_base'; the unknown-config-key warning was disabled for them as well. Both now gate on issubclass(BaseChatOpenAI). (#4146)
  • agents: Coalesce SystemMessages before the LLM request; ensure a visible response after tool runs; avoid a default LLM title call before stream end; reserve ellipsis room so the local title respects max_chars; and snap the tool-output tail forward so fallback truncation respects max_chars. (#3711, #4033, #3885, #4052, #4017)
  • agents: Skip dateless reminders in the dynamic-context date scan; load SOUL.md from agent dirs without config.yaml; require config.yaml in update_agent's legacy-agent guard; and refuse empty SOUL.md updates. (#3685, #4136, #4166, #4219)
  • middleware: Window the loop-detection tool-frequency counter so long runs no longer false-trip; prevent the title middleware from streaming tokens; fix positional fallback consuming an unrelated todo when the same-content list is exhausted; acquire the token-budget lock across _apply, before_agent, _clear_run_state, and _drain_pending_warnings; drop orphan ToolMessages so strict providers don't 400; sanitize invalid tool-call arguments; and recover from empty tool-call names and malformed tool-call ids in dangling repair. (#4072, #3566, #3709, #3714, #4080, #4193, #4008, #4246)
  • subagents: Inherit LoopDetectionMiddleware and summarization middleware so tool loops break and steps are captured; surface the turn-budget cap as MAX_TURNS_REACHED with a partial result; unify guardrail caps on the additive stop_reason + token_budget; inject durable context before compaction; preserve the parent checkpoint namespace; prohibit the task tool in the general-purpose system prompt; re-buffer subagent events on flush failure to avoid losing steps; and fix the lost loop_capped stop reason when a subagent's run_id is None. (#3931, #4009, #3949, #3980, #4040, #4215, #4161, #4082, #4059)
  • memory: Harden against null/empty edge cases - skip whitespace-only facts; coerce null confidence / source.confidence in updates, searches, and the three remaining raw reads; treat explicit null backend_config values as omitted; fix KeyError / UnboundLocalError when a fact has no id or the facts list is empty; stop the busy-spin in the debounced update queue; and flush the memory queue on graceful shutdown to prevent loss. (#3719, #4074, #4076, #4034, #4217, #3993, #3992, #4073, #4181)
  • runs: Close multi-worker ownership gaps in run atomicity; fail-stop local execution when lease renewal cannot be confirmed before its deadline and fence late completion writes after peer takeover; degrade cancel to lease takeover for multi-worker; keep create_thread idempotent when the insert loses a race; read stop_reason from runtime context; and persist run duration in checkpoints for history reads. (#4003, #4064, #4414, #3800, #4188, #4118, #4431)
  • runtime: Serialize SQLite event-store writes to prevent per-thread sequence collisions; skip hidden human messages in the journal; and drop the silent delta-discard in _merge_stream_text. (#4077, #3698, #4085)
  • gateway: Attach thread-message feedback by real event_type; offload blocking filesystem IO in artifact serving, gateway uploads, and the Discord channel; limit the uploaded-file context manifest; and live-tail malformed Redis reconnect ids. (#3651, #3551, #3935, #3927, #3917, #4012)
  • uploads: Claim the converted-Markdown companion filename before writing it, so two convertible uploads sharing a stem (or a convertible plus a same-stem .md upload) no longer silently clobber each other within one request. When uploads.auto_convert_documents is on, the companion .md now gets a unique name (e.g. a_1.md); POST /threads/{id}/uploads and DeerFlowClient.upload_files both report the actual name in markdown_file. (#4288)
  • config: Coerce null object config sections to their defaults; honor the unified database configuration in the store and sync checkpointer; and have legacy DB backfill create missing Index objects on existing tables. (#3573, #3904, #3994, #4090)
  • models: Apply the stream_chunk_timeout default to all BaseChatOpenAI subclasses; and normalize api_base -> base_url for ChatOpenAI with a warning on unknown config keys. (#4102, #3790)
  • mcp: Isolate tool-discovery failures per server; synchronize the session-pool singleton lifecycle; invalidate the tools cache on config content
    • path (not just newer mtime); validate MCP tool names at load so deferred prompts stay inert; and route tools by source server, not name prefix. (#3772, #3797, #4124, #4154, #3812)
  • skills: Activate a slash skill once per run, not per model call; close the skill-install security-scan coverage gap; recognize fully deleted skill packages in review CI and remaining requests / httpx methods as network sinks in SkillScan; reuse the resolved app config in the no-arg skills prompt section; and reload mounted skills without restarting the Gateway. (#4103, #3924, #4169, #4130, #4160, #4264)
  • sandbox: Guard the reverse path-translation and output-masking regexes with segment boundaries; handle one-sided line ranges and empty files in read_file / str_replace; align the AIO bash working directory; use os.sep in the reverse-resolve containment check on Windows; normalize Windows backslash paths in bash commands; stop glob / grep / ls from surfacing disabled skills' files; and allow valid heredoc commands in the sandbox audit. (#4035, #4053, #4078, #4079, #4051, #4058, #3869, #4096, #3786)
  • sandbox: Synchronize the sandbox provider singleton lifecycle (with concurrency regression tests) and keep k8s calls off the event loop in the provisioner. (#3730, #3941)
  • sandbox: Align sandbox artifact mounts with the channel user; fix local-dev (make dev) on non-root / NFS hosts; reap macOS nginx processes on stop; and fix production Postgres UV-extras detection in Docker. (#3729, #3590, #3828, #3897)
  • channels: Validate the channel provider before resolving its config; dedupe GitHub webhook redeliveries and drop redundant GitHub review-comment webhook fan-out; scope the slash-skill whitelist check to the run's owner; batch Feishu file messages into one thread and dispatch Feishu group commands prefixed with a bot @mention; accept leading @mentions before /connect bind codes and don't treat a bare "connect" as a bind command; stop Feishu from creating thread topics and throttle card updates; let the UI runtime channel config win over config.yaml; fix require_mention gating on whitespace-only bot_login / mention_login; guard null quote fields in WeCom; and key inbound dedupe on chat-scoped workspaces so Telegram, Feishu, WeChat and DingTalk redeliveries stop re-running the agent on a default (unbound) configuration, releasing the dedupe key on transient failures so a redelivery can still recover. (#4100, #4104, #4131, #4129, #3753, #4229, #4222, #4251, #3810, #3674, #4055, #4069, #4287)
  • frontend: Preserve messages and durable context across summarization; preserve artifacts and stabilize artifact paths during streaming; resolve relative artifact image paths; retain presented artifacts in the header dropdown; keep orphan tool messages visible; show assistant text during tool steps; reset new chat on client-side navigation; prevent stream cancellation on concurrent submit; fix stale-run reconnect and cancel handling; fix chat math rendering, single-tilde markdown, double reasoning rendering, UTF-16 markdown binary classification, and <memory> tags in Streamdown; make recent-chat rows fully clickable; validate attachment limits before upload and fix uploaded-file metadata in message copy; fix mobile workspace and accessibility blockers, the card tool-message bug, and side-chat toolbar / panel-button behavior; block unresolved suggestion-template placeholders; refresh notification permissions; show the branch action only for completed turns; enable regenerate in custom agent chats; and generate a fallback title for interrupted first-turn runs. (#3826, #3791, #4094, #4038, #3854, #3880, #4114, #3673, #3878, #3908, #3557, #4245, #3870, #3966, #4209, #3733, #3900, #3944, #3740, #3976, #3959, #3961, #3764, #3768, #4147, #3967, #3874, #3644)
  • tui: Interrupt an active run before /quit exits. (#4235)
  • harness: Don't flag the outline as truncated at exactly MAX_OUTLINE_ENTRIES headings. (#3856)
  • tracing: Attach Langfuse trace metadata to the goal evaluator. (#4202)
  • context: Resolve the context-compress bug. (#4065)
  • threaddata: Fix AttributeError when runtime.context is None. (#3989)
  • goal: Stop continuation_count double-bump during stand-down. (#4199)
  • circuit-breaker: Stop wedging after a non-retriable half-open probe. (#3991)
  • github: Match allow_authors logins case-insensitively. (#4218)
  • community: image_search now returns the full-resolution image URL. (#3990)
  • skills: Offload blocking filesystem IO in the skill-history endpoint. (#3563)
  • skills: Don't treat a lazily evaluated PEP 695 type alias as a network sink in SkillScan. (#4315)
  • skills: Bound skill-archive extraction by member count, not only by uncompressed size. safe_extract_skill_archive() is the always-on path every .skill install goes through; it capped total uncompressed bytes as a zip-bomb-by-size defence but had no member limit, so a small archive holding tens of thousands of tiny entries extracted cleanly. scan_archive_preflight() already capped entries at 4096, but only ran when the optional skill_scan.enabled kill switch was on, leaving the default path uncapped. Extraction now enforces the same 4096-member cap unconditionally, raising a plain ValueError; where SkillScan is enabled the structured package-too-many-members finding still surfaces first. (#4241)
  • tracing: Resolve the Langfuse trace user from runtime context. (#3794)
  • guardrails: Propagate internal owner attribution into the guardrail context. (#3839)
  • subagents: Clamp the subagent limit consistently with MIN_SUBAGENT_LIMIT. (#4081)
  • subagents: Load user-scoped skills. (#4356)
  • mcp: Per-server fail-soft OAuth priming, and persist rotated refresh tokens. (#4084)
  • mcp: Ignore malformed path-like text. (#4456)
  • auth: Resolve email accounts case-insensitively. (#4101)
  • auth: Recover from setup-status timeouts. (#4371)
  • scheduler: Close a dispatch race that could launch two runs for one scheduled task. (#4105)
  • channels: Buffer and drain GitHub comments queued during a busy run. (#4133)
  • channels: Escape Slack reserved characters before mrkdwn conversion. (#4197)
  • channels: Check response.success() on Feishu card/reaction SDK calls. (#4234)
  • channels: Drop inbound DingTalk messages that carry no conversation identity. (#4316)
  • channels: Receive inbound Telegram attachments. (#4392)
  • memory: Consolidated facts inherit expected_valid_days from their sources. (#4225)
  • config: Sync _memory_config with AppConfig auto-reload. (#4208)
  • postgres: Harden the async engine with pool_recycle and command_timeout to stop stale-connection 504s. (#4230)
  • harness: Add a timeout to invoke_acp_agent to prevent indefinite hangs. (#4238)
  • community: Surface the target-page error status in web_fetch (Browserless). (#4239)
  • sandbox: Widen the BoxLite/AIO tenant hash and verify identity on reclaim. (#4171)
  • sandbox: Make an empty old_str a no-op in str_replace on any file. (#4256)
  • sandbox: Serialize E2B release transitions. (#4355)
  • sandbox: Bound E2B output-synchronization resources. (#4364)
  • sandbox: Unwrap Overwrite-wrapped sandbox state in after_agent. (#4381)
  • sandbox: Bypass proxies for local AIO traffic. (#4444)
  • models: Surface length-capped model responses instead of dropping them. (#4309)
  • streaming: Keep large file generation responsive. (#4354)
  • streaming: Expose custom events to astream_events. (#4403)
  • streaming: Signal replay history gaps. (#4426)
  • summarization: Summarize with the run model and fall back on summary-provider failure. (#4361)
  • runtime: Remove transient image context after model calls. (#4267)
  • runtime: Stop subgraph stream frames from impersonating root frames. (#4407)
  • runtime: Reject unsupported run options and stream modes. (#4430)
  • runtime: Serialize checkpoint writes with active runs, linearize delta-mode checkpoint resume, and accept the SDK's default stream_resumable=false to avoid resume races. (#4437, #4460, #4468)
  • checkpoint: Unwrap Overwrite first writes into empty channels. (#4383)
  • nginx: Allow long chat prompts through /api/langgraph/ without a raw 500. (#4277)
  • gateway: Prefer X-Trace-Id over metadata.deerflow_trace_id when the header is set. (#4283)
  • gateway: Seed branch run-events so inherited history survives forking. (#4385)
  • gateway: Scope branch-history seed run ids per inherited turn. (#4459)
  • frontend: Harden artifact and markdown rendering. (#4117)
  • frontend: Classify a symlink replacing a file distinctly from deleted in workspace-change review. (#4170)
  • frontend: Offload blocking filesystem IO in the workspace-change text-cache lifecycle. (#4268)
  • frontend: Encode artifact URL path segments. (#4278)
  • frontend: Clarify run-duration display. (#4348)
  • frontend: Preserve regenerate state in branched threads. (#4358)
  • frontend: Default the reasoning-effort label to Medium when unset. (#4373)
  • frontend: Strip and parse the <current_uploads> upload-context tag. (#4402)
  • frontend: Keep leading orphan tool messages visible. (#4408)
  • frontend: Keep completed subtask cards stable after reload. (#4432)
  • frontend: Apply message-image maxWidth via inline style. (#4446)
  • frontend: Restore resizing for the artifacts and sidecar panels. (#4469)
  • frontend: Allow dev-server access from non-localhost hosts. (#4471)
  • safety: Backfill empty content-filter responses so they don't poison the thread. (#4394)
  • tools: Exclude injected runtime from the list_uploaded_files schema. (#4376)
  • mcp: Bound MCP server bring-up — tool discovery (subprocess spawn + initialize + tools/list) and persistent stdio session initialization — with a new per-server session_init_timeout (default 60s, null disables), so a hung stdio server can no longer block agent construction, or the whole Gateway event loop, indefinitely. tool_call_timeout still bounds individual stdio tool calls. (#4657)
  • runtime: Tool-output budget externalization no longer trips run delivery verification. The default .tool-results storage dir (and any custom tool_output.storage_subdir) is excluded from workspace-change snapshots and produced-artifact detection, so a run that only externalized oversized tool outputs succeeds instead of failing as an error. (#4657)
  • frontend: Hide stale follow-up suggestion chips while a turn is still streaming. (#3396)
  • frontend: Fix streaming render glitches: stop the word animation from replaying, keep step text stable, preserve message order during long runs, and keep reasoning above the answer. (#4266, #4510, #4513, #4578)
  • frontend: Encode thread IDs in chat routes so IDs with special characters no longer break navigation. (#4302)
  • frontend: Render citation links from React children. (#4486)
  • frontend: Localize conversation export failure messages. (#4493)
  • frontend: Sync side panel state when a drag collapses the panel. (#4556)
  • frontend: Render one workspace-change card per run instead of duplicates. (#4559)
  • frontend: Refresh the active artifact's content when it changes. (#4584)
  • gateway: Reject non-positive read limits in API requests. (#4284)
  • gateway: Handle a null config.configurable when resolving the thread id instead of failing. (#4301)
  • gateway: Unify thread id validation across API routes. (#4589)
  • gateway: Merge concurrent thread metadata updates instead of letting them silently overwrite each other's changes. (#4489)
  • gateway: Expose the run metadata response header to cross-origin clients, so a split-origin frontend learns new run ids instead of staying stuck on the new-thread placeholder route until reload. (#4535)
  • gateway: Replay edit and rerun from a settled checkpoint so the edited prompt actually runs (previously a first turn's edit replayed the original prompt and vanished after reload), and keep a manual rename through the rerun. (#4534, #4539)
  • runtime: Cancel a run from any live gateway worker, not only the one that owns it, so the stop button no longer depends on request routing. (#4500)
  • runtime: Close a replacement run when interrupt or rollback admission is cancelled mid-flight, instead of stranding an unseen active run on the thread. (#4472)
  • runtime: Regenerating a response now preserves the thread's current title and supports the latest interrupted response whose partial message never reached a checkpoint. (#4480, #4524)
  • agents: Classify web_fetch error pages such as 404s as errors rather than successful evidence, so retries and stagnation guards can react. (#4314)
  • agents: Handle XML-to-dict option shapes when normalizing clarification choices. (#4527)
  • subagents: Run delegated subagents with isolated callbacks and lazy skill activation, fixing cross-event-loop failures and passive skills stripping baseline tools like write_file. (#4497)
  • sandbox: Handle overwrite-wrapped state when ensuring the sandbox is initialized. (#4429)
  • sandbox: Reconcile E2B sandboxes safely: pick the first healthy candidate, adopt the canonical instance per user and thread, defer a peer's live duplicates, and reap orphans after a grace window. (#4443)
  • sandbox: Claim ownership before destroying a sandbox that failed its readiness check, so a peer gateway can no longer adopt the not-yet-ready sandbox and kill a live turn. (#4505)
  • sandbox: Allow grep to search a single file. (#4512)
  • sandbox: Enforce the E2B capacity limit deployment-wide when sandbox ownership uses Redis, so multiple gateways cannot create past it. (#4575)
  • skills: Activate managed integration skills from the managed integrations root on slash invocation. (#4570)
  • skills: Offload blocking filesystem IO when updating a skill and serialize concurrent writes. (#3565)
  • mcp: Ignore oversized path-like text. (#4582)
  • memory: Harden long-term memory: reject duplicate facts inside the create critical section, truncate injected mem0 context on entry boundaries, and keep task-scoped instructions such as "inspect only" out of long-term memory. (#4599, #4600, #4604)
  • scheduler: Keep a successfully launched scheduled run's slot and run id when post-launch bookkeeping fails, preventing a later dispatch from launching a duplicate run. (#4504)
  • config: Treat a deleted extensions config file as absent instead of raising, so tool and skill config resolution keeps working. (#4275)
  • config: Normalize the postgres:// short scheme for the async ORM engine. (#4293)
  • console: Disable cost reporting when model pricing mixes currencies instead of reporting a meaningless cross-currency total. (#4564)
  • browserless: Accept the timeout config key and harden its coercion. (#4519)
  • docker: Send Connection: upgrade only when the browser requests it, fixing login-page refresh loops when the Docker dev stack is accessed via a remote host. (#4250)
  • runtime: Group JSONL batch event writes by run, so a batch covering several runs no longer lands all events in the first run's file and makes later runs unreadable through per-run APIs. (#4938)
  • runtime: Restore standalone LangGraph Studio compatibility: the graph entrypoint and file-based app load again, the Studio identity can discover system assistants, and the documented langgraph dev workflow works. (#4760, #4838)
  • gateway: Stamp turn_duration on a run's last AI message only in /messages/page, so multi-step turns no longer repeat the same run lifetime as thinking latency on every intermediate message. (#4755)
  • gateway: Preserve exact history attribution beyond the event page limit, so older AI messages on long-lived threads are no longer credited to a later turn's run and duration. (#4953)
  • gateway: Reject MCP task cancellation with HTTP 503 when the task worker is stopped, instead of acknowledging a cancellation that would never run. (#4963)
  • middleware: Correct four context-handling defects: fallback dynamic-context injection targets the latest user message instead of resurrecting an old prompt as the current turn; bare string blocks in list-form user content are sanitized like all other user text; duplicate placeholders are no longer emitted for the same invalid tool call; and summarization no longer compresses away the current request's user message while leaving the previous turn's behind. (#4667, #4668, #4693, #4882)
  • middleware: Restore the system-prompt injection that teaches the model about the write_todos tool, which the todo middleware's model-call override had silently dropped. (#4735)
  • agents: Make SQL agent-store signatures content-sensitive, so an agent update that reuses its previous timestamp no longer leaves the GitHub agent registry serving stale webhook routing. (#4709)
  • tools: Resolve presented files with the runtime user, so present_files no longer rejects valid artifacts as outside the outputs directory when the request user context is unavailable. (#4677)
  • tools: Retain a strong reference to deferred subagent cleanup tasks, so garbage collection can no longer destroy a pending cleanup and leak cancelled subagent records, locks, and memory. (#4928)
  • subagents: Give every background subagent run a server-side execution ID, so concurrent runs that reuse a provider tool-call ID can no longer overwrite, poll, or cancel each other's state. (#4758)
  • harness: Offload ACP workspace creation and MCP config loading from the event loop, so invoking an ACP agent no longer raises blocking-IO errors or stalls other async work. (#4965)
  • mcp: Reject non-finite poll_after_seconds values on task snapshots when they arrive, so a bad polling interval no longer crashes scheduling and persistence after a successful poll. (#4750)
  • mcp: Keep the configured grant_type authoritative over extra_token_params during OAuth token exchange, so extra parameters can no longer silently switch the configured flow and be rejected by the token endpoint. (#4860)
  • runtime: A slow or hung stdio MCP server no longer stalls the whole Gateway. Agent construction assembled MCP tools synchronously on the event loop, so a wait for an in-flight MCP initialization blocked SSE delivery, run cancellation, and timers for every other request, not just the caller waiting for its tools. Tool assembly is now dispatched to a worker thread at each async entry point: the subagent spawn path in task_tool, SubagentBatchService._execute_item for durable batches, run_agent's agent factory (which covers both get_available_tools call sites in lead-agent assembly), and the checkpoint state-accessor build, whose cold-cache reads pay the MCP-init wait in the worker thread instead of stopping the loop. The four offloads ride a dedicated bounded pool (run_assembly() in utils/assembly_io.py, 8 workers, overridable with DEER_FLOW_ASSEMBLY_WORKERS) rather than the loop's default executor, so a worker parked for the full MCP timeout cannot queue every other to_thread caller behind it; contextvars are copied across the hop so the extension build-context snapshot still propagates, and pending assemblies are logged with a throttled warning once they exceed the worker count. Because assembly can now be parked, a durable batch re-checks its item's durable state immediately before launching, so a batch cancelled during assembly no longer starts a model call. (#5217, #5224)
  • agents: LoopDetectionMiddleware no longer spends one turn's budget on another turn's legal work. It scoped its identical-call hashes and per-tool frequency windows only by thread_id, so a compiled agent reused across turns — DeerFlowClient retains the graph and assigns a fresh run_id per turn — counted earlier turns' ordinary calls toward the later turn's limit, producing a false loop warning on the third separate turn and stripping a legitimate tool call on the fifth at the default identical-call thresholds. State is now scoped by (thread_id, run_id), with evidence still accumulated across repeated graph entries inside one run, including hidden goal continuations, and independent warning queues for concurrent sibling runs. LRU eviction drops a whole run scope, reset(thread_id) still clears every scope that thread owns, and thresholds and warning/hard-stop behavior are unchanged. (#5344)
  • agents: A run's token_budget.max_tokens cap now holds across the hidden continuations of an active /goal. The worker re-enters the graph under the same run_id, and the middleware cleared that run's usage in after_agent and then marked every existing message as already seen, so each continuation started counting from zero: with max_tokens: 10000, a turn that hit the hard stop at 12k was followed by a continuation that kept calling tools, and the run reported stop_reason: token_capped while spending 20k tokens and running two more tool calls after the cap. Usage and warning state now survive graph entries with the same run_id — as loop detection already did — while a later user run still gets a fresh run_id and a fresh budget. After a cap, a continuation still makes one model call before its tool calls are stripped. (#5410)
  • middleware: A Human Input Card reply is now recognized as the user's current request. The card answer arrives as a hidden HumanMessage carrying a valid human_input_response, and the turn-detection helpers used is_real_user_message, which rejects every hidden message with no carve-out — so a run started from a card reply kept the older visible request as "current" while the answer itself was treated as a framework injection. In summarization, the current-request rescue therefore locked onto the stale message: after compaction the model saw the original request word for word and the user's region and year constraints only inside the summary. In McpRoutingMiddleware, a routing keyword that appeared only in the card answer matched nothing, so the deferred MCP tool was never auto-promoted and the model had to call tool_search by hand — the identical answer sent as a visible message promoted it, so behavior depended only on how the answer was transported. Both now use is_genuine_user_message, which skips hidden messages unless they carry a valid human_input_response; hidden messages without one still stay skipped. (#5416, #5426)
  • goal: A goal that a token-capped run already satisfied is no longer followed by a pointless continuation. Now that continuations share the run's token budget, a continuation queued after the hard stop made one model call and then had its tool calls stripped, so it could not make progress — yet the worker still queued it, paying for an evaluator call and a model call each time until the continuation or no-progress limit stopped the goal. run_agent now passes the run's stop_reason into goal-continuation preparation, and when it is token_capped the goal stands down with stand_down_reason: "token_capped" instead of queueing another continuation. The evaluator still runs first, so a goal the capped run did satisfy is still cleared, and other stop reasons behave as before. (#5424)
  • agents: A retried model call no longer reaches the provider without the warning a guard middleware had already queued for it. LoopDetectionMiddleware, TokenBudgetMiddleware, and ToolProgressMiddleware drain their queued warning or hint inside wrap_model_call before calling the handler; because LLMErrorHandlingMiddleware wraps them and retries by calling its handler again, the second attempt ran with an empty queue while the warning had already been marked sent. A model that looped on one tool call and failed once with a 503 on the request carrying the warning never saw it and ran on to the forced stop. All three now put the drained warnings back at the front of the queue when the handler raises, so the retry picks them up; successful calls are unchanged, per-run caps still apply, and warnings are still dropped at after_agent if the run ends without another model call. (#5433)
  • agents: The token budget works again for runs that have no run_id — LangGraph Server, langgraph dev, and direct create_deerflow_agent callers. Two defects rode the runtime-local fallback key. A subagent's hard stop was stored under the id string but read back with consume_stop_reason(None), because SubagentExecutor propagates the parent's run_id and the parent has none, so a token-capped subagent was reported to the parent as a clean Task Succeeded instead of a capped failure. And LangGraph gives each node its own Runtime wrapper, so id(runtime) differed between after_model, the next wrap_model_call, and before_agent: the queued budget warning was never delivered, after_model missed the before_agent baseline and counted every AIMessage in the thread, and a second invocation on the same thread was charged with the first invocation's tokens. Invocations without a non-empty string context run_id are now keyed by LangGraph's run-scoped Runtime.control object, the same anchor loop detection uses, and the stop reason is stored under the context run_id exactly as given, None included. (#5436)
  • agents: Cancelling a read_file or write_file call can no longer strand or release the read-before-write gate's lock. asyncio.to_thread() cancellation only cancels the asyncio waiter, not an already-running worker, so a cancellation could leave a queued threading.Lock.acquire() hanging or free the gate while an off-thread probe was still running. Dispatched gate operations are now drained under asyncio.shield() before the cancellation propagates, the first CancelledError is preserved and re-raised across repeated cancellations (including when the drained worker task is itself cancelled), a lock that succeeds after cancellation is released exactly once, and the same-path gate is held until the write-check and read-mark work finishes. (#5395)
  • agents: Three create_deerflow_agent features now do what they advertise. Factory graphs were built without DurableContextMiddleware, which is what writes the delegations ledger — so SubagentLimitMiddleware always counted zero earlier delegations and only the per-response limit applied — and which is the only thing that puts summary_text back into a model request, so after the first compaction the model saw only the kept tail and the summary was lost. Separately, RuntimeFeatures(token_budget=True) built a TokenBudgetConfig() whose enabled defaults to false, so every hook returned early and there was no warning, no hard stop, and no token_capped. _assemble_from_features now always adds DurableContextMiddleware in the same position as the lead-agent chain, factory model requests carry the hidden durable-context block when the graph has a summary, delegations, or loaded skill files, and token_budget=True builds TokenBudgetConfig(enabled=True). (#5488)
  • subagents: Subagent acceptance checks no longer let an out-of-scope command count as evidence on Windows. _cd_target_in_scope() normalized bash cd targets with the host platform's path module, so on Windows an absolute POSIX path or a .. escape could be judged a safe relative target and a test run outside the checked scope could satisfy an acceptance criterion. Bash targets and configured roots are now normalized with POSIX semantics on every host, drive-qualified Windows paths are recognized and compared case-insensitively, and an unmatched drive or directory fails closed. (#5162)
  • sandbox: Concurrent subagents stopped working once their count passed the AIO image's shell-session ceiling. Each concurrent subagent gets its own persistent scoped shell, but the AIO image caps MAX_SHELL_SESSIONS at 10, so the eleventh shell evicted the oldest idle session while DeerFlow still held its scoped id — the subagent's next command then failed with 404 Session not found, and recreating sessions without raising the capacity only evicted another subagent and lost its shell state. New local containers now receive subagent_runtime.max_running + 1 (the extra slot leaves room for the lead shell) as MAX_SHELL_SESSIONS, provisioner mode forwards the same value to the sandbox Pod, and an explicit sandbox.environment.MAX_SHELL_SESSIONS set below the required capacity now fails at provider startup naming both values. When a scoped session is lost anyway — to a timeout or external cleanup — DeerFlow recreates it once, and only for the structured 404 Session not found response. (#5178)
  • view-image: view_image no longer serves a stale or missing picture for a remote sandbox image. It resolved /mnt/user-data/... to a Gateway host path, so bytes only became available after synchronization and an older host copy could stand in for the current one. Bytes are now read from the current live sandbox when one is available, without acquiring a replacement merely because a persisted sandbox ID exists, and lightweight provenance — the exact SHA-256 plus the source sandbox ID — is recorded in viewed_images instead of checkpointing image bytes or base64. A replacement sandbox that reports the image missing falls back to the synchronized host copy only when it matches both the recorded size and SHA-256, so same-size stale content is rejected; missing-file classification is provider-neutral over an explicit __cause__ chain and deliberately ignores implicit exception context. Async tool invocation and model injection now run the blocking sandbox read through run_sync_lifecycle_operation(), so a cancellation cannot tear down sandbox lease cleanup before the read has drained. (#5306)
  • subagents: A subagent keeps its instructions after its context is compacted. The executor builds the agent with system_prompt=None and puts the assembled prompt into state as the first message, so it holds the role prompt, the <report_contract> citation rules, the acceptance-criteria note, the skills index, and the deferred MCP tools and routing hints — and compaction cuts by index, so index 0 always fell in the summarized part. Every model call after the first compaction ran with no system prompt at all, and the summary did not stand in for it because it is injected as hidden data the authority contract tells the model not to follow. Dynamic-context preservation now rescues SystemMessages as well as tagged reminders and the latest user message, keeping their order so the prompt stays first and is not sent to the summarizer; when the prompt plus the current request are all that remains, compaction is skipped instead of summarizing the prompt away. The lead agent is unaffected — its prompt is the request's system message, not a message in state. (#5454)
  • subagents: A repeatedly cancelled subagent no longer leaks its execution slot permanently. SubagentExecutionCapacity.slot() increments the process-wide running count before yielding and releases it in the async context manager's finally, but a second Task.cancel() arriving while that release was blocked acquiring the capacity lock interrupted the cleanup: the task exited cancelled with _running still incremented. With max_running=1, every later native subagent then queued until timeout or was rejected even though nothing was running, violating the documented invariant that cancellation and timeout release queue and slot ownership. The final release now runs in its own task, is shielded from caller cancellation, and is drained across repeated cancellation before the cancellation propagates. (#5477)
  • worker: A delegated subagent's error no longer fails the parent run. When a subagent's model call ended in an error after its retries, the executor reported task_failed and the lead agent still answered, but the worker also saw the deerflow_error_fallback marker inside the root-level task_running custom event — each carries a subagent message with additional_kwargs — and marked the parent run error with the subagent's error text. Goal continuation stopped, and an edit-and-rerun rolled the thread back, discarding the edited question and the new answer. Custom frames no longer feed parent error-fallback detection, while the lead's own error fallback still arrives through values, messages, and updates frames; the parent run now ends success when the lead finishes. (#5407)
  • sandbox: Normalize separators in masked output tails so virtual paths are spelled POSIX-style on Windows hosts too. Both output maskers searched for the host base in a forward-slash-normalized copy of the output but sliced the matched tail out of the original text, so nested backslashes survived into the spliced result and glob results and masked skill reads came back as mixed spellings such as /mnt/user-data/workspace/pkg\util.py. Depth-1 tails happened to splice cleanly, which is why Linux CI never caught it; the two splice sites now normalize the tail before joining it to the virtual prefix. (#5247)
  • sandbox: Reverse-resolve forward-slash spellings of Windows host paths. Forward resolution spells resolved paths with /, since backslashes would break bash escape sequences like \U, but the reverse scanner that maps host paths in output back to their /mnt/... form still anchored its matches on the native backslash base — so every forward-resolved path that came back in command output or in an agent-written file failed to match and leaked the raw host path, real username and full directory tree, to the model instead of the container path the agent is meant to reference. The scanner now matches separator-agnostically, the same contract the sandbox tools already use; POSIX hosts, where the two spellings coincide, are unaffected. (#5373)
  • sandbox: Stop remote list_dir from reporting failures as an empty directory. find ... 2>/dev/null on a missing path produced empty stdout and client errors were swallowed as [], so ls told the agent the directory was (empty) — a dead sandbox, a closed client, and a nonexistent path all looked like a writable empty tree, letting the agent write over existing files or skip recovery. AIO, E2B, BoxLite, OpenSandbox, and Tenki now raise OSError on command or client failure and FileNotFoundError when nothing is listed, matching what the local sandbox already did for a path that is not a directory; a genuinely empty directory still reports (empty), and find -H keeps a symlinked root listable. (#5264)
  • sandbox: Reject a partial remote list_dir traversal instead of presenting it as complete. find exits 1 both for a missing start path and for a file or subdirectory it could not read after printing some entries, and the parser accepted 1 in both cases — so a traversal that failed partway returned the visible entries as a successful complete listing and ls handed the agent a silently incomplete tree. Status 1 with no entries still raises FileNotFoundError, while status 1 with any entries now raises OSError naming the incomplete traversal and suggesting a narrower path, the same contract remote glob already applied. Every provider sharing the remote list_dir helper inherits it without provider-specific changes. (#5422)
  • sandbox: Stop remote grep and glob from reporting failures as "no matches". The searches ran with stderr discarded and took the pipeline status from head, so a missing search root, a missing grep/find binary, or an unreadable tree printed nothing and exited 0 — and the tools told the agent "No matches found" where the local sandbox reports Error: Directory not found. A shared wrapper now records the search command's own status: a missing root raises FileNotFoundError, and any other failure — including grep 2 or find 1 after some results were already printed, since a caller cannot tell a partial result from a complete one — raises OSError telling the agent that some paths could not be read. A genuine no-match still returns []. E2B, OpenSandbox, BoxLite, and Tenki also raise on a closed client instead of returning empty. (#5380)
  • sandbox: Report read_file truncation in lines and name the line to resume from. The tool head-truncates at sandbox.read_file_output_max_chars (default 50,000) and its marker told the model to continue with start_line/end_line, but the cut was made at a character offset and the marker reported only character counts — so it almost always landed mid-line and the last line the model saw was a fragment that read as complete, with nothing saying which line to continue from. Asked for the next read of a truncated file, three models picked the right start_line in 0 of 15 attempts, landing tens or hundreds of lines off. The cut now lands on the last line boundary the budget allows, and the marker states the position in lines and names the exact continuation, as in [truncated: showing first 743 of 1828 lines (49746 of 155704 chars). Continue with start_line=744]; a ranged read reports file line numbers rather than the provider's slice-relative ones. A single line longer than the budget still cuts at the character limit, since dropping it would discard most of the budget, and the marker then points at bash rather than at a read_file call that cannot return it. (#5474, #5478)
  • sandbox: Force a UTF-8 console in PowerShell so CJK tool output is no longer garbled on Windows. The command runner captures stdout through a UTF-8 pipe reader, but Windows PowerShell 5.1 writes console output in the legacy OEM codepage — GBK on zh-CN Windows — unless explicitly switched, so every CJK character arrived as mojibake, and because the reader replaces undecodable bytes rather than raising, the corruption was silent. Every PowerShell -Command payload now sets InputEncoding, OutputEncoding, and $OutputEncoding to UTF-8 before the user command runs. (#5440)
  • sandbox: Validate the managed Lark CLI sandbox runtime in a platform-aware way on Windows hosts. The check required a POSIX executable bit, which NTFS does not preserve, so every candidate reported st_mode & 0o111 == 0 and the Gateway raised ValueError: Managed Lark CLI sandbox runtime file is not executable — leaving the managed Lark runtime unusable on a Windows development host, and on a Windows Docker host that mounts the managed Linux CLI runtime into the sandbox. POSIX keeps the strict executable-bit contract unchanged; on Windows the Linux-only artifacts are validated by content instead, requiring executable image magic on the per-arch binaries and a shebang on the launcher. (#5442)
  • sandbox: Default the Docker-outside-of-Docker sandbox port bind to loopback on Docker Desktop. DEER_FLOW_SANDBOX_HOST defaults to host.docker.internal in DooD mode, which resolves inside containers to the Docker Desktop VM gateway, and publishing the sandbox port to that address made the host socket layer reject the bind with WSAEADDRNOTAVAIL — so the first sandbox shell action failed with ports are not available even though startup had succeeded. When the Docker server is Docker Desktop and no DEER_FLOW_SANDBOX_BIND_HOST override is configured, the bind host is now 127.0.0.1, which Docker Desktop forwards; an explicit override still wins, and native Linux DooD is unaffected. (#5446)
  • sandbox: Drain the skill-sync worker before a cancellation is treated as failure cleanup. The default sync_agent_skills_async() wrapper used a bare asyncio.to_thread(), so cancelling the awaiting task returned before the synchronous sandbox mutation had finished, and SandboxMiddleware then released the execution holder while provider work was still in flight — breaking the lifecycle contract that blocking work started on behalf of a holder must drain before that holder leaves the boundary. The wrapper now runs through the existing cancellation fence, so the ordering holds for third-party and upload-based providers that do not serialize sync and release themselves. (#5350)
  • frontend: Keep a human-input card with the turn that requested it. An existing needYourHelp card could drift below the next user message during a multi-turn conversation, so the chronology became misleading and an answered request could read as part of the new turn. A pre-submit baseline message could be woven in after the newly persisted human message, a card confirmed only through REST history might never enter the checkpoint baseline, and steps already persisted for the current run could be lifted into the previous turn after an interrupt. Baseline messages are restored before the pending human message, REST-confirmed cards count as established history, and the current run's own steps stay below the message that started them. (#4892)
  • frontend: Preserve server-assigned message positions through a live content merge. Streaming updates that replaced a message's content also dropped its deerflow_seq, so ordering the server had already settled was recomputed client-side and came out wrong — a loaded history window of 1,3,5 merged with a live tail of 2,5 rendered as 1,3,2,5, and long threads (history pagination, or a conversation resumed after compaction) showed their steps out of order until a refresh. Content and position are now handled separately, the merge, compaction bridge, and render ledger share one position priority so an already-sorted upstream is not re-sorted downstream, and anomalous sequence values (null, string, NaN, non-integer, non-safe-integer) never overwrite a known position. (#5293)
  • frontend: Restore the user's input after an incremental stream reconnect. Refreshing during a later turn could let the reconnect stream replay AI and tool chunks before the current human input had entered the durable history feed, so the user's message briefly vanished and its reasoning steps were grouped under the previous turn. The active run's input is now hydrated from kwargs.input and merged with the latest durable thread state before the incremental stream is joined, deduplicated by id; if the metadata or state read is unavailable, the previous reconnect path is used unchanged. (#5428)
  • frontend: Show an agent's skill badge even when its tool groups are explicitly empty. The badge container's nullish-coalescing chain stopped at the tool-group count 0, so an agent declared as tool_groups: [] with skills: ["data-analysis"] lost its badge in the Agents gallery — even though an explicit empty tool-group list is valid and supported by the Agent API and the update_agent tool. The container now shows when either list has at least one entry. (#5326)
  • frontend: Scope composer slash-skill suggestions to the active agent. Custom-agent chats can restrict which skills they may activate, including to an explicit empty list, but the composer still offered the global enabled-skill catalog — so an agent could suggest a skill it was not permitted to activate. Suggestions now pass through the active agent's allowlist, with an explicit empty list treated as no skills available rather than briefly leaking the global catalog while the agent loads, and the same scoping applied when a saved composer draft with a selected skill chip is restored. (#5451)
  • frontend: Confirm before a sidebar chat is deleted. Choosing Delete in a recent chat's menu removed the conversation and its files immediately, with no confirmation step despite deletion being irreversible. A dialog now names the conversation and warns that deletion is irreversible, focuses Cancel, and leaves the chat intact on Cancel, Escape, or close; actions are disabled and dismissal blocked while deletion is pending, and a failed deletion keeps the dialog open with the underlying error so it can be retried by keyboard. The dialog is hosted outside the virtualized sidebar rows, so a list refresh cannot remove the retry UI after a partial deletion. (#5406)
  • agents: Keep the Custom Agent settings dialog within the viewport. Choosing Selected subagents could make the dialog taller than the window, pushing the title, close control, and Save/Cancel buttons off-screen while a long worker description filled the nested list. The header and action buttons now sit outside a single scrolling form area, subagent descriptions show a two-line preview with keyboard-accessible expansion that scrolls into view without moving focus or changing checkbox state, and long names and descriptions wrap without shrinking the checkboxes. (#5458)
  • frontend: Keep the MCP configuration dialog within the viewport. Opening a long MCP server definition in the Capability Center let the JSON textarea grow past the window, so the centered dialog clipped its title, close control, and Save/Cancel buttons and became hard to edit or dismiss on smaller windows. The dialog is now viewport-contained with more horizontal room on desktop, long JSON scrolls inside the editor, and the title, Close, Save, and Cancel stay outside the scrolling region even at very small viewports or with a long server name or validation message. (#5492)
  • frontend: Show a pointer cursor over interactive controls that were clickable while still displaying the default cursor, making their affordance unclear. One global selector now covers native buttons, role="button" elements, dropdown menu items, and command items, excluding native disabled controls and anything marked aria-disabled="true"; generated ui/ and ai-elements/ components are untouched. (#4921)
  • frontend: Localize Chinese documentation links. MDX links and Nextra cards in the localized docs were rewritten without knowing the active docs language, so a Chinese reader following a quick-start link was sent to the English page. The localized docs layout now provides the language through context and link rewriting honors it. (#5275)
  • runtime: Memory-only deployments keep their run history through scheduled cleanup. cleanup() evicted unconditionally, so an embedded consumer that built RunManager() with the documented default store=None lost completed runs from history when cleanup fired — get(run_id) returned None and list_by_thread() dropped the record, because there is no durable copy for those reads to fall back to. Eviction is now gated on a backing store, keeping the retain-forever behavior memory-only mode had before; store-backed managers still evict after the grace period. (#5453)
  • events: Read JSONL event records that contain U+0085, U+2028, or U+2029. All three readers split on those Unicode separators as if they were record boundaries, so a record carrying one was skipped on read — which reused event sequence numbers after a reopen and turned idempotent inserts into duplicates. Thread reads, run reads, and sequence recovery now split on physical line feeds; existing valid files need no rewrite. (#5429)
  • events: Keep the per-thread JSONL write lock until filesystem work settles. Cancelling a store call during its worker release dropped the lock while the append was still in flight, so a later deletion could complete before the cancelled append recreated the record, and a cancelled mixed-run batch could roll back over a write it had already acknowledged. Admitted mutations now retain their lock through file I/O, rollback, and bookkeeping before propagating the cancellation, covering put, put_if_absent, batched writes, and both deletion methods; queued callers can still cancel before admission, and unrelated threads stay independent. (#5439)
  • events: Stop JSONL thread mutations from splitting onto two lock generations across a deletion. delete_by_thread() removed the per-thread lock from its registry while still holding it, so a mutation already queued on the old lock could run concurrently with a later mutation that resolved a freshly created lock for the same thread — overlapping sequence assignment and file mutation right after a delete boundary. The registry is now a WeakValueDictionary and the entry is no longer popped, so holders and queued waiters keep the same generation alive until they drain, after which the entry disappears on its own. (#5455)
  • channels: Keep one lock generation for a channel's first-use thread creation. The cleanup removed the per-conversation lock unconditionally when the current creator exited, so a creator that failed or was cancelled released and unregistered its lock while a queued creator entered through it, and a late arrival could install a new lock and enter concurrently — two callers creating Gateway threads for the same conversation, with the later mapping write winning and adjacent messages split across the orphaned thread. The hand-managed lifecycle is replaced by the existing waiter-aware keyed lock table, so a conversation key keeps one discoverable generation through exceptions and cancellation and the idle key is reclaimed only after the last participant leaves. (#5480)
  • gateway: Read a thread's runs as the data owner, not as the authorization identity. The list, keyset-page, and single-run read endpoints filtered run rows by the caller's authorization identity, while start_run stamps them with the data identity — the two only coincide for already-safe owner values, so trusted internal callers always saw an empty runs list and 404s even on threads they were authorized to act on, and the symptom was easily misread as run loss. Internal callers now skip the per-user store filter, which thread visibility on these endpoints already authorizes, while browser and API sessions keep their exact per-user filter; the two message-read endpoints had the same conflation and are corrected the same way. (#5448)
  • gateway: Apply the same data-identity scoping to the message edit and regenerate helper paths. Those three helpers still resolved the authorization identity and passed it as the data filter, so regenerate/prepare and edit-regenerate/prepare failed with 409 for internal callers on threads they were authorized on. They now resolve their filter id the same way the read endpoints do, and browser and API sessions keep the prior per-user filter and the 409 on cross-user runs. (#5483)
  • conversation: Keep conversation-reader pages inline and say when text was dropped. A page was filled to its 20,000-character cap by cutting the last message that did not fit, and a cut suffix could never be paged back — the oldest of six 3,500-character messages came back as about 2,500 characters with has_more: false. Worse, a page over the default 12,000-character tool-output budget was externalized by ToolOutputBudgetMiddleware, so the model saw only a synopsis plus a file copy of the source text in the destination thread. Pages are now sized by the budget that actually applies to read_conversation (per-tool override, else externalize_min_chars, and fallback_max_chars), still capped at 20,000 text characters, and a message that does not fit starts the next page intact — so one operator setting stays in charge, and tool_output.tool_overrides.read_conversation tunes this tool alone. Truncated results now carry a notice asking the agent to acknowledge the omission and ask for the missing material before claiming full coverage. (#5421)
  • goal: Stop a goal loop while a clarification card is still open. The goal evaluator only reads human and AI messages, so a question asked through ask_clarification — which arrives as a tool result — was invisible to it; it judged the goal unmet, and the worker queued a hidden continuation telling the agent to keep working and not to ask the user unless genuinely blocked. The agent then acted on its own guess, including on a risk_confirmation question, while the card sat waiting in the UI. A trailing human-input request is now detected before the evaluator runs and stands the goal down with the existing reason blocked:needs_user_input; the user's answer is a new human message, so the next run evaluates normally, and a card with no assistant text before it reports why it stopped instead of run_failed. (#5467)
  • client: Emit text a later node appends to an AI message already sent. LoopDetectionMiddleware, TokenBudgetMiddleware, SafetyFinishReasonMiddleware, SubagentLimitMiddleware, and TerminalResponseMiddleware each rewrite the last AI message under the same id from their own node, but the stream marks an id seen once and then skips it, so the rewrite never reached messages-tuple consumers: chat() and headless --print returned the text from before the rewrite instead of the terminal error or the forced-stop notice, the TUI never showed the stop or safety notice, and token_usage_attribution added after the model node never arrived. The values path now remembers the message object last seen per id and re-examines it when a snapshot holds a different one, emitting only the added text as one more delta and routing new additional_kwargs through the existing metadata-only follow-up; unchanged messages are still skipped without re-extracting text. A replacement that does not extend the sent text is not re-emitted. (#5479)
  • mcp: Hold the MCP session pool to its capacity limit under concurrent initialization. MCPSessionPool checked LRU capacity only before session creation started, and because initialization awaits, several distinct keys could each observe spare capacity, enter _inflight, and then promote into _entries with no second check — leaving the persistent-session registry above MAX_SESSIONS and retaining the extra subprocesses and connections it implied. Capacity is now rechecked and enforced atomically when a session is promoted, and promotion-time eviction victims are closed outside the registry lock through their owner-task lifecycle. (#4962)
  • mcp: Stop parallel synchronous MCP calls from cancelling each other's connection. When an embedded client dispatched two synchronous calls to the same stdio server, each wrapper ran on its own event loop and the pool treated the sibling loop's live session and in-flight creation as stale — cancelling or closing them and aborting a tool step with CancelledError instead of returning both results. Established sessions and in-flight creation are now keyed by (server_name, scope_key, owning_loop), so same-loop callers still share creation and state while separate loops get their own, and each owner retires only its own records, including after a normal asyncio.run() shutdown. Synchronous calls still use independent subprocess sessions and gain no shared server-side state. (#5396)
  • web-fetch: Resolve relative destinations in extracted Markdown against the page they came from. Jina, Browserless, and InfoQuest returned links exactly as written in the HTML, so a page at https://example.com/docs/current yielded [Next](../next) and left the agent with no URL it could follow up on. The three providers now pass the requested URL to the shared extractor, which resolves anchor and image destinations against that URL or against the first usable document <base>; only destination attribute values are rewritten rather than serializing a different parse tree, so malformed markup, comments, scripts, and attribute formatting survive untouched and still reach Readability.js unchanged. Fetch validation, the 4096-character output limit, and off-thread extraction are unchanged, and the Python fallback keeps its text-only behavior. (#5310)
  • community: The Firecrawl web_fetch and web_search tools honor the tool's base_url. _get_firecrawl_client read only api_key, so FirecrawlApp always fell back to https://api.firecrawl.dev — and because the SDK raises Error: No API key provided whenever the target is the cloud API and no key is set, a self-hosted Firecrawl was unreachable no matter how the config was written. base_url is now forwarded as api_url, letting a local Firecrawl serve both tools with no cloud key, and configurations without base_url construct the client exactly as before. (#5392)
  • community: Tavily extraction reads its credentials from the web_fetch entry instead of the search configuration. With web_search on Serper and web_fetch on Tavily, fetching built its client from the Serper key and ignored a separate Tavily fetch key entirely. Each tool now reads api_key from its own entry and falls back to the SDK's TAVILY_API_KEY when omitted, matching the existing Exa helper. Configurations that put a shared Tavily key only under web_search must also set it under web_fetch, or rely on TAVILY_API_KEY for both. (#5496)
  • client: DeerFlowClient.stream() emits a streamed tool call once, with its complete arguments. OpenAI-style models put the tool name and id in the first chunk and argument fragments with no id in the rest, and the client emitted a messages-tuple tool_calls event for every chunk, each parsed from that chunk alone — so the complete call, which does arrive in the values snapshot, was skipped because its message id was already in streamed_ids. The tool itself ran with the right arguments; only the event stream was wrong, and in the TUI the tool card showed bash with an empty detail instead of the command. One event is now emitted per message from the values snapshot, while whole AI messages from non-streaming models and streamed text are unchanged. (#5408)
  • browser: Keep browser session teardown alive across caller cancellation. BrowserSessionManager.close_session() removed a session from the registry before awaiting its close, and that close awaited the private Playwright loop directly, so a cancellation at that point propagated through asyncio.wrap_future() into the cleanup itself — leaving the browser process and context orphaned, with the session already out of the registry and no owner left to retry. close_all_sessions() had the same ownership problem at a larger scale: it cleared the whole registry first, so a cancellation while closing the first session stranded every later session with no teardown scheduled at all. Teardown is now submitted to the private loop before any cancellable await and awaited behind asyncio.shield(), and close-all schedules every session before its first await and shields the group wait. The caller still receives CancelledError; only ownership of the already-started cleanup is isolated from it. (#5444)
  • browser: Detect the browser dependency regardless of tool field order. Startup dependency detection recognized a tool name only when name was the first key of its YAML list item, so moving name below use or group kept the tool configuration working but silently omitted the browser extra from uv sync, leaving browser tools without their required dependencies. The direct name field is now recognized anywhere in a tool entry, tracked indentation keeps names inside nested options from enabling the extra, and the detector stays standard-library-only because it runs before dependencies are installed. (#5456)
  • setup: The same pre-sync extras detector now reads config.yaml as utf-8-sig. PyYAML accepts a leading UTF-8 BOM but the detector read the file as plain utf-8, leaving the BOM attached to the first line, so its anchored section patterns failed to recognize a valid config's opening section and omitted the extras it declared — postgres, browser, and ollama detection all failed the same way when their section came first, while a plain-UTF-8 copy of the same file worked. (#5504)
  • video: Forward --aspect-ratio into the Gemini Veo request. The skill CLI accepted the flag and passed it to generate_video(), but the Gemini branch built its predictLongRunning body from instances alone and dropped the value, so every Veo request rendered at the provider default ratio regardless of the argument. It is now sent as parameters.aspectRatio. (#5388)
  • channels: Both WeCom outbound paths sent unbounded text while the bot protocol caps content at 20480 UTF-8 bytes, a bar a deep-research report clears easily. Over the cap, _send_with_retry retried three times and gave up: the stream path left the reply stuck mid-way without ever sending finish=true, and the push path dropped the reply entirely. Stream replies now clip on a UTF-8 character boundary behind a visible truncation marker — one stream carries the whole reply, so it cannot split mid-way — while proactive pushes with no replyable frame, such as scheduled-task notifications, split into sequential markdown messages at newline boundaries so the full content still arrives. Both paths now measure the payload in UTF-8 bytes rather than characters. (#5148)
  • channels: Inbound WeChat (iLink) and WeCom media was downloaded in full before any size limit applied, and the download target was a URL taken straight from the message payload with no destination validation, so a legitimately oversized attachment spiked Gateway memory before being rejected. Both channels now stream the transfer and abort in flight once it exceeds max_inbound_image_bytes / max_inbound_file_bytes, with the exact post-decrypt size checks kept as a second line. Inbound full_urls are gated to http/https plus a dot-boundary host-suffix allowlist — the qq.com family and the configured CDN host by default, extendable with channels.wechat.allowed_media_hosts and channels.wecom.allowed_media_hosts — with the WeCom COS gate pinned to the verified Tencent Cloud APPID shape, and the WeCom manager reader bounded by a 50 MB in-flight cap. Readers ask for Accept-Encoding: identity and refuse any residual Content-Encoding, so httpx cannot inflate a compressed body past the cap before it is measured. Well-formed media under the limits is unaffected; oversized or non-allowlisted media is now skipped with a warning naming the reason. (#5225)
  • channels: Keep one undecodable WeChat message from dropping the rest of its batch. _poll_loop persisted the get_updates_buf cursor for the entire batch immediately on receipt, then iterated data["msgs"] with no per-message error isolation, so a single message whose processing raised — a corrupt or undecryptable attachment, such as a truncated encrypted image payload, is the realistic trigger — aborted the loop with the cursor already advanced past the whole batch. Every message after the failing one was permanently dropped and the next poll never re-fetched it. _handle_update now runs inside a per-message try/except, so siblings still reach the bus, the failure is logged with its message id, and the cursor still advances once the batch has been fully attempted. (#4231)
  • channels: When the Discord client thread died — an invalidated token, an unrecoverable close — Client.start() returned and left its event loop stopped but not closed, so call_soon_threadsafe queued a callback that would never run and the unbounded await asyncio.wrap_future(...) in send, send_file and _get_channel_or_thread hung forever. Each outbound to the dead channel permanently consumed one ChannelManager worker from the default pool of five, so a handful of messages froze inbound processing for every IM channel, not just Discord. Those cross-loop calls now route through a helper that bounds the wait at DISCORD_OUTBOUND_TIMEOUT_SECONDS (30 s), with file uploads given DISCORD_UPLOAD_TIMEOUT_SECONDS (120 s) so a slow uplink plus a large 429 retry-after cannot cancel a healthy transfer, and a missing or stopped loop closes the coroutine and raises immediately. DiscordChannel.is_running also requires the client thread to be alive, as FeishuChannel already did, so readiness polling restarts the channel instead of treating a dead one as healthy; and a half-started instance is now stopped and discarded when start() fails, so repeated readiness retries cannot accumulate stale outbound listeners on the bus. (#5227)
  • channels: Discord thread mappings are persisted off the event loop after a thread is created, so a process killed between the creation and its background write could still lose the newest mapping and leave a Discord thread unreachable after the restart. stop() now flushes the in-memory mappings to disk, wrapped so a shutdown path is never blocked — a best-effort net over the hard-kill window rather than a new write path. (#5461)
  • channels: Enabling rich_messages in the Telegram channel config made every final outbound message render as a mangled single line: command menus such as /help and plain error replies were sent through sendRichMessage with rich_message.markdown, which collapses single newlines and strips the <name> / <skill-name> / <task> angle-bracket tokens the menus rely on. A message is now sent as rich only when rich_messages is enabled and the text actually contains a rich construct — a fenced code block, a table row or separator, a task list, a [text](url) link, bold, italic, a <details> block, or $$ math. The detector is deliberately conservative, requiring a table to lead its line with |, so a line like /goal [condition|clear] in /help never trips it. (#5470)
  • community: The InfoQuest reader, web search and image search called requests with no transport timeout, so a stalled endpoint left the synchronous worker waiting indefinitely even when the remote crawl timeout field was configured. All three call sites now share a 30-second connect/read inactivity bound; crawl timeout and navigation payload fields, successful parsing and the existing Error: returns are unchanged. This bounds inactivity, not total wall-clock time. (#5315)
  • models: The request-admission limiter decided immediate admission and queue insertion under two separate lock acquisitions, so a blocking caller could observe that the next permit was not yet due, be descheduled before it enqueued, and then be overtaken by a newer caller once the permit came due while the waiter queue was still empty — the newcomer took the permit and the older caller waited a further interval, or under a short max_wait_seconds timed out while the later request succeeded. Immediate admission and FIFO insertion are now a single lock-protected decision. Non-blocking calls still fail fast and never enter the queue, and queued pacing, cancellation, timeout, queue-capacity and no-burst behavior are unchanged. (#5459)
  • uploads: Uploaded-document outlines counted any line starting with # as a heading, so a document with 51 #tag lines before # Real section consumed every outline slot and hid the real section; four-space and tab-indented comments became false headings, and optional closing hashes stayed in the titles. The extractor now matches root-level ATX syntax on the original line — one to six hashes, a space or tab separator or end of line, at most three leading spaces — and strips valid closing hashes before the existing bold cleanup. Physical line numbers, PDF structural headings, fenced-code exclusion and the heading-count limit are unchanged, and the scanner stays bounded rather than becoming a Markdown parser. (#5316)
  • uploads: A single 200,000-character paragraph produced a 199,999-character upload preview despite the five-line limit, and long valid headings bypassed the 50-entry limit as a practical context-size bound. Outline titles are now capped at 200 characters and fallback preview text at 2,000 characters across all lines, with the omission markers counted inside those budgets. Physical line numbers, short text, the existing heading and preview counts, and the original file bytes are preserved; this bounds returned summary text only, not file-scan memory or a budget across uploads. (#5323)
  • doctor: On a fresh clone, make config followed by make doctor reported four errors where only one was real: config.example.yaml ships a models: key with every entry commented out, which parses as None, so the [] default in .get("models", []) never applied and a broad except rendered the resulting TypeError as three 'NoneType' object is not iterable check results. A new user saw what looked like a broken install at the exact moment make doctor is meant to reduce confusion, when they had simply not configured a model yet. The LLM checks now skip when no models are configured, leaving the actionable models configured failure and its make setup hint; nothing changes once a model is configured. (#5296)
  • scripts: detect_uv_extras.py resolves uv extras from config.yaml so make dev does not wipe optional dependencies on every restart, but it had no rule for Ollama — so with an Ollama model configured it returned nothing, serve.sh ran uv sync without --extra ollama, and langchain-ollama was uninstalled from a working setup. The configured model then failed with ModuleNotFoundError: No module named 'langchain_ollama' with nothing pointing back at make dev as the cause, even though make doctor had just reported the package installed. Configuring an Ollama model now makes make dev pass --extra ollama, the same way database.backend: postgres passes --extra postgres; a config without an Ollama model is unaffected, and the commented-out Ollama examples in config.example.yaml are correctly ignored. (#5318)
  • docker: On Windows Git Bash, make docker-start and make up aborted unconditionally with Docker socket not found at /var/run/docker.sock — AioSandboxProvider (DooD) will not work. MSYS2/Git Bash has no physical Unix socket at that path — Docker Desktop uses named pipes — even though the daemon natively handles mounting /var/run/docker.sock into Linux containers, so the check could never pass. Under MINGW*, MSYS* and CYGWIN* the preflight now verifies docker info connectivity instead of requiring a socket file; POSIX hosts still fail fast on a missing socket. (#5371)
  • deploy: make up failed on Windows Git Bash during container startup with mkdir C:\Program Files\Git\var: Access is denied. scripts/deploy.sh exported DEER_FLOW_DOCKER_SOCKET=/var/run/docker.sock, and MSYS converted that exported value into a Windows host path when invoking native docker compose, so docker-compose.dood.yaml mounted C:\Program Files\Git\var\run\docker.sock and the daemon tried to create a directory that does not exist on the host. The socket is now kept in an unexported local variable, as scripts/docker.sh already did, and on Windows Git Bash the variable is unset when it holds the default so Compose expands its own literal /var/run/docker.sock; an operator-set custom socket path is still passed through. (#5402)
  • mcp: Exclude the internal stdio MCP temp directory (.mcp/tmp) from workspace changes, so MCP temporary and debug files no longer appear alongside user deliverables or crowd real changes out of the file budget. (#4898)
  • mcp: Cancel the remote task when a durable task submission is cancelled mid-flight, so an interrupted submission no longer leaves a remote task running with no record to poll or stop. (#4933)
  • sandbox: Accept the documented E2B reconciliation config fields, so valid E2B configuration no longer produces misleading startup warnings. (#4772)
  • sandbox: Bound E2B mount upload resource use per file, per mount, and across the whole upload pass (shared size and file budgets plus a wall-clock deadline), so large mounts can no longer spike Gateway memory or hold sandbox capacity indefinitely. (#4812, #4842)
  • sandbox: Preserve trailing whitespace in E2B-synced filenames and tolerate out-of-range remote mtimes, so output sync no longer re-downloads files repeatedly or aborts mid-sync. (#4861)
  • sandbox: Reject non-finite Redis lease-timing values in sandbox ownership config at parse time instead of crashing with an OverflowError during startup. (#4960)
  • sandbox: Resolve structured skill reads through the sandbox provider's path mappings, so read_file opens legacy and per-user custom skills under the same enabled-state projection as ls and shell execution. (#4792)
  • skills: Parse Responses API content blocks in the moderation scanner, so valid skill-management decisions returned as content blocks are no longer rejected as unparseable. (#4936)
  • memory: Reject non-positive and non-finite timeout and character-limit settings in the Honcho and Mem0 backends at config parse time, so a bad value fails fast instead of silently truncating stored text or crashing on the first HTTP call. (#4783, #4823)
  • memory: Scope custom-agent bootstrap facts to the selected agent's bucket, so facts learned during setup no longer leak into the default bucket and influence ordinary lead-agent conversations. (#4804)
  • artifacts: Support atomic saves on Windows, and serve a SHA-256 ETag on artifact reads so inline preview and editing work on non-secure contexts such as plain-HTTP LAN origins where crypto.subtle is unavailable. (#4629, #4865)
  • frontend: Keep conversation order stable around long runs: the submitted user message no longer renders twice or sinks below its own processing steps, and after a mid-run page reload a turn's steps can no longer appear above the user message that started the run. (#4620, #4660, #4834)
  • frontend: Stop matching <header> as <head> when injecting the base href into HTML artifact previews, so relative assets in report fragments that begin with <header> now load in the sandboxed preview iframe. (#4625)
  • frontend: Open landing-page case studies on a public read-only /showcase/ route so anonymous visitors are no longer redirected to login. (#4635)
  • frontend: Sort the chats page by pinned state, so pinned threads no longer render below unpinned ones. (#4643)
  • frontend: Keep <think> pairs written inside markdown inline code in the rendered content instead of hollowing them out into the Reasoning panel, and restore the copy button for turns that contain only reasoning. (#4647)
  • frontend: Surface model-loading failures with a workspace error banner and retry action instead of a silently empty model list. (#4840, #5021)
  • frontend: Preserve copy and other actions on completed assistant messages while a later turn is still streaming. (#4844)
  • frontend: Keep the browser live stream connected after a successful reconnect instead of tearing down the new socket and immediately creating another. (#4951)
  • frontend: Reuse the shared clipboard fallback when copying the Lark authorization link, so the copy action works in browsers without the Clipboard API. (#4767)
  • frontend: Use consistent "DeerFlow" casing in the composer disclaimer and fix the "What's New" heading on the landing page. (#4970)
  • channels: Bound inbound intake with a fixed worker pool and bounded admission queues, and await real cross-thread tasks on shutdown, so message floods are rejected promptly instead of accumulating and channel shutdown no longer tears down transports with work still in flight. (#4800, #4816)
  • channels: Offload outbound attachment file IO for Feishu, Telegram, and WeCom to worker threads, so sending a large artifact no longer stalls the Gateway event loop. (#4633)
  • channels: Run Telegram connection-identity lookups on the Gateway event loop, so inbound messages and commands no longer crash with a cross-loop error when channel connections are enabled. (#4815)
  • feishu: Keep file receiving off the event loop and preserve every inbound attachment: duplicate provider filenames no longer overwrite each other, writes can no longer be redirected outside the thread bucket, and a failed attachment no longer blocks the rest of the message. (#4627, #4903)
  • dingtalk: Strip leading @bot mentions before command classification, so slash commands like /new sent in group chats are recognized instead of treated as plain chat. (#4724)
  • discord: Refuse to start typing-indicator loops after the channel stops, so shutdown no longer leaves an infinite typing task sending events in the background. (#4752)
  • wecom: Serialize WebSocket start/stop transitions and await the SDK's real receive-task shutdown, so stopping the WeCom channel can no longer return before the socket closes or clear a newer connection's state. (#4762)
  • buzz: Drop replayed events across reconnects using a persistent seen-id store, so the agent no longer re-answers the last message in a channel after a relay or Gateway restart. (#4888)
  • lark: Keep the CLI lock directory writable inside sandboxes while the credential-bearing config root stays read-only, restoring Lark API commands that previously failed with a read-only filesystem error. (#4701)
  • scheduler: Enforce the global max_concurrent_runs budget for manual triggers too, returning HTTP 409 when the cap is reached instead of letting manual launches exceed it. (#4769)
  • scheduler: Coerce serialized task timestamps on read, so scheduled-task operations no longer fail when string-form timestamp values reach the database layer. (#4785)
  • scheduler: Support safe multi-instance scheduler recovery: startup no longer treats live runs owned by peer Gateway instances as local leftovers, so a restarting instance cannot interrupt a live run or trigger a duplicate execution; multi-instance mode is opt-in via scheduler.multi_instance. (#4713)
  • scheduler: Enqueue busy scheduled task runs instead of skipping them: occurrences that hit a busy reused thread now wait in a durable queue (bounded by scheduler.queue_timeout_seconds) and survive Gateway restarts, and the UI explains the queueing behavior. (#4918)
  • cli: Add --recursion-limit to headless --print, --json, and --cli runs, so long-running agent loops are no longer stuck at the default recursion limit of 100. (#4615)
  • dev: Exclude backend runtime state from the Uvicorn reload watcher in the backend make dev launcher, so an agent task writing files under the runtime tree can no longer restart the Gateway and reset concurrent users' requests. (#4759)
  • dev: Resolve diagnostic script paths from the script's own location, so root diagnostic commands work when invoked from any working directory. (#4736)
  • docker: Harden local and container startup: make up waits for the Gateway health probe before declaring the stack ready, Docker startup no longer aborts when .env is missing, the Gateway can write extensions_config.json in production, runtime data stays out of the image build context, log commands resolve the checkout root correctly, and the default loopback origins are allowed so the dev setup page can hydrate. (#4658, #4806, #4852, #4853, #4956, #4959)
  • gateway: Stamp the server-authoritative feed position onto persisted messages, so an early user message no longer vanishes or jumps into the middle of the step stream once history exceeds one page and context compaction has fired. (#4696)
  • lark: Preserve the new app secret during managed credential switches by clearing the previous app's OAuth data before the replacement is written, so the subsequent browser authorization no longer resolves an empty client_secret. (#4820)
  • messages: Drop legacy <uploaded_files> tag handling: the backend treats the pre-#4174 spelling as ordinary content and strips only <current_uploads>, while the frontend keeps stripping the legacy tag so old threads still render cleanly. (#4826)
  • skills: Reject a blank SKILL.md description at the write gate, matching what the loader already requires, so editing a custom skill with an empty description no longer writes a file the loader then rejects - which destroyed the skill on disk. (#4867)
  • sandbox: Make the model-facing description argument optional (empty by default) across bash, ls, glob, grep, read_file, write_file, str_replace, and task, so providers that omit it are no longer rejected before execution. (#4878)
  • sandbox: Bound Windows command execution: host commands run in a new process group killed via taskkill /T /F on timeout so a descendant cannot hold the call open, and output flows through the existing bounded 10 MiB capture. (#4946)
  • sandbox: Scope the Windows MSYS path-conversion exclusion to safe virtual path prefixes instead of disabling conversion globally, so host-native CLI launchers that need normal conversion work again. (#5003)
  • skills: Rebuild per-user skill storage after an app-config hot reload, so it no longer stays bound to paths from the previous config instance. (#4972)
  • skills: Tokenize portable allowed-tools scalars with parenthesis awareness, so Bash(tvly *)-style entries stay intact, unmatched parentheses are rejected instead of silently fragmenting, and argument-scoped entries remain literal rather than broadening access. (#4984)
  • agents: Normalize ToolMessages returned inside Command results, so error payloads no longer earn a default success receipt and tool-progress tracking sees them. (#4977)
  • config: use_previous_response_id works in a config.yaml model entry — it is not a ModelConfig field and reaches ChatOpenAI only through the model factory's extra="allow" passthrough — but was never documented, and reads like a synonym for use_responses_api. It is not: use_responses_api picks the endpoint, while use_previous_response_id switches the Responses API from replaying the full history each turn to chaining on server-side state. The OpenAI Responses API example in config.example.yaml gains a commented use_previous_response_id: false line noting that it is forwarded to ChatOpenAI as-is, that chained context is still billed as input tokens, and that client-side history rewrites only apply when the history is replayed. Comment only — no schema change, no new ModelConfig field, no config_version bump. (#5359)
  • mcp: Tear down the in-flight session owner when get_session is cancelled during eviction, so a cancelled caller no longer leaks the owner task or parks past its timeout. (#5008)
  • mcp: Reconnect ordinary stdio tools after a transport disconnect: the failed pooled session is evicted (only if still registered), the original error surfaces without automatic replay, and a later retry starts a fresh subprocess. (#5018)
  • mcp: Preserve pooled stdio sessions after protocol timeouts during durable MCP task polling - a 408 is not a disconnect - so task state survives and the next poll no longer reports task_not_found. (#5027)
  • mcp: Reject credentials that cannot travel as HTTP header values (trailing newline or whitespace, non-ASCII) at the config boundary, so the transport's exception - which echoes the full value - can no longer leak a secret into model context, checkpoints, and traces. (#5066)
  • subagents: Clean up the background-task entry when the poller exits unexpectedly and drop a PENDING registry entry when submission fails, so a failed or crashed poll no longer leaks the entry or leaves the subagent running unattended. (#5069)
  • subagents: Stop the zombie PENDING registry entry on the submit-failure path, and derive the capacity snapshot's queued count from the waiters' length instead of iterating a deque other threads mutate. (#5086)
  • channels: Synchronize ChannelStore reads with mutations, so get_thread_id()/list_entries() can no longer raise dictionary changed size during iteration. (#5083)
  • discord: Retain strong references to ack-reaction tasks and drain them on shutdown, so a GC pass can no longer silently drop a reaction or pin the channel across restart cycles. (#5049)
  • buzz: Move seen-event persistence off the event loop with coalesced atomic writes, preserving dirty generations when events arrive mid-write and awaiting the final flush on shutdown. (#5103)
  • streaming: Stop an IndexError in MemoryStreamBridge._make_gap when a subscriber reconnects to an empty or drained stream with an expired cursor. (#5047)
  • uploads: Keep deduplicated filenames within the 255-byte limit by truncating the stem on a UTF-8 code-point boundary, so two max-length files that differ only by a dedupe suffix upload successfully instead of failing the whole batch. (#5059)
  • frontend: Format structured upload error details (FastAPI validation issues, objects, arrays) instead of showing [object Object]. (#5071)
  • frontend: Keep a renamed thread's title in sync across the active chat header, document title, search results, and metadata caches without a reload. (#5045)
  • frontend: Truncate selected model names to the selector button width, so long model names ellipsize in the composer and sidecar instead of overflowing. (#5050)
  • frontend: Truncate long subtask card titles to one line with a tooltip, so a delegation whose model omitted description (falling back to the full prompt) no longer overflows the chat layout. (#5136)
  • dev: Default the frontend dev server to Webpack on all platforms (DEER_FLOW_DEV_BUNDLER=turbo opts back into Turbopack), avoiding Turbopack's macOS PostCSS worker leak and its Windows runtime panics. (#5036, #5133)
  • scripts: Run repo shell scripts through an explicit interpreter (bash scripts/...), so a lost executable bit - zip/tarball downloads, core.fileMode=false, non-POSIX filesystems - no longer breaks make docker-start and friends with Permission denied. (#5031)
  • deps: Depend on the renamed tenki package instead of the PyPI-removed tenki-sandbox (same tenki_sandbox import), so clean checkouts can resolve dependencies again on make dev/uv sync. (#5087)
  • memory: Memory reads configured to stop the turn now raise a backend-neutral MemoryReadError that prompt assembly preserves instead of swallowing: strict OpenViking (read: raise), Mem0, and Honcho reads propagate, OpenViking scope-resolution failures follow the configured read policy, and the 5-second injection deadline honors the same policy (fail-open continues without the context; strict raises with the timeout as its cause). (#4726)
  • persistence: Add the storage foundation for a per-thread incarnation token without activating it. New nullable threads_meta.incarnation and mcp_tasks.thread_incarnation columns (a new Alembic head) let a task be tied to the specific thread a reused thread ID belongs to, since a deleted thread's ID can be taken by a later thread while durable MCP work outlives the deletion. A random 32-character incarnation is assigned to newly created thread records and copied into new MCP task rows only when the thread is owned by the task user or is an unowned legacy row, with the lookup and insert kept atomic on SQLite through a scalar subquery and a share lock on PostgreSQL. Nothing reads the fields yet, they are kept out of the thread and MCP task API responses, and both columns are nullable with no default so older binaries keep writing during a rolling deployment. (#5216)
  • scripts: On native Windows, shutil.which("pnpm") follows PATH/PATHEXT resolution and can select a generic pnpm match — an .exe or .bat, or an earlier-path wrapper — ahead of the npm-installed pnpm.cmd wrapper. The shared host runner now checks pnpm.cmd and corepack.cmd before the generic names, matching the Windows command-selection order the repo already documents; POSIX resolution order is unchanged. (#5305)
  • tests: The backend suite failed 68 shell-script tests across seven files on a Windows contributor host, none of them for anything wrong with the code under test. CreateProcess searches System32 before PATH, so the WSL bash.exe launcher won every ["bash", ...] spawn and could not run the repo scripts against Windows checkout paths — on non-English Windows its localized UTF-16LE warning crashed the harness's pipe decoding and buried the real failure behind a TypeError; sh does not exist outside MSYS2; and dev-entrypoint.sh's command -v python3 probe selected the Microsoft Store alias stubs, which exit 49 when executed. The suites now resolve a shell through a shared helper that mirrors the repo's Git Bash wrapper discovery and explicitly rejects the WSL launcher and the Store stubs, and skip cleanly where no Git Bash exists. POSIX and CI behavior is unchanged. (#5404)
  • tests: Six tests across three suites failed on a Windows host purely on path-separator spelling. The provisioner's join_host_path deliberately preserves host-native style and even has an explicit PureWindowsPath branch, and os.path.normpath re-spells POSIX inputs with backslashes there, so the hostPath strings it builds, the docker --mount src= spelling, and the review CLI's PYTHONPATH never matched POSIX-style literals. The assertions now normalize the host-native side before comparing, which is a no-op on POSIX so CI expectations stay byte-identical; production behavior is unchanged. (#5413)
  • tests: Five tests in the skill request-scoped-secrets suite failed on a Windows host and two negative checks passed vacuously, because the probes embedded POSIX syntax: LocalSandbox._get_shell() resolves to PowerShell there, where $VAR is an undefined PowerShell variable and expands to nothing, so the positive checks could never see an injected value and the "secret" not in out assertions were comparing against empty output. Probes are now rendered in the syntax of the shell production actually resolved — $env:NAME under PowerShell, %NAME% under cmd.exe, $NAME under POSIX shells — so the negative checks genuinely verify scrubbing on Windows, and the per-call-scoping case also proves the injected value reached the first subprocess. POSIX output is unchanged in form. (#5415)
  • tests: test_run_on_isolated_subagent_loop_survives_caller_loop_teardown failed intermittently under CI load, on unrelated PRs and on main itself. The race was in the test, not in run_on_isolated_subagent_loop: that helper is asyncio.run_coroutine_threadsafe, whose concurrent.futures.Future is only marked done after the coroutine returns, but the test signalled from inside the coroutine and asserted done() immediately, so the main thread could wake while the future was still pending. The assertions now block on result(timeout=10) before checking done(); no sleep is introduced and the behavior under test is unchanged. (#5299)
  • tests: Checkpoint retention needs an executable statement of what a deletion may never break — a newest-N proposal had to be withdrawn once review showed it would silently break branch and time-travel parent-chain semantics. The new suite pins six retention scenarios across the memory, SQLite and Postgres checkpointers: growth baselines per step for the full and delta schemas; deleting a branch ancestor fails loudly with CheckpointLineageError instead of silently corrupting branch/regenerate; deleting an explicit resume target removes the resume surface; pending writes are retained state rather than garbage; and a trailing duration-only leaf and a leaf sibling branch — the real fork path — can be deleted safely. A companion draft documents the protected set, the provably safe deletion shapes, and joint-table mechanics with orphan accounting, for any future max_checkpoints_per_thread or TTL work to be validated against. (#5255)
  • memory: Buffered memory extraction is cancelled when a custom agent is deleted or cleared, so pending debounce timers can no longer resurrect the deleted per-agent memory scope or overwrite a fresh clear with a stale pending update. (#5123)
  • agents: Conversation titles are generated from the user's original message content when upload (or other) context wrappers have been injected into the text, so titles no longer quote server-injected <current_uploads> context; attachment-only messages keep the New Conversation fallback. (#4729)
  • agents: Fraction summarization triggers resolve against the model's declared context_window (now translated into the LangChain profile), and an unresolvable fraction clause degrades to a never-firing trigger with a warning instead of crashing the whole agent build; percent-style and non-finite trigger values are rejected at config load. (#4901)
  • agents: Custom-agent storage falls back to file-backed storage only when search mode cannot resolve the main application config — invalid configuration and missing config paths surface instead of silently switching storage backends — and store IO moves off the event loop in async agent routes. (#4952)
  • middleware: Loop-detection hard stops win across a whole tool-call batch: selecting a soft warning no longer ends inspection, so a later call in the same response that crosses an operator-configured hard limit is rejected instead of riding along with the earlier warning. (#5245)
  • sandbox: list_dir and glob in the five remote sandbox providers (E2B, OpenSandbox, AIO, Tenki, BoxLite) return filenames verbatim instead of stripping whitespace, so files whose names begin or end with spaces are no longer listed under paths that then do not exist. (#4980)
  • sandbox: Concurrent subagents sharing one thread sandbox run under process-local execution leases with task-scoped AIO shell sessions, so one sibling finishing can no longer release the shared sandbox underneath the others or corrupt the implicit persistent session; a healthy replacement session is promoted after corruption instead of returning to it. (#5134)
  • sandbox: The Bash tool guides the agent to detect its execution environment with evidence (uname -s, sw_vers, uname -a) instead of model assumptions, and rejected host paths direct it to command-only probes or allowed virtual paths rather than repeating the blocked command. (#5111)
  • sandbox: The Docker AIO compatibility capability allowlist gains FOWNER, so AIO images whose startup chmods /run/user/1000 (e.g. 1.11.0) start again under the hardened default capabilities while no-new-privileges stays on. (#5163)
  • sandbox: File appends no longer destroy existing content when the pre-read fails: E2B append treats only missing-file errors as an empty file and re-raises anything else instead of overwriting the file with just the appended tail, and AIO appends use the server's native append mode without a pre-read at all. (#5261, #5278)
  • skills: Skill markdown is read explicitly as UTF-8, so localized skills validate on Windows hosts whose default code page is not UTF-8 instead of failing with UnicodeDecodeError. (#4995)
  • mcp: The MCP tools cache re-initializes after a runtime config change: a module-level asyncio.Lock bound to a closed event loop and an unsynchronized initialized flag had left every post-update call failing with Lock is bound to a different event loop or racing across worker threads. (#5062)
  • mcp: Sync-wrapped MCP tools keep LangGraph ToolRuntime injection (the annotation-less sync wrapper is now functools.wraps-transparent), so per-user scope resolution and durable task submission no longer run with runtime=None — which had routed completion-notification runs under the default lead agent instead of the thread's custom agent. (#5164)
  • auth: A duplicate OAuth identity no longer reports "Email already registered" — the two integrity violations are distinguished — and the partial OAuth-identity index now declares postgresql_where so Postgres builds the intended partial index instead of a full one. (#5026)
  • frontend: The mobile sidebar trigger stays clickable on regular and custom-agent welcome pages, where multi-line (longer localized) welcome text in a same-z-index overlay could cover the header's tappable area. (#5149)
  • subagents: SubagentResult lifecycle timestamps are UTC-aware on every writer, matching the repo-wide convention instead of stamping local wall-clock time on non-UTC hosts. (#5153)
  • browser: Background live-frame scheduler tasks retain strong references, so garbage collection can no longer silently stop the Browser Live view from refreshing by stranding a pending guard that was cleared only in a collected task's finally. (#5155)
  • runtime: Duplicate on_llm_end callbacks for the same LangChain run id persist one durable llm.ai.response event (replayed usage merged by generation position, first callback canonical), so append-only message APIs no longer return duplicate responses from providers that re-fire the callback with usage populated. (#5187)
  • runtime: Terminal finalization completes after a cancellation raised inside the completion hook or task-stop fan-out, so extension observers run and the stream END marker is published before the interruption is re-raised; the finalization tail stays interruptible. (#5191)
  • models: A cancelled LLM call releases its owned circuit-breaker recovery probe across provider execution, concurrency admission, and backoff, so subsequent calls no longer see CircuitBreakerOpen after a cancellation; probe ownership is fenced by a per-call token so cancelling an older call cannot release another call's probe. (#5197)
  • runtime: The embedded DeerFlowClient keys its graph cache by effective user in every authorization mode and materializes the same user in runtime context, so sequential reuse for different users can no longer serve a graph assembled with another user's prompt and workspace state. (#5206)
  • runtime: Cancelled workspace-change snapshot captures drain their already-running scan and clean up the per-run text cache instead of leaking deerflow-workspace-changes-* directories, while metadata-only captures propagate cancellation promptly without waiting on the scan. (#5232, #5234)
  • persistence: Gateway startup tolerates a database already migrated to the explicitly-reviewed newer revision (0019_thread_incarnations), so rolling back to this image after a newer deployment stays possible; other unknown revisions, an empty version table, and multiple version rows still fail closed. (#5219)
  • community: Tavily Extract results without a title fall back to the result or requested URL as the display heading instead of raising KeyError and discarding usable page content. (#5280)
  • uploads: Fenced code blocks are excluded from uploaded-document outlines, so code comments and bold examples inside fences no longer crowd real sections out of the 50-entry heading budget. (#5281)
  • subagents: After compaction, the delegation ledger distinguishes execution completion from task acceptance and preserves bounded examples of a completed subagent's unmet and unverified acceptance criteria, so the lead agent repairs the remaining gaps instead of treating the completed result as fully done. (#5287)
  • dev: _pick_python() validates interpreter candidates through /usr/bin/env, mirroring how the frontend is launched, so make dev starts the frontend on Windows where Microsoft Store Python stubs satisfy Bash-side probes but not env. (#5181)

Performance

  • runtime: Index MemoryRunStore by thread_id and MemoryRunEventStore events by run_id to avoid O(n) scans. (#3562, #3686)
  • subagents: Deduplicate streamed AI messages via a seen-id set (O(n²) -> O(n)). (#3687)
  • sandbox: Cache LocalSandbox path-rewrite regexes and local-path masking patterns per instance instead of recompiling per search match. (#3648, #3713)
  • messages: Index tool-call results per group. (#4411)
  • frontend: Coalesce streaming renders to a frame budget instead of per chunk. (#4425)
  • frontend: Stop re-deriving message content on every stream chunk. (#4441)
  • sandbox: read_file reads only the requested line range from the sandbox instead of fetching the whole file first. (#3824)
  • browser: Encode Browser Live progress frames as JPEG to cut progress payload size. (#4836)
  • middleware: Inject view_image content via wrap_model_call instead of a checkpointed hidden message, so up to 20 MB of base64 no longer sits in two checkpoints per viewed image and an interrupted run can no longer leave the payload behind. (#5014)
  • frontend: Cache settled copy-data derivation across streaming chunks, so each chunk no longer re-derives toolbar/copy text for every settled message. (#5095)
  • runtime: Bound gateway memory after terminal runs, stopping the post-GC low-water mark from creeping upward across completed sessions. (#5112)
  • frontend: Chat streams request messages-tuple + updates + custom instead of full values state snapshots — the retransmitted historical values messages were ~75% of SSE payload — folding reducer events into rendered state locally while keeping full snapshots only for replay-gap recovery. (#5159)

Security

  • uploads: Deleting an upload no longer follows a symlink to delete a different file. A symlink planted in the sandbox-writable uploads directory made DELETE /api/threads/{id}/uploads/{filename} (and DeerFlowClient.delete_upload) remove the upload it pointed to, plus that file's companion .md, while reporting the requested name as deleted. Symlinks now return 404, matching the upload listing; links that leave the uploads directory are still rejected with 400. (#5547)
  • frontend: Tool steps no longer turn non-web URLs into links. The web_fetch URL and web_search / image_search result links in the chain-of-thought panel skipped the scheme allowlist that markdown links use, so a prompt-injected tool call could put a file: or OS protocol-handler link (ms-msdt:, vscode:, …) into the chat. They now pass isSafeHref and show an unsafe URL with the same "Unsafe link omitted" marker as markdown links. A tool call whose args are missing, or whose web_fetch URL is not a string, no longer crashes the message list. (#5526)
  • skills: Close gaps that let files skip SkillScan in the public skill review gate. The review analyzer passed SkillScan only files it had decoded as text, so executable binaries and nested archives were never checked; it exempted every file anywhere under an evals/fixtures/ directory; and a duplicate archive member or a case-folded name silently overwrote an earlier file before scanning. SkillScan now receives every file byte for byte, only eval fixture SKILL.md samples stay exempt, and path collisions mark the review incomplete. SkillScan also skipped code files containing a NUL or non-UTF-8 byte, so one byte in a comment hid a reverse shell from the review gate, and a NUL byte skipped static analysis at install. Such files now raise package-undecodable-script and are still analyzed, so CRITICAL matches keep blocking. SkillScan's Mach-O detection missed 32-bit little-endian and fat variants that the installer blocks; the installer, export guard, and SkillScan now share one code-file and executable-magic definition. Review snapshots gain a content_base64 field for binary files. (#5431)
  • prompt-injection: New input-sanitization middleware defends against prompt-injection, forged framework tags in the input guardrail are blocked, and system context is injected as a SystemMessage for role isolation. (#3662, #4155, #3661)
  • prompt-injection: HTML-escape untrusted content rendered into model prompts
    • memory facts and summaries, SOUL.md, subagent descriptions, skill metadata, and the conversation block in the memory-update prompt - and neutralize prompt-injection tags in web_capture tool results. (#4028, #4119, #4137, #4157, #4162, #4099, #4060, #4097, #4128)
  • prompt-injection: Escape MindIE tool-response framing. The provider's _fix_messages escaped tool-call names and arguments before rendering them into <tool_call> / <function=...> tags, but the sibling path that wraps a ToolMessage in <tool_response> passed its text through unescaped — and that output arrives largely unsanitized, because the tool-result sanitizer covers only the remote-content tools on its allowlist (web_fetch, web_search, image_search, web_capture) and deliberately leaves local bash / read_file output and differently named MCP tools alone. A literal <tool_response> in a read_file result therefore closed the framing early, and everything after it was presented to the MindIE model as though it sat outside the tool response, so a payload planted in an untrusted file could forge a <system-reminder>. Content is now HTML-escaped with quote=False, matching the tool-call path; the model still decodes the entities back, so only the framing is protected and legitimate results are understood exactly as before. (#4253)
  • prompt-injection: Close two input-sanitization bypasses. hide_from_ui and a human name="summary" tell is_genuine_user_message that the framework authored a message, which skips sanitization entirely. Untrusted run input and thread-state writes carrying either marker are now marked server-side and sanitized regardless, so a caller can no longer land a raw <system-reminder> outside the user-input boundary markers that the lead-agent prompt declares trusted framework data. The markers themselves are preserved, so messages that use hide_from_ui only to stay out of the transcript — quoted conversation context, sidecar context, the agent save command, HumanInputCard replies — keep doing that, and trusted internal launchers are unaffected. Sanitization also covers every genuine user message instead of only the newest: the transformation is request-scoped, so a last-turn-only scan neutralized a payload for exactly one model call and then replayed it verbatim from the next turn on. (#5375)
  • secrets: Scrub inherited secret environment variables (MYSQL_PWD, REDISCLI_AUTH, abbreviated *_PASS, and Postgres PGPASSFILE) from the skill environment; request-scoped secrets are bound for both slash-activated and autonomously-invoked skills. (#4018, #4026, #3871, #3938)
  • web_fetch: SSRF guard for self-hosted providers. (#3942)
  • guardrails: An empty allowlist now denies all tools instead of failing open. (#4067)
  • authz: Global skills-management endpoints now require admin; the legacy skills mount is gated by user visibility; artifacts honor a trusted owner-user-id header; and the trusted authorization principal is propagated through the runtime. (#3855, #3985, #3982, #4203)
  • auth: Persist the csrf_token cookie for the access-token lifetime. (#3872)
  • storage: Stop persisting base64 image data in checkpoint state. (#4140)
  • mcp: Reject legacy MCP credentials in run metadata. (#4448)
  • mcp: Constrain stdio launcher arguments and environment variables at the config API, rejecting launcher flags and env names that could turn an allowlisted npx/uvx server registration into arbitrary code execution. (#4617)
  • auth: Harden validation of the post-login next path. (#4587)
  • runtime: Honor the LangGraph Server's authenticated user identity across agents, uploads, thread data, memory, and skills, and reject client-supplied auth identity fields. (#4538)
  • frontend: Send the session cookie on model, workspace-change, and ranged artifact reads in split-origin deployments. (#4827)
  • frontend: Restore sanitization in custom streamdown rehype chains, so artifact markdown previews and the memory settings summary can no longer render hostile HTML such as javascript: links or on* event handlers. (#4987)
  • skills: Copy projected skill files instead of hardlinking them, so a sandboxed write can no longer mutate the canonical skill source, and fail closed on a drifted projection namespace on every platform, including Windows. (#4825, #4830)
  • scripts: Redact secret-shaped keys (db_pass, signing_key, ...) wherever they appear in bundled config, not only under well-known key names. (#4242)
  • sandbox: Sanitize MCP-sourced tool results through the same trust boundary as the built-in web tools, so a hostile or compromised MCP server can no longer hand the model forged <system-reminder> or user-input boundary tags. (#4839)
  • sandbox: Harden local Docker sandbox containers: published ports bind the Docker bridge gateway instead of 0.0.0.0 when the sandbox host is non-loopback (DEER_FLOW_SANDBOX_BIND_HOST=0.0.0.0 restores the broad bind), Docker's default seccomp profile replaces unconditional seccomp=unconfined (opt back in with DEER_FLOW_SANDBOX_SECCOMP_UNCONFINED=1), and containers drop all capabilities, get no-new-privileges, and run with bounded resources. (#4986)
  • authz: Enforce run-create authorization on stateless stream/wait endpoints (runs:create), and require both threads:write and runs:create for scheduled-task create, update, resume, and manual-trigger mutations. (#5030)
  • authz: Re-check the authorization policy before reusing a persisted sandbox, so a revoked sandbox:execute grant takes effect on the next sandbox-backed turn instead of outliving the policy in the cached sandbox. (#5006)
  • skills: Enforce custom-Agent skill allowlists at the sandbox filesystem level: an explicit skills policy materializes a signed per-user/thread skills view, so a custom agent with shell or file tools can no longer read skills its policy excludes. (#5077)
  • runs: Reject cancel/rollback actions on GET stream joins with 405 Method Not Allowed - cancel-then-stream is a POST operation - closing a state change that CSRF middleware deliberately exempted on safe methods; action-less GET joins are unchanged. (#5092)
  • lark: Lark CLI credential trees on Windows enforce private ACLs (gateway-SID-only protected DACL, reparse-point rejection, handle-relative traversal), extending the POSIX 0700/0600 confidentiality contract against inherited grants, junction redirection, hard-link aliasing, and TOCTOU replacement. (#5141)
  • sandbox: SSH_AUTH_SOCK is scrubbed from the sandbox subprocess environment — inheriting the host ssh-agent socket lets sandboxed code sign and authenticate with every key the agent holds — unless a skill explicitly declares it via required-secrets. (#5145)
  • artifacts: Serve XML artifacts as download attachments like HTML and SVG. GET /api/threads/{id}/artifacts/{path} rendered .xml, .xsl, and .rdf files — and +xml types such as .rss wherever the host MIME database maps them — inline in the application origin, so an XML document with an XHTML-namespaced <script>, written by a prompt-injected agent and opened from a chat link, could call the API with the viewer's session. Every XML MIME type (text/xml, application/xml, text/xsl, any +xml subtype) is now treated as active content, including .skill archive members; the artifacts panel keeps previewing XML through its ranged fetch. (#5353)

Documentation

  • docs: Clarify how LocalSandboxProvider resolves sandbox.mounts[].host_path under production Docker, with gateway bind-mount and config examples. (#3833)
  • docs: Document that Crawl4AI >= 0.9 requires a bearer token. (#4518)
  • docs: Document the GitHub inbound-dedupe TTL semantics, including what redeliveries are not deduped, and tighten the redelivery tests. (#4274)
  • docs: Update the agent AGENTS.md and ARCHITECTURE.md guides. (#4817)
  • docs: Document the Honcho memory backend with a dedicated guide and a long-term memory section entry in the README. (#4822)
  • docs: Align custom-agent documentation with the API across the English and Chinese agents/threads/lead-agent pages: the required ASCII name request field, lowercase storage, /api/agents/check name-availability behavior, and no auto-derived slug from display_name. (#4944)

Internal

  • tests: Migrate frontend unit tests to rstest and run hook-level tests in a DOM environment. (#3703, #4453)
  • tests: Require explicit opt-in for live client tests. (#4482)
  • tests: Rename the LLM-error test stand-in instead of the shared FakeError. (#4744)
  • tests: Replace the magic unwritable absolute path in tool-output tests with a self-constructed failure condition. (#4722)
  • tests: Add multi-turn message-stream invariants as graph integration tests. (#3708)
  • tests: Add trace-based behavioral tests with Monocle Test Tools, asserting agent routing, tool calls, and token/duration cost. (#4025)
  • tests: Cover passive skill tool visibility in the MCP layer. (#4247)
  • tests: Add SQL and concurrent-reconciler coverage for lease-aware orphan recovery. (#4427)
  • tests: Restore memory updater regression coverage. (#4490)
  • tests: Lock in POST logout from the gateway-offline banner. (#4506)
  • tests: Document known instance-client false negatives in the SkillScan tests. (#4644)
  • refactor: Extract frontend placeholder detection into a tested utility. (#3783)
  • refactor: Consolidate E2B client lifecycle helpers and reuse the kill helper during warm-pool eviction. (#4262, #4298)
  • refactor: Name the E2B capacity-ledger meta-field count so the admission offset is explicit. (#4764)
  • dev: Trace self/cls attribute chains and local aliases in the blocking-IO detector's call graph, closing false negatives. (#4200)
  • ci: Publish the lark-cli-init and lark-broker images. (#4558)
  • dev: Route host-side pnpm consumers through a shared runner with a Corepack fallback so local workflows work without a pnpm shim. (#4405)
  • bench: Add an isolated checkpoint channel-mode benchmark comparing full and delta across latency, storage, and replay metrics. (#4395)
  • deps: Bump cryptography 49.0.0 -> 50.0.0, postcss 8.4.31 -> 8.5.25, h2 4.3.0 -> 4.4.1, langgraph-checkpoint-sqlite and langgraph-checkpoint-postgres 3.1.0 -> 3.1.1, nanoid 5.1.6 -> 5.1.16, h3 1.15.6 -> 1.15.9, and next 16.2.11 -> 16.3.3. The next bump closes two unauthenticated remote code execution advisories — one in the Image Optimization API's AVIF path, one on Windows-hosted servers — while h3 stops a double-encoded dot segment from traversing out of the static route. (#4211, #4681, #4683, #4737, #4738, #4747, #4748, #5377)
  • bench: Add a reproducible hybrid memory-eviction evaluation under backend/scripts/benchmark/deermem_eviction/ with a deterministic, blind-by-construction grader for the #4789 policy. (#4810)
  • bench: Measure Postgres checkpoint/blob/write storage growth in the checkpoint benchmark alongside memory and SQLite. (#5051)
  • tests: Exclude tests/blocking_io/ from make test; the dedicated make test-blocking-io suite (and its CI workflow) remains the owner. (#5105)
  • refactor: Share sandbox identity derivation and acquire serialization across the five remote sandbox providers (RFC #4741), replacing five per-provider lock tables that grew unboundedly with process lifetime; derived ids are pinned byte-identical by per-provider golden vectors. (#5089)
  • ci: Split the backend unit-test workflow into four parallel shards — each on its own runner with isolated Postgres/Redis — using pytest-split with a committed duration baseline and failing closed when the baseline is missing; make test remains the canonical full offline suite. (#5137)
  • dev: Launch the Playwright webServer's Next.js through pnpm exec, so Windows E2E runs resolve the platform package binary instead of failing on the extensionless POSIX shim. (#5185)
  • tests: Skip the POSIX mode-bit skill-permission assertions on Windows, where the chmod contract is unobservable, so Windows contributors can reach a green backend-suite baseline. (#5244)

2.0.0 — 2026-06-15

DeerFlow 2.0 is a ground-up rewrite around a "super agent" harness with sub-agents, persistent memory, sandbox execution, and an extensible skills/tools system. It shares no code with the 1.x line, which now lives on the main-1.x branch.

This release closes milestone 2.0.0 with 180 merged pull requests since the first 2.0 milestone tag.

⚠ Breaking changes

  • harness: Hydrate runs from RunStore and persist interrupted status. Run cancellation/multitask semantics now require a working RunStore on the worker that owns the run; cross-worker cancels return 409 instead of silently appearing successful. (#2932)

Added

Agents & runtime

  • agent: Custom-agent self-updates with user isolation — agents can persist edits to their own SOUL.md / config.yaml from inside a normal chat. (#2713)
  • loop-detection: Make loop detection configurable with per-tool frequency overrides; keep configurable on/off switch. (#2586, #2711)
  • loop-detection: Defer warning injection so detector pairs cleanly with tool-call lifecycle. (#2752)
  • run: Propagate model_name from the gateway request through the runtime and persistence stack into the SQLite-backed store. (#2775)
  • subagents: Stream subagent token usage to the header via terminal task events. (#2882)
  • memory: Add memory.token_counting config to opt out of tiktoken for network-restricted deployments. (#3465)
  • suggest: Make AI follow-up question suggestions optional. (#3591)

Models & integrations

  • models: Add StepFun reasoning model adapter. (#3461)
  • community: Add Brave Search web search tool. (#3528)
  • channels: Enhance Discord with mention-only mode, thread routing, and typing indicators. (#2842)
  • im: Add user-owned IM channel connections — users can bind their own Slack/Telegram/Discord/Feishu/DingTalk/WeChat/WeCom accounts on top of the operator-configured bots. (#3487)
  • models: Add patched MiMo reasoning content support. (#3298)
  • models: Add MiniMax provider for image/video/podcast skills plus a new music-generation skill. (#3437)
  • community: Add SearXNG and Browserless web search/fetch tools. (#3451)
  • community: Add Serper Google Images provider for image_search. (#3575)
  • channels: Stream Telegram agent replies by editing the placeholder message in place. (#3534)

Observability

  • trace: Set the LangGraph trace name to lead_agent (or the custom agent's agent_name) for cleaner Langfuse/LangSmith traces. (#3101)
  • frontend: Refine token usage display modes. (#2329)
  • defaults: Enable token usage tracking by default. (#2841)
  • defaults: Raise default summarization trigger threshold. (#3174)
  • trace: Attribute subagent spans to the parent thread's Langfuse trace. (#3611)

Skills

  • skill: Add blocking-io-guard skill for blocking-IO triage and runtime anchors. (#3503)
  • skill: Add maintainer issue and PR workflow skill. (#3554)
  • skill: Strengthen the maintainer orchestrator review workflow. (#3606)

Performance

  • harness: Push thread metadata filters into SQL instead of post-filtering in Python. (#2865)
  • runtime: Index runs by thread_id to avoid O(n) scans in RunManager. (#3499)
  • runtime: Index messages in MemoryRunEventStore to avoid O(n) scans. (#3531)
  • persistence: Cache Base.to_dict column reflection per class. (#3654)
  • sandbox: Speed up should_ignore_name in glob/grep walks. (#3657)

Security

  • upload: Reject symlinked upload destinations. (#2623)
  • uploads: Add Windows support for safe symlink-protected uploads. (#2794)
  • mcp: Mask sensitive values in MCP config API responses. (#2667)
  • mcp: Harden the MCP config endpoint against malformed input. (#3425)
  • auth: Reject cross-site auth POSTs. (#2740)
  • gateway: Cap skill artifact preview decompression to prevent zip-bomb-style abuse. (#2963)
  • sandbox: Mount the host Docker socket only in aio (DooD) sandbox mode. (#3517)
  • sandbox: Do not bind-mount host CLI auth dirs by default. (#3521)

Fixed

Runtime, gateway & persistence

  • runtime: Rollback restore checkpoint now supersedes newer checkpoints. (#2582)
  • runtime: Persist run message summaries. (#2850)
  • runtime: Bound write_file execution-failure observations to keep failure traces from blowing out the context. (#3133)
  • runtime: Protect the sync singleton's init and reset paths. (#3413)
  • runtime: Avoid PostgreSQL aggregate FOR UPDATE on run events. (#2962)
  • runs: Restore historical runs from persistent store after a gateway restart. (#2989)
  • gateway: Return ISO 8601 timestamps from threads endpoints. (#2599)
  • gateway: Make cancel idempotent for already-interrupted runs. (#3058)
  • gateway: Split stream_existing_run into per-method routes for unique OpenAPI operationIds. (#3228)
  • events: Serialize structured DB event content. (#2762)
  • persistence: Emit timezone-aware timestamps from SQLite-backed stores. (#3130)
  • persistence: Reuse token usage model grouping expression. (#2910)
  • runs: Ignore stale run reconnect conflicts. (#3284)
  • nginx: Defer CORS to the gateway allowlist instead of double-applying it. (#2861)
  • persistence: Fix runtime journal run lifecycle events. (#3470)
  • gateway: Enforce thread ownership on stateless run endpoints. (#3473)
  • runtime: Propagate interrupt through SSE values events for the LangGraph SDK. (#3605)
  • serialization: Strip base64 image data from streamed values events. (#3631)
  • history: Strip base64 image data from REST endpoint responses. (#3535)
  • gateway: Attribute token usage to the actual models. (#3658)

Agents, subagents & middleware

  • subagents: Make subagent timeout terminal state atomic. (#2583)
  • subagents: Use model override for tools and middleware. (#2641)
  • subagents: Consolidate system_prompt and skills into a single SystemMessage. (#2701)
  • subagent: Isolate subagents from the parent run's checkpointer. (#3559)
  • agents: Make update_agent honor runtime.context user_id like setup_agent does. (#2867)
  • agents: Resolve duplicate todos channel type conflict in TodoMiddleware. (#3200)
  • agents: Offload blocking filesystem IO in the custom-agent router off the event loop. (#3457)
  • agents: Keep new agent bootstrap in user scope. (#2784)
  • loop-detection: Keep tool-call pairing on warn injection. (#2725)
  • middleware: Sync raw tool-call metadata. (#2757)
  • middleware: Handle invalid tool calls in dangling pairing middleware. (#2891)
  • middleware: Prevent todo completion reminder IM-message leak. (#2907)
  • middleware: Normalize tool result adjacency before model calls. (#2939)
  • agents: Require config.yaml in resolve_agent_dir to skip memory-only directories. (#3481)
  • agents: Sync agent_name across context/configurable and reject empty soul. (#3553)
  • middleware: Offload the uploads scan in UploadsMiddleware off the event loop. (#3311)
  • middleware: Offload memory injection off the event loop to prevent tiktoken blocking. (#3411)
  • middleware: Externalize oversized tool output into the sandbox for non-mounted sandboxes. (#3417)
  • middleware: Preserve the sandbox reducer in middleware state. (#3629)
  • subagents: Raise general-purpose max_turns to 150 and default timeout to 30 min. (#3610)

Memory & tracing

  • memory: Replace short-lived asyncio.run() with a persistent event loop. (#2627)
  • memory: Isolate queued memory updates by agent. (#2941)
  • memory: Parse wrapped memory-update JSON responses. (#3252)
  • tracing: Propagate session_id and user_id into Langfuse traces. (#2944)
  • trace: Decode unicode escape sequences in non-ASCII memory trace info. (#3104)

Tools, sandbox & MCP

  • mcp: Fix env resolution in MCP config lists. (#2556)
  • models: Record Codex token usage in usage_metadata. (#2585)
  • sandbox: Supplement list_running in RemoteSandboxBackend. (#2716)
  • sandbox: Disable MSYS path conversion for Git Bash on Windows. (#2766)
  • sandbox: Avoid blocking sandbox readiness polling. (#2822)
  • sandbox: Uphold the /mnt/user-data contract at the Sandbox API boundary. (#2881)
  • sandbox: Scope provisioner PVC data by user. (#2973)
  • sandbox: Merge idempotent sandbox state updates. (#3518)
  • tools: Introduce Runtime type alias to eliminate Pydantic serialization warnings. (#2774)
  • tools: Preserve tool_search promotions across re-entrant get_available_tools. (#2885)
  • harness: Wrap async-only config tools for sync client execution. (#2878)
  • harness: Wrap all async-only tools for sync clients. (#2935)
  • tool-search: Reliably hide deferred MCP schemas by removing the ContextVar. (#3342)
  • search: Fix DDGS Wikipedia region handling. (#3423)
  • web_fetch: Support a proxy for the Jina reader in restricted networks. (#3430)
  • sandbox: Persist lazily-acquired sandbox state via Command. (#3464)
  • sandbox: Fix stale AIO sandbox cache reuse. (#3494)
  • sandbox: Create a shell session before retrying on a fresh id. (#3577)
  • sandbox: Stop flagging string-literal path fragments as unsafe absolute paths. (#3623)
  • sandbox: Return an actionable hint when read_file hits a binary file. (#3624)
  • mcp: Make stdio MCP-produced files resolvable via virtual sandbox paths. (#3600)
  • mcp: Surface admin-required state on the settings tools page. (#3533)
  • mcp: Add a tools cache reset endpoint. (#3602)
  • uploads: Fix the upload file size contract. (#3408)

Skills & channels

  • skills: Enforce allowed-tools metadata. (#2626)
  • skills: Harden slash skill activation across chat channels. (#3466)
  • skills: Fix custom skill install permissions. (#3241)
  • channels: Authenticate gateway command requests. (#2742)
  • skills: Surface the offending line and a quoting hint on SKILL.md YAML errors. (#3335)
  • skills: Keep skill archive installation off the event loop. (#3505)
  • channels: Ignore hidden control messages when extracting replies. (#3270)
  • channels: Reload config on channel restart. (#3514)
  • channels: Surface WeCom WebSocket connection failures. (#3526)
  • channels: Close the Discord file handle after upload. (#3561)
  • channels: Require a bound identity for user-owned IM messages. (#3578)
  • channels: Scope IM files and helper commands to the owner. (#3579)
  • channels: Make runtime provider state authoritative. (#3580)
  • channels: Harden runtime credential management APIs. (#3581)
  • channels: Make the channel connect flow deterministic. (#3582)
  • channels: Centralize shared channel retry helpers. (#3583)
  • channels: Add operational guardrails. (#3584)
  • channels: Unsubscribe channel listeners by equality. (#3608)

Auth

  • auth: Replace setup-status 429 rate limit with a cached response. (#2915)
  • auth: Persist auto-generated JWT secret so it survives restarts. (#2933)
  • auth: Align auth-disabled mode with mock history loading. (#3471)

Frontend

  • frontend: Restore localhost fallback for getGatewayConfig in prod mode. (#2718)
  • chat: Prevent the first user message from being swallowed in new conversations. (#2731)
  • frontend: Use backend thread token usage for the header total. (#2800)
  • frontend: Wait for async chat submit before clearing the input. (#2940)
  • frontend: Resolve login page flickering and the resize-observer loop. (#2954)
  • frontend: Deduplicate restored thread messages. (#2958)
  • frontend: Avoid duplicate optimistic user message. (#3002)
  • frontend: Hide the copy button for streaming assistant messages. (#3176)
  • frontend: Show a new thread in the sidebar immediately on creation. (#3283)
  • frontend: Isolate new chat thread messages. (#3508)
  • frontend: Cap deeply nested list indentation to prevent render crashes. (#3393, #3570)
  • token-usage: Dedupe token usage aggregation by message id. (#2770)
  • frontend: Fall back to Streamdown clipboard copy. (#3397)
  • frontend: Remove the Backspace shortcut for deleting prompt attachments. (#3410)
  • frontend: Restructure the Memory settings toolbar into two rows. (#3433)
  • suggestions: Strip inline <think> reasoning before parsing follow-up questions. (#3435)
  • frontend: Stop fetching follow-up suggestions when they are disabled. (#3599)
  • frontend: Paginate the workspace chat list beyond 50 threads. (#3485)
  • frontend: Prevent user message bubble overflow with long unbreakable strings. (#3488)
  • frontend: Keep the workspace interactive when the SSR auth probe cannot reach the gateway. (#3495)
  • frontend: Render user messages as plain text and cap blockquote nesting. (#3502)
  • frontend: Reset the active chat after deletion. (#3519)
  • frontend: Improve the mobile workspace layout. (#3646)
  • frontend: Render full content for multi-part AI messages. (#3649)

Build, deploy, scripts & config

  • packaging: Add postgres extra for store/checkpointer support; clarify install guidance. (#2584)
  • harness: Resolve runtime paths from the project root. (#2642)
  • docker: Force nginx to resolve upstream names at request time. (#2717)
  • docker: Default Gateway to a single worker to prevent multi-worker breakage. (#3475)
  • scripts: Preserve uv extras across make dev restarts. (#2767, #2754)
  • scripts: Clean up local nginx on stop. (#3005)
  • deploy: Fall back to python / openssl when python3 is absent for secret generation. (#3074)
  • config: Make the reload boundary discoverable from code. (#3144, #3153)
  • replay-e2e: Key replay fixtures by caller and conversation. (#3453)
  • setup: Refresh LLM provider wizard defaults. (#3421)
  • config: Coerce null config.yaml list sections to an empty list. (#3434)
  • scripts: Exclude runtime state from gateway reload. (#3426)
  • scripts: Create the backend/sandbox dir before the uvicorn reload-exclude. (#3460)
  • scripts: Stop next-server correctly after make start-daemon. (#3498)
  • makefile: Fix per-commit hooks installation. (#3569)
  • replay-e2e: Match replay by conversation, not the living system prompt. (#3436)

Changed

  • provider (refactor): Share assistant payload replay matching across providers. (#3307)
  • lead-agent (refactor): Make build_middlewares public to drop the last cross-module private import. (#3458)
  • todo (refactor): Remove the unused completion reminder counter. (#3530)

Documentation

  • Document blocking-IO detection usage and maintenance. (#3233)
  • Clean standalone LangGraph server remnants from docs. (#3301)
  • Add AI assistance disclosure to the PR template and CONTRIBUTING. (#3398)
  • Document custom AIO sandbox images. (#3548)

Internal

  • dev: Add async/thread boundary detector. (#2936)
  • runtime: Add lifecycle end-to-end coverage. (#2946)
  • windows: Add PYTHONIOENCODING and PYTHONUTF8 to backend Makefile targets. (#3069)
  • blocking-io: Fail-loud repo-root resolution and shared detector CLI shim. (#3512)
  • runtime: Add a Blockbuster runtime anchor for JsonlRunEventStore async IO. (#3313)
  • ci: Consolidate PR/issue labeling and fix the reviewing-job crash and label thrash. (#3455)