From 5c6c074542770c10c8e0e7f0cc0a306e8524fef2 Mon Sep 17 00:00:00 2001 From: Willem Jiang Date: Thu, 1 Oct 2026 09:38:04 +0800 Subject: [PATCH] chore(doc): updated the CHANGLOG with lasted changes (#6121) --- CHANGELOG.md | 1180 ++++++++++++++++++++++++++++++++++++++++++++++- CHANGELOG_zh.md | 941 ++++++++++++++++++++++++++++++++++++- 2 files changed, 2118 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88c2b11bc..1dd5c178af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This section accumulates work toward the **2.2.0** milestone ([2.2.0](https://github.com/bytedance/deer-flow/milestone/3)). -This release closes that milestone with **181 merged pull requests**. +This release closes that milestone with **301 merged pull requests**. ### Added @@ -55,6 +55,47 @@ This release closes that milestone with **181 merged pull requests**. and overlay text is literal, so stock behavior is unchanged until an operator opts in. ([#5794]) +- **agents:** Tool-produced file paths, URLs, and task ids become short, stable + artifact handles (`art_xxxxxxxx`) that survive context compaction, so the model + no longer loses or fabricates exact references after earlier tool messages are + summarized. Each thread keeps a bounded registry in `ThreadState.tool_artifacts` + projected into the model's hidden durable-context block, and a handle passed in + any tool argument — bare, backticked, or nested in dict/list args — is resolved + back to the real reference just before the tool executes; unknown or expired + handles fail the call without running the tool. A new `tool_artifacts` config + section (`enabled`, `max_entries` default 100, `detect_refs_in_text`, + `inject_model_context`, `resolve_handles_in_args`) switches each piece, all on + by default. Handles are agent-local: `task` arguments resolve parent handles to + concrete references, and delegated reports must return concrete ones. Tool-call + steps in the chat timeline show a badge per captured artifact, and SSE `values` + snapshots gain an additive `tool_artifacts` key. ([#4929]) + +- **context:** `history_search` accepts an optional `role` (`user`, `assistant`, + or `tool`). The search returns at most eight matches across all roles, so when + a caller needs a user's earlier correction, repeated assistant discussion or + tool output mentioning the same keyword could fill that budget first. The + filter applies to active and archived messages before the eight-result limit, + using the role already stored in archive records; omitted or null keeps the + unchanged all-roles search, returned roles stay `human`/`ai`/`tool`, an + unknown role returns an error, and no storage migration or `history_read` + change is involved. ([#5927]) + +- **subagents:** Delegations can opt into a JSON syntax acceptance criterion, + `acceptance_criteria=["file:../outputs/report.json json-valid"]`, on both + `task` and `batch_task`. A delegated task could produce a non-empty + `report.json` that is not valid JSON, and the existing existence and + non-emptiness checks could not express the difference. The criterion holds for + complete UTF-8 JSON up to 50,000 bytes; invalid JSON, empty files, and + `NaN`/`Infinity` do not hold; a missing file is negative once absence is + established; oversize, incomplete, or unavailable reads and parser resource + limits stay `UNVERIFIED`. Local reads are bounded binary reads; remote reads + keep the fresh-shell, absolute-utilities, regular-file, and containment checks + and transfer at most 50,001 bytes through a base64 envelope, with no unbounded + `read_file` fallback — unsupported remote tools stay `UNVERIFIED`. Scalars, + duplicate keys, and syntactically valid large numbers are allowed; a UTF-8 BOM + is rejected. Syntax only — no schema or business-field checks; existing + criteria and verdict semantics are unchanged. ([#5947]) + #### Memory - **memory:** Cross-session memory gains `user.cognitiveStyle` for stable @@ -95,6 +136,38 @@ This release closes that milestone with **181 merged pull requests**. corrections, and isolation across agent and user memory scopes, and changes no runtime memory behavior. ([#5564]) +- **memory:** The memory management API — read, reload, status, import/export, + clear, and single-fact CRUD — accepts an optional `agent_name` and then operates + only on that agent's bucket, so each custom agent's memories can be inspected + and maintained in isolation. Omitting the parameter preserves the existing + behavior: reads and import use the default bucket, and clear stays user-wide. + Names are validated (letters, digits, hyphens; otherwise 422), and a backend + must declare `supports_agent_scoped_management` or scoped requests fail with + 501 instead of silently editing the wrong bucket. The Gateway preserves the + caller's spelling because Mem0 treats the agent name as a case-sensitive + storage identity, while DeerMem keeps its own lowercase canonicalization. + ([#5565]) + +- **memory:** Memory extraction gains an opt-in Jev (TypeSafe) pre-screen and + signal classification, both off by default. `memory.prescreen` (`off` / + `shadow` / `enforce`) classifies each conversation batch before the extraction + LLM call: `shadow` only records the verdict; `enforce` lets a low-probability + batch skip the call and advance the watermark, consuming that batch as + "nothing durable here". `memory.signal_classification` (`off` / `shadow` / + `hints`) merges `reinforcement`/`correction` labels into the extraction hints + and, only while the pre-screen is `enforce`, may veto a skip. Both ride a new + shared TypeSafe client, `deerflow.typesafe`, extracted from the guardrail risk + gate — whose behavior is unchanged — with one error taxonomy: a request-level + failure raises, a question-level failure is data + (`AnswerSet.errors_by_question`). A new optional top-level `typesafe:` block + (api key/env, `base_url`, model, timeout, deadline, attempts, backoff) is + shared by all consumers; consumer `config` still wins, so existing guardrail + deployments keep working. `config_version` moves 49 → 50 (`make config-upgrade` + merges the keys), and `backend/scripts/eval_memory_prescreen.py` runs the + shadow evaluation — exact Clopper–Pearson miss bound, per-stratum checks — + that must pass before `enforce`; missing evidence reports `INSUFFICIENT`. + ([#5906]) + #### Knowledge & retrieval - **knowledge:** Custom-agent chats can scope RAGFlow retrieval per message. An @@ -168,6 +241,39 @@ This release closes that milestone with **181 merged pull requests**. provider-specific effort tokens. Profiles without the block keep the unchanged legacy path. ([#5780]) +- **community:** The InfoQuest tools no longer block the agent event loop. + `InfoQuestClient.fetch`/`web_search_raw_results`/`image_search_raw_results` + used blocking `requests.post(..., timeout=30)`, so with the community tools + enabled each call could stall the loop for up to 30 seconds. They now use + `httpx.AsyncClient`, the `web_search`/`web_fetch`/`image_search` tool wrappers + are coroutines, and `web_fetch` offloads the CPU-bound readability extraction + via `asyncio.to_thread` — matching the earlier `jina_ai` migration. Return + values, error strings, and the 30-second transport timeout are unchanged. + ([#5782]) + +- **community:** The Tavily `web_search`/`web_fetch` tools stop blocking the + event loop: `_get_tavily_client()` now returns `AsyncTavilyClient`, and + `web_search_tool`/`web_fetch_tool` are coroutines that await + `client.search(...)`/`client.extract(...)`, closing the client on every + outcome. Credentials handling is unchanged — with no key in the tool config + the client is still built with `api_key=None`, so the SDK keeps reading + `TAVILY_API_KEY` from the environment. ([#5784]) + +- **community:** The Firecrawl `web_search`/`web_fetch` tools become coroutine + tools: `_get_firecrawl_client()` returns `AsyncFirecrawlApp` and the tools + await `search()`/`scrape()` instead of the previously blocking sync calls. + Result normalization, error handling, API key handling, and the custom + `api_url` behavior are unchanged. ([#5786]) + +- **community:** The image-search tool no longer runs DuckDuckGo search on the + event loop. `image_search_tool` is now a coroutine that offloads both config + resolution and the blocking `DDGS` call to a worker via `asyncio.to_thread` + (the `ddgs` SDK has no async API), keeping the loop responsive during config + file access and network I/O. Search parameters, result shape, timeout, and + error handling are unchanged; an already-started request cannot be cancelled + and may keep a bounded worker alive until the existing 30-second timeout. + ([#5788]) + #### Channels - **channels:** WeChat can be connected by scanning a QR code from the web UI. @@ -404,7 +510,7 @@ This release closes that milestone with **181 merged pull requests**. wrapped in LangGraph's `Overwrite`. Without unwrapping, `isinstance(sandbox_state, dict)` returned `False`, causing large tool output externalization to fail and fall back to inline truncation, and - read-before-write lock scoping to miss the active sandbox ID. ([#6051]) + read-before-write lock scoping to miss the active sandbox ID. ([#6015]) - **doctor:** `make doctor` now checks the config file the Gateway actually loads. It always inspected `/config.yaml` and ignored `DEER_FLOW_CONFIG_PATH` and `DEER_FLOW_PROJECT_ROOT`, so a missing override @@ -1505,6 +1611,877 @@ This release closes that milestone with **181 merged pull requests**. before editing anything when `uv` is missing, instead of leaving a half-bumped working tree behind. Only the root package's version line moves. ([#5859]) +- **sandbox:** The temporary sandbox lease acquired by the HTTP upload route is + now released. In remote/provisioner deployments, + `POST /api/threads/{id}/uploads` temporarily acquires the thread sandbox to + sync uploaded files into non-mounted sandboxes, but the lease was never + released — not on success, 413, or failure — and Feishu file sync held the + same non-releasing pattern, so the acquired sandbox (a Pod plus NodePort + Service) never returned to the warm pool. Both paths release after sync, and + a release only parks the sandbox in the warm pool once the last holder + exits: when the same thread already has an active run sandbox, releasing the + temporary lease just decrements it instead of parking the sandbox in use. + ([#3261]) + +- **frontend:** Stop retrying the initial run-creation request after an + ambiguous failure. The LangGraph SDK retries failed HTTP requests by + default, and `POST /runs/stream` is not idempotent: an intermediary could + return an ambiguous gateway error such as `504` after the backend accepted + the request, and the retry then created the same run twice, duplicating + user-visible work. Run creation is now issued with retries disabled, while + reads, explicit stream joins, and SSE recovery — the `GET` to the + server-provided `Location` carrying the `Last-Event-ID` cursor — keep their + normal retry behavior. ([#4725]) + +- **gateway:** A stream-bridge failure no longer cancels a healthy run as if + the client had disconnected. Both creator-side consumers — the SSE + `POST /runs/stream` response and `POST /wait` — applied the creator's + `on_disconnect` policy (cancel) from an unconditional `finally`, so any exit + from `StreamBridge.subscribe()`, including an infrastructure exception or the + subscription exhausting before a terminal event, was treated as a client + disconnect and could cancel a running agent. The policy now applies only to + observed disconnects (`request.is_disconnected()`, SSE generator close, + request-task cancellation); bridge exceptions propagate without cancelling + the run, and premature subscription exhaustion raises a server error. + ([#5623]) + +- **logging:** Redact credential-bearing header values from urllib3's + "Failed to parse headers" warning. The dump carries the raw header block + following the first malformed line, and `UrlRedactionFilter` had no pass + shaped for it, so `Set-Cookie`/`Authorization`/`WWW-Authenticate`/`Location` + values reached the log whole — twice, since the `exc_info` traceback repeats + them. New passes, gated on urllib3's own literals, split the dump on its + logical breaks and collapse only credential-bearing field values, keeping + field names and non-sensitive fields so the record still says what failed; + the exception text runs the same passes via `exc_text`, which + `JsonTraceFormatter` now honors instead of re-rendering exceptions itself + (`logging.enhance.format=json` previously kept leaking). ([#5752]) + +- **uploads:** The link-failure cleanup arm in the upload staging helper + (`_link_staged_no_overwrite`) no longer removes a staged name its caller + still owns. The arm did a hard `os.unlink()` tolerating only + `FileNotFoundError`; on the conversion path (`unlink_staged=False`) the + caller keeps a descriptor on the staged inode, so on Windows the removal + raised `PermissionError`, which replaced the original `os.link` failure the + caller needs to see and left the `.part` behind. The arm now removes the + staged name only when this call owns it, through the best-effort + `_remove_staged_file`, so a removal failure can never mask the link error; + with `unlink_staged=False` the caller cleans up via `_abort_upload_temp`. + ([#5763]) + +- **events:** A cancelled JSONL snapshot no longer releases the per-thread lock while its + file scan is still running. `find_latest_ai_message_run_ids()` takes the per-thread + write lock for one complete thread-log read, but the scan ran through a bare + `await asyncio.to_thread(...)`: cancelling the caller cancelled only the awaiter, so + the lock was released while `_read_thread_events()` was still reading files and a + writer could enter mid-snapshot. The scan now drains through the `_await_owned_task()` + helper (task name `jsonl-snapshot:{thread_id}`); the first caller `CancelledError` + stays primary and a read failure is chained as its cause. ([#5795]) + +- **sandbox:** Hold the AIO cross-process flock until its lifecycle worker settles, even + when the caller is cancelled. `_discover_or_create_with_lock_async()` dispatched + discovery, registration and creation through bare `asyncio.to_thread()`; cancelling + the caller released the flock while a worker was still mutating the sandbox lifecycle, + letting another process enter the same discover/create critical section. Sync steps + under the lock now drain through `run_sync_lifecycle_operation()`, the whole async + create runs inside `await_drained()` so a created sandbox still reaches readiness and + registration, and the flock is retried with cancellable non-blocking attempts so a + cancelled waiter exits promptly. ([#5820]) + +- **helm:** The chart now plumbs the optional lark-cli sandbox runtime images into the + provisioner. The provisioner reads `LARK_CLI_INIT_IMAGE` and `LARK_CLI_BROKER_IMAGE` + and Compose and the root README document them, but the chart had no corresponding + value, so an operator following the README had nothing to set and the sandbox + silently launched without `lark-cli`, failing only later with `command not found` + inside the sandbox. New values `provisioner.larkCliInitImage` and + `provisioner.larkCliBrokerImage` (broker supersedes init) render the matching env var + on the provisioner Deployment only when set; unset, they are omitted entirely so + existing releases render an unchanged Pod spec. ([#5835]) + +- **tenki:** A Tenki `sticky` setting supplied through an environment reference + (e.g. `sandbox.sticky: $TENKI_STICKY` with `TENKI_STICKY=false`) no longer + enables host pinning. Environment substitution leaves this provider-specific + field a string, and the previous `bool(...)` conversion counted any + non-empty string — `false`, `0`, `off`, `no` — as `True`, so the provider + asked `Client.create` for sticky sandboxes the operator had disabled. + Values are now parsed with Pydantic's boolean validator: + case-insensitive `false`/`0`/`off`/`no` disable pinning, + `true`/`1`/`on`/`yes` enable it, omitted or `null` stays disabled, native + YAML booleans are unchanged, and an invalid value fails provider + initialization. ([#5849]) + +- **config:** `stream_bridge.recovered_stream_cleanup_delay_seconds` now + rejects booleans, non-finite values, and anything above 86400 at load time, + like its guarded sibling `heartbeat_interval_seconds`. The field only + carried `ge=0`, so a hand-edited `config.yaml` with `…: true` was silently + coerced to 1.0 — collapsing the reconnect grace window to one second — and + `…: .inf` or `1e30` was accepted as-is, making the post-END cleanup task + sleep forever and keeping an orphaned run's Redis stream until the rolling + `stream_ttl_seconds` (default 86400) expired it instead of the configured + grace delay. `0` stays valid ("delete as soon as END is published") and the + documented default of 60 is unchanged. ([#5850]) + +- **community:** DDG `web_search` now warns and falls back to the default of + 5 results when `max_results` in `config.yaml` is a boolean or a fractional + number, as the README already documents and as `image_search` has done + since #5807. `web_search`'s `_coerce_max_results` went straight through + `int()`, which truncates `3.5` to 3 and counts `true` as 1, so the tool + quietly narrowed its result set instead of flagging the invalid value. + Integral floats such as `4.0` and numeric strings keep their behavior. + ([#5852]) + +- **llm:** `strip_think_blocks()` no longer slows quadratically on model text with many + unclosed `` tags. The helper cleans model text used by goal evaluation, + suggestions and input polishing, and its complete-block regex retried the remaining + suffix at every opening tag, so 12,000 repeated `` tags took about 3.1 seconds + with the cost still growing. It now scans opening and closing tags forward in one + pass, consuming each matched block once and stopping at the first unclosed opening + tag; case-insensitive matching, opening-tag attributes, whitespace before the closing + `>`, and both `truncate_unclosed` modes are unchanged. ([#5854]) + +- **sandbox:** `ls /mnt/skills` no longer fails with "Directory not found" when only + category mounts exist. In the default skills projection the provider mounts + `/mnt/skills/public`, `custom`, `legacy` and `integrations` but maps no `/mnt/skills` + root, so `LocalSandbox.list_dir` resolved the root to its literal host path and the + host scan raised `FileNotFoundError` before the virtual sub-directory overlay could + list the categories. `list_dir` now treats a missing host path as an empty listing + when at least one mapping is mounted inside the requested container path; paths with + nothing mounted inside them still raise `FileNotFoundError`. ([#5857]) + +- **examples:** The Jev fetched-content screening example no longer lets a misbehaving + endpoint or an unusual deployment past its fail-open and data-egress guarantees. A + deeply nested answer body (`RecursionError`) or a huge `noul` integer + (`OverflowError`) escaped the fail-open path and, inside the per-message task group, + cancelled the other messages' screening requests and dropped every flag of the tool + call — parsing now runs inside the fail-open block, which catches both. The 16 KiB + response cap counted decoded bytes, so a compressed body could expand past it first; + requests now send `Accept-Encoding: identity` and drop encoded responses. A + plain-HTTP loopback endpoint, the only plain-HTTP form the validator accepts, + followed `HTTP_PROXY` and `ALL_PROXY` and sent the bearer key and excerpt to the + proxy in cleartext; such endpoints now build the client with `trust_env=False`, + while HTTPS endpoints keep proxy settings. The root README also no longer says the + excerpt is always PII-redacted: it is sanitized, and redacted only when + `pii_redaction` is enabled. ([#5858]) + +- **scheduler:** One scheduler's late completion write can no longer clobber another + scheduler's fresh claim of the same run. `ScheduledTaskRunRepository.update_status` + guarded `expected_lease_owner` (and the `protect_terminal` backfill) on a plain + `session.get`, so when an expired launch lease was requeued and re-claimed by another + scheduler (`lease_owner=B`) between that read and the write, the stale guard passed + and the first scheduler's late write cleared B's lease fencing and wrote `failed` + over its `launching` claim. The row is now re-read under the parent task's writer + lock — the same task → run lock order every other mutating path in this repository + uses — with `populate_existing=True` so the identity map serves the fresh row; + Postgres gains a matching `FOR UPDATE` row read. ([#5860]) + +- **tools:** A synchronous call on a bound `task` or `batch_task` tool now goes + through the bound runtime's own submitter and execution capacity instead of + the process-global fallback. `get_available_tools` wraps the shared tool + singletons in place with a sync `func`, but `bind_task_tool` and + `_bind_batch_tool` rebound only the `coroutine` of their copies, so each copy + inherited a sync wrapper around the unbound coroutine: its sync path fell + through to the process-global submitter (the exact fallback the binding + contract rules out) or raised `NotImplementedError` in a fresh process where + the singleton had never been wrapped. Both binders now rebind `func` through + the same `make_sync_tool_wrapper` helper, so both invocation paths of a bound + copy run the bound coroutine. ([#5861]) + +- **models:** CLI credential files whose token fields are not strings no + longer load as "successful" credentials. The loaders in + `credential_loader.py` guarded container fields, `expiresAt`, and the Codex + `account_id`, but not the tokens, so a hand-edited or script-rewritten file + with a number, list, or object in `accessToken`/`access_token` passed the + truthiness check — a malformed Claude override from + `$CLAUDE_CODE_CREDENTIALS_PATH` masked the valid + `~/.claude/.credentials.json` fallback, and a bogus Codex token became a + garbage `Authorization: Bearer …` header ending in a confusing 401. Such + tokens are now skipped (strings are stripped of padding, blanks count as + missing), and a non-string `refreshToken` degrades to empty like + `account_id`. ([#5864]) + +- **mcp:** Durable MCP task status and cancel calls now authenticate as the + task's persisted owner on HTTP/SSE servers with `user_auth` enabled. A task + that submitted fine inside an authenticated Agent turn then failed every + background call: `McpTaskToolCaller` never exposed the stored owner to the + credential interceptor, so without a live run/request context credential + selection resolved to `default`, the service retried that authentication + failure instead of observing the remote task, and a service rebuilt from SQL + failed the same way. Background HTTP/SSE calls with enabled `user_auth` now + bind an ID-only owner through the existing user ContextVar and restore the + previous context in `finally`; submit, stdio, and calls without `user_auth` + keep their original context, concurrent owners stay isolated, and background + calls still never touch `headers_from_context` request secrets. ([#5870]) + +- **agents:** Loop detection no longer hard-stops paginated or content-varying + calls to generic tools. The per-call key kept only the first salient + argument (`path`/`url`/`query`/`command`/...), so an MCP `fetch` resuming + with `start_index`, a search paged with `page`, or `list_uploaded_files` + following its `cursor` collapsed onto one key, and the fifth distinct call + ended the run with `stop_reason=loop_capped`. Keys now cover every argument; + only the UI-narration `description` of `bash`/`ls`/`glob`/`grep` stays + excluded (rewording it still cannot dodge detection), while `description` + elsewhere remains keyed because it can be the payload. ([#5872]) + +- **middleware:** Input sanitization no longer strips message metadata from + sanitized user messages. `InputSanitizationMiddleware` rebuilt each + sanitized `HumanMessage` from four selected fields, so anything outside + content/id/name/`additional_kwargs` — `response_metadata` carrying gateway + provenance in particular — was dropped in both the text and multimodal + paths. Sanitized messages are now copied from the original with only + `content` and `additional_kwargs` replaced, so metadata, identity fields, + and the message subclass survive while the original request message stays + untouched. ([#5875]) + +- **community:** InfoQuest timeout options in `config.yaml` are now normalized the way + the sibling search/fetch providers normalize theirs. `deerflow.community.infoquest.tools` + handed the five documented numeric options (`timeout`, `fetch_time`, + `navigation_timeout`, `search_time_range`, `image_search_time_range`) to + `InfoQuestClient` unvalidated, and a `$VAR` reference is substituted verbatim from + the environment — so `timeout: $FETCH_TIMEOUT` arrived as a string and the client's + `> 0` comparisons raised `TypeError`, turning `web_fetch`, `web_search` or + `image_search` into a hard failure whose message pointed at the client instead of + the config key. Values now pass through one coercion step: an integer, an integral + float or an integer-form string is used as configured, while a boolean, a fractional + value, a blank/non-numeric string or `None` falls back to the documented `-1` (that + filter is disabled) with a warning naming the rejected key. An already-valid integer + config produces a byte-identical request. ([#5883]) + +- **sandbox:** Keep AIO acquire serialization until a started acquire worker settles, + even when the caller is cancelled. Inside the per-`(user_id, thread_id)` serializer + region, in-process cache reuse and warm-pool reclaim ran as bare `asyncio.to_thread()` + calls, so cancellation released the scope while the worker was still running and a + retried acquire for the same user/thread could overlap it. Both steps now run through + a helper that cancels work still queued (the old behavior) but keeps serializer + ownership and drains a started worker across repeated cancellation; the flock wait, + readiness polling, and the skills-projection step remain cancellable. ([#5888]) + +- **skills:** Managed integration skill discovery stops descending at each skill package + boundary. Unlike the public and custom skill loaders, the integration walk kept going + after finding a `SKILL.md`, so a nested evaluation fixture carrying its own + `SKILL.md` could surface as a runtime skill — or, with a matching frontmatter name, + replace the real public skill. Provider and namespace directories without a + `SKILL.md` of their own still expose their nested skills. ([#5890]) + +- **models:** Claude threads no longer die with HTTP 400 after a few turns + when prompt caching is on. `ClaudeChatModel._apply_prompt_caching` wrote + its `cache_control` markers into blocks the request shares with the thread + — langchain-anthropic passes Claude-native blocks (an image or document + with a `source`, a search result) by reference — so markers were + checkpointed with the messages, and from the third turn every request + exceeded Anthropic's four-breakpoint limit with no way to recover the + thread. Markers now land on copies, and every request first strips markers + already present, with caching on or off, so a thread whose checkpoint + carries stale markers works again on its next turn without a migration. + ([#5892]) + +- **subagents:** A durable batch no longer stays `running` forever when its + last active item dies from an expired lease on its final allowed attempt. + `claim_items` marked such an item `failed` but never refreshed the parent + batch, so a batch whose final item ended this way kept `running` with no + `completed_at` and no terminal result to observe. Lease recovery now + refreshes the batch in the same transaction: a batch with no successful items + becomes `failed`, one with partial success becomes `completed`, failed item + counts stay visible, and an explicit item retry reopens the batch as before. + ([#5897]) + +- **agents:** A provider that reuses a `tool_call_id` across runs no longer + corrupts the durable delegation ledger. Entries were deduplicated by the raw + ID with the earlier `run_id` carried forward, so a later run's delegation + with the same ID could replace the earlier entry — its task vanished from + durable context and `max_total_subagents` undercounted that run — and its + reply could be mistaken for one to an older orphaned task. The ledger now + keys entries by `(run_id, tool_call_id)` for capture, the retained window, + and terminal-status protection; orphan closure on a new user turn is bounded + by that run's opening message, and a resumed run without a new user message + keeps an already-answered entry. ([#5898]) + +- **sandbox:** Sandbox network-policy mutations now drain across caller cancellation. + The async wrappers for `consume_network_policy_events()`, + `deny_pending_network_policy_events()` and `decide_network_policy_request()` offloaded + the stateful trusted-proxy mutations with bare `asyncio.to_thread()`, so cancelling + the awaiting tool/agent task left the lifecycle boundary while the policy state was + still changing. All three now route through the cancellation-safe + `run_sync_lifecycle_operation()` helper: the admitted worker settles first, then the + original `CancelledError` propagates. ([#5899]) + +- **tracing:** Langfuse traces now carry the model that actually runs. The + `model:` tag was injected before model resolution, so a default-model + run could carry no model tag at all, and a fallback run kept the name of the + request that was rejected. `LeadAgentAssembly` now exposes the resolved + model as `effective_model`; the Gateway worker injects trace metadata after + assembly, and the embedded client resolves its agent before injecting, so + both tag the selected default or fallback model while caller-provided + metadata keeps precedence. ([#5902]) + +- **runtime:** Image-only user input is now journaled once per run. + `RunJournal` used the optional text summary to decide the input was already + captured, and a textless message leaves that summary `None`, so every later + model call appended another `llm.human.input` event and inflated + `message_count`; a batched callback could even record an older prompt after + the latest image-only turn. Capture is now tracked independently of the + nullable summary, and the batch scan stops once input is recorded; the media + content and the empty display text are preserved. ([#5903]) + +- **mcp:** Make MCP task notification recovery rollback-safe ahead of the + atomic launch reservation. The worker reused the batch scan timestamp after + asynchronous run lookups and launches, so retry delays started too early and + repository expiry predicates compared against a stale time, and unexpected + per-record failures were handled only after the whole batch `gather()` + finished, tying one record's retry timing to its slowest sibling. Lookup and + launch outcomes now use a completion time bounded not to precede the claim, + per-record failures are released inside each child task, and repositories can + read, recover, poll, and settle a `launching` notification — preserving its + snapshot and idempotency key — while no production path writes that state + yet, so an older Gateway stays compatible after a rollback; the frontend keeps + polling while a notification is `launching`. ([#5908]) + +- **subagents:** Cancelled or timed-out sub-agent executions keep token usage + already reported by completed model calls. A model can finish and report + usage while model middleware is still awaiting, before the graph publishes + its next `values` frame; cancellation in that window bypassed the + result-handling branches, so the outer timeout/cancellation wrapper finalized + the result from the older progress snapshot — two completed responses costing + 15 and 30 tokens reported only 15. After stream cleanup drains, the executor + now publishes the token collector's final cumulative snapshot before the + terminal status is chosen; first-terminal-write semantics, cancellation + propagation, and parent-journal deduplication are unchanged. ([#5910]) + +- **paths:** IDs with a trailing newline are now rejected by the path + isolation validators. Python's `$` also matches just before a final newline, + and `_validate_user_id`, `_validate_project_id`, and + `_validate_integration_id` used `.match()`, so a caller-supplied ID like + `"alice\n"` passed the documented restricted-charset check and produced a + filesystem bucket the strict grammar never intended. All three now require a + `fullmatch`, so only documented characters through end-of-string are + accepted. ([#5911]) + +- **uploads:** An upload filename containing a NUL character no longer fails + the request with a 500. Normalization let the embedded NUL through to + `os.lstat()`, which raised an unhandled `ValueError` + (`lstat: embedded null character in path`) and aborted the upload. + `normalize_filename` now rejects such names up front; in a multi-file + request the invalid file is skipped and the remaining valid files are still + stored. ([#5915]) + +- **goal:** The agent now sees the active `/goal` on every model call. + `ThreadState.goal` previously reached only the goal evaluator, so a goal set + through `PUT /goal`, `DeerFlowClient.set_goal()` or the TUI was ignored, and + compaction could take the goal away with the message that stated it. + `DurableContextMiddleware` now opens its hidden `` + message with an `` element — HTML-escaped, capped at 4,000 + characters like the goal routes, and PII-redacted when redaction is on — and + the durable-context contract gains a static exception letting the agent work + toward that element at user priority, with no system or developer authority. + `active_goal` joins the input sanitizer's tag denylist, so user input and + remote results cannot forge it; subagents, whose state carries no goal, are + unaffected. ([#5917]) + +- **frontend:** A fence line with an info string (`` ```python ``) no longer + closes the open code block. CommonMark requires a closing fence to be at + least as long as the opening one and to carry no info string, but of the + three fence scanners only the Mermaid normalizer checked both halves — the + render path (`stripLeakedSystemTags`) and the export path + (`stripInternalMarkers`) compared just fence character and length, so the + block ended early and the rest was treated as prose: render stripped the + system-marker tags inside sample code and exposed their content, and thread + export deleted the whole ``/`` span, losing user text. + Both scanners now share the `isClosingFence` predicate that moved into + `core/streamdown/fences.ts`. ([#5919]) + +- **skills:** Skill discovery no longer rescans the same tree through symlink + cycles. `load_skills()` followed directory links but never detected a link + back to an ancestor, so two cyclic links in a namespace repeatedly expanded + the same tree — real scans exceeded a three-second diagnostic deadline where + ordinary external links completed in under a millisecond. A shared + `walk_skill_directories` helper now prunes a directory whose resolved path + is already on the current traversal branch, across local and user-scoped + public/custom/integration/legacy discovery; independent aliases of an + external tree remain discoverable. ([#5921]) + +- **backend:** asyncio subprocesses work again on native Windows. Since + #3236, a `backend/sitecustomize.py` switched every Python process with + `backend/` on `sys.path` to `WindowsSelectorEventLoopPolicy`, whose loop + cannot spawn subprocesses: the `invoke_acp_agent` tool failed instantly + with an empty error (a bare `NotImplementedError`), and stdio MCP servers + and local-sandbox command execution hit the same wall. The policy's stated + premise (async psycopg) no longer holds — the postgres driver is asyncpg, + which ships no Windows wheels, and the default SQLite backend runs on + either loop. The file is deleted, restoring Python's default Proactor loop + on Windows; the postgres benchmark script now sets the selector policy only + for its own postgres cases. ([#5937]) + +- **sandbox:** Non-finite OpenSandbox timeouts are rejected at provider construction + instead of failing the first sandbox acquire. These options are provider-specific + extras, so YAML `.nan` and `.inf` survived `SandboxConfig` validation and the + provider's sign-only checks accepted them, deferring the failure to SDK or + `timedelta` conversion at acquire time with a low-level runtime error. + `request_timeout` and `ready_timeout` must now be finite positive numbers and + `sandbox_timeout` finite non-negative (`0` still means explicit cleanup); + `config.example.yaml` documents the requirement. ([#5944]) + +- **subagents:** Context snapshots no longer drop Anthropic-native `document` + blocks. With `context_mode="snapshot"` delegation, `document` was missing + from the recognized media types, so the block was silently discarded and a + child could lose a requirement the parent had. Serializable documents are now + kept as historical user-role data, with the existing reserved-tag and + user-input neutralization applied at capture time to their textual fields + (`title`, `context`, text-source `data`, content-source text blocks, and + citation prose such as `cited_text`), `cache_control` removed, and nested + copies isolated from the parent. Neutralization runs at capture because + hidden snapshot messages skip the input-sanitization middleware; an + unserializable document falls back to the existing historical-media omission + notice. ([#5948]) + +- **sandbox:** The middleware path that applies a network-approval response now drains + across cancellation. `SandboxMiddleware.abefore_agent()` offloaded + `_apply_network_policy_response()` with bare `asyncio.to_thread()`, so cancelling the + awaiting agent task could leave the middleware boundary while a started + `decide_network_policy_request()` worker was still mutating sandbox network-policy + state — bypassing the provider wrapper that #5899 already made cancellation-safe. + The async bridge now routes through `run_sync_lifecycle_operation()`; the synchronous + `before_agent()` path is unchanged. ([#5949]) + +- **runtime:** Subagent step-event flushes now survive host cancellation. A flushed + batch was removed from `_pending` before `RunEventStore.put_batch()` was awaited, and + only ordinary failures were re-buffered — `CancelledError` bypassed the + `except Exception` fence, so cancellation could unwind the flush while persistence was + in flight and a later store failure silently lost the batch. Each admitted write now + runs in its own shielded task and absorbs repeated cancellation; if the drained write + fails, the batch is re-buffered before the original `CancelledError` propagates, and + ordinary store failures still log and re-buffer. ([#5950]) + +- **context:** Two distinct `task_note` additions in one model response can no longer + silently delete an existing note. Each call read the same state snapshot and merged + its own update, so with seven existing notes the merged result kept only the last + eight keys while both calls still returned `saved`. Remaining slots are now reserved + for distinct new keys in the batch's tool-call order: excess additions return + `note_capacity` before emitting a state update, existing notes are preserved, + repeated writes to one new key share a slot, and structurally invalid sibling calls + do not consume capacity. Slots freed by sibling deletions or failed calls become + available in the next batch. For deployments with `task_continuity.enabled`, this + changes the documented last-eight retention behavior for parallel overflow. + ([#5954]) + +- **mcp:** Deleting a thread now also tears down its persistent MCP sessions. + `DELETE /api/threads/{id}` removed checkpoints, runs, events, feedback, and + the live browser session but never the thread's persistent stdio MCP sessions + — `close_scope` had no production caller — so a stateful server such as + Playwright kept server-side state a recreated thread with the same id could + inherit (stale pages, cookies), while the leaked owner task and its + subprocess held memory and file descriptors. Deletion now calls the new + `MCPSessionPool.close_thread_scope()` best-effort after the browser-session + step, matching the legacy scope and every incarnation of that user/thread + (established entries and in-flight creations) while leaving other threads + untouched; a failing teardown still deletes the thread. ([#5956]) + +- **tools:** Community search tools no longer fail on an infinite + `max_results`. The coercion guards caught only `TypeError` and `ValueError`, + but `int(float("inf"))` raises `OverflowError`, and both `yaml.safe_load` + (`.inf` is a legal YAML float) and `json.loads` (accepts `Infinity`) can hand + the guard an infinite value — instead of falling back to the default, the + coercion blew up and SearXNG's `web_search_tool` returned an error payload + for every search. The guards in brave, serply, serper, searxng, groundroute, + and sofya (`max_results`, plus Sofya's content limit) now also catch + `OverflowError`, so an infinite value falls back to the default like any + other invalid input. ([#5957]) + +- **models:** Whitespace-only Codex CLI credentials are now rejected at model + initialization instead of producing an invalid `Authorization` header. The + loader accepted any string token, so an `auth.json` with + `"access_token": " "` constructed `CodexChatModel` successfully and the + misconfiguration only surfaced when a provider request failed. String + tokens are trimmed before the existing type and empty checks, blank or + whitespace-only values count as missing credentials (the existing + "Codex CLI credential not found" error), and a valid token keeps working + with surrounding whitespace normalized away. ([#5959]) + +- **agents:** An agent name differing only by a trailing newline is now + rejected with 422 instead of a 500. The Gateway router validated + `AGENT_NAME_PATTERN` with `.match()`, and Python's `$` matches before a + final newline, so `POST /api/agents` with `{"name": "reviewer\n"}` passed + the router and crashed in `FileAgentStore.create()`'s `fullmatch` — the + actionable message stayed in the server log while the caller got an internal + error. DeerMem's `validate_agent_name` accepted the same names, letting + `agent_facts_directory()` build a bucket its own validator then refuses to + read back. Both validators now use `fullmatch`; nothing previously valid + changes. ([#5960]) + +- **gateway:** Gateway shutdown now closes pooled MCP sessions instead of + abandoning them. The lifespan teardown closed the browser session manager but + never `MCPSessionPool`, although a pooled session's owner task is the only + thing that runs the transport's `__aexit__` — once the event loop stopped, + stdio MCP subprocesses and SSE/HTTP connections got no orderly close at all. + Lifespan shutdown now awaits `MCPSessionPool.close_all()` immediately before + the browser-session step, bounded by the shared shutdown-hook timeout and + best-effort: a timeout or failure is logged and the remaining teardown + proceeds. ([#5961]) + +- **tui:** The headless `deerflow --print` and `--json` runners no longer leak + a Python traceback when a run fails. Session creation, thread resolution, + and stream iteration ran without an error boundary, so any raised exception + — including the documented `CheckpointModeMismatchError` — escaped `main()` + with no stable non-zero exit code. `--print` now prints one + `Error: ` line to stderr and exits 1; `--json` appends a terminal + `{"type": "error", "data": {"message": ...}}` record — every stdout line + stays valid NDJSON even on a mid-stream failure — and exits 1. + `KeyboardInterrupt` and `SystemExit` still propagate. ([#5963]) + +- **mcp:** MCP task notification launches are now reserved atomically before an + Agent run is created. The worker used to launch the run before persisting any + durable launch phase, so if ownership expired or was reclaimed during that + side effect, another worker could launch the same notification concurrently. + The reservation is a single conditional `UPDATE` (`begin_notification_launch`) + fenced by lease owner, per-claim token, lease expiry, and dispatch version; a + stale unstarted snapshot facing a newer event is rejected before any launch + side effect, while a recovered `launching` row keeps its original event and + idempotency key. Deterministic launch rejections (400, 401, 403, 404, a bare + 409, 422, 501) dead-letter immediately, and ambiguous 408/429/500 outcomes or + cancellation preserve the `launching` snapshot for reconciliation without + consuming the retry budget. No schema migration. ([#5965]) + +- **mcp:** MCP connections created in the web editor now belong to the + signed-in user instead of the deployment. The editor wrote deployment-wide + configuration, so a connection added with a user's credentials became a + shared Agent capability while ordinary users could not maintain their own. + New owner-only CRUD lives at `/api/mcp/personal/config`, persisted under + `.deer-flow/users//integrations/mcp.json`; the capability center + shows deployment guides and shared connections under "Platform provided" and + personal ones under "My plugins"; assembly combines shared tools with only + the current user's connections (personal discovery stays outside the global + tool cache), and durable tasks record the owning scope so submit, poll, and + cancel keep it even when a deployment server name collides with a personal + runtime name. Personal HTTP/SSE endpoints may connect only to + public-network-vetted addresses with the original Host, TLS SNI, and + certificate verification preserved, and privileged personal definitions + recheck current administrator authority at discovery, invocation, and durable + submit/poll/cancel (demotion fails closed). `config.yaml` tools and existing + `extensions_config.json` entries remain shared. ([#5966]) + +- **middleware:** PII redaction no longer empties `response_metadata` on the + user messages it rewrites. The middleware hand-built the redacted + `HumanMessage` from content/id/name/`additional_kwargs`, so a message + carrying gateway provenance or other metadata reached the model with that + field silently reset whenever redaction changed its content; the redaction + itself was already correct. The rebuilt message now copies the original and + replaces only `content` (deep-copied, since untouched blocks are retained + by reference) and `response_metadata`, keeping `id`, `name`, and the message + subclass without sharing mutable state with the original. Placeholders and + redaction behavior are unchanged. ([#5968]) + +- **agents:** Caller-attached `response_metadata` on the user turn now survives + dynamic-context injection. `DynamicContextMiddleware` rebuilt the injected + copy as a fresh `HumanMessage` carrying only + `content`/`id`/`name`/`additional_kwargs`, silently dropping every other + field — including `response_metadata` — from the model-bound message list, + while the sibling PII-redaction and thread-data middlewares already used + `model_copy`. The copy is now built with + `original.model_copy(update={"id": ...})`, keeping the documented + `{id}__user` id swap; `UploadsMiddleware`'s user-message rewrite gets the + same treatment. ([#5977]) + +- **persistence:** A literal `+` in a PostgreSQL DSN query string survives + schema injection. When `database.postgres_schema` is set, + `dsn_with_search_path()` round-tripped existing parameters through + `parse_qsl`/`urlencode`, and `parse_qsl` decodes `+` as a space — an + HTML-form rule libpq does not apply — so `application_name=web+app` reached + the server as `web app`, and `options=-c timezone=UTC+8` decayed into + `-c timezone=UTC 8`, no longer one `-c` setting. The helper now carries + existing parameters through byte-for-byte and only appends (or merges into) + a percent-encoded `options=-c search_path=`; keyword-form DSNs and + `postgresql+asyncpg://` normalization are unchanged. ([#5978]) + +- **goal:** An active goal now survives a failed artifact delivery. A run could write + and present an output, receive a satisfied goal evaluation, and delete the goal — and + only afterwards would delivery verification mark the same run as an error (a terminal + delivery-receipt write failure could produce the same inconsistency), leaving the + thread without its goal despite unfinished work. A satisfied evaluation is now only a + completion candidate: the worker clears the goal after delivery, receipt persistence + and terminal cancellation arbitration succeed and successful status persistence is + confirmed, re-checking the goal, the visible conversation and the checkpoint under + the existing checkpoint-write reservation so a newer admitted run wins. Any failure + retains the original goal and counters without an extra model call; normal + completion, including runs without changed outputs, still clears the goal. + ([#5980]) + +- **config:** `extensions_config.json` saved as UTF-8 with a byte-order mark + now loads. Both the runtime loader (`ExtensionsConfig.from_file`) and the + raw read-modify-write reader behind Gateway skill toggles opened the file + as plain UTF-8, so an editor-written BOM failed with + `Unexpected UTF-8 BOM (decode using utf-8-sig)` — blocking MCP/skill + configuration loading and preventing a toggle from saving an otherwise + valid configuration. Both readers now accept an optional leading BOM; + writes remain UTF-8 without one and raw `$VAR` references are still + preserved. ([#5983]) + +- **agents:** A non-finite `read_file` line bound no longer crashes the run + inside loop detection. A model-emitted number such as `start_line: 1e999` + parses to `float("inf")`, and `_coerce_line_number()`'s `int()` conversion + raised an uncaught `OverflowError` on the `after_model` path, ending the run + with an opaque internal error instead of letting `read_file`'s own schema + validation return a normal tool error. The coercer now treats + `OverflowError` like its other unusable inputs: the bound falls back to + `None` and the call keys as the existing open-ended read; integral bounds + behave exactly as before. ([#5990]) + +- **integrations:** Completing a Lark authorization no longer flips the + integration panel's runtime card to "not ready" in remote provisioner mode. + The mutation response verified authorization but never probed sandbox + runtime readiness, and the frontend cached it wholesale, so the panel showed + a state a page refresh would contradict. Install, configuration completion, + credential switching, and authorization completion now share one + mutation-status builder probing runtime readiness consistently with the + `GET /lark/status` route, built after the per-user credential lock is + released (the probe previously ran while holding that lock and could block + other credential operations for a network timeout). The status API exposes + `sandbox_runtime_probed`, letting the frontend keep previously confirmed + runtime fields only for unprobed mutation responses. ([#5994]) + +- **channels:** Feishu and DingTalk inbound attachments are now readable by + the sandbox. In AIO/Docker sandbox mode the sandbox runs as a non-root user + over the bind-mounted thread directory, and these two channels persist + inbound files themselves at `0o600` — the agent got a + `/mnt/user-data/uploads/` path that read as empty, so attachments from + these channels were silently unusable. Both now grant group/other read + right after their own write, matching the channel manager and HTTP upload + paths (Telegram was already covered by #5581). Local sandboxes are + unaffected. ([#5998]) + +- **database:** `DatabaseConfig` now validates `pool_size`, `pool_recycle`, + and `command_timeout` strictly. YAML booleans were silently coerced to + `1`/`0`, so `pool_size: true` limited the pool to a single connection and + `command_timeout: true` imposed a 1-second statement timeout; `pool_size` + also accepted non-positive values (`0`, `-1`), and `command_timeout` + accepted `inf` (via YAML `.inf` or `"1e999"`), which never times out. + `pool_size` and `pool_recycle` now enforce positive integers, and + `command_timeout` rejects booleans and non-finite floats while `null` still + disables the timeout. ([#6009]) + +- **community:** The Firecrawl tools now close the pooled async HTTP client + each call constructs. Since the async migration, `_get_firecrawl_client()` + builds a fresh `AsyncFirecrawlApp` per call, and that app eagerly creates an + `httpx.AsyncClient`-backed pool with no public teardown, so every + `web_search_tool` / `web_fetch_tool` call abandoned one unclosed pool. Both + tools close it in a `finally` through a best-effort + `_aclose_firecrawl_client` helper: a client without the internal + `_v2_client.async_http_client` handle degrades to a no-op, and close failures + are logged without masking the tool's own result or error. ([#6013]) + +- **capabilities:** Administrators can manage shared MCP servers again from the + Capability Center. After #5966 the Platform provided section rendered the + same read-only projection for admins and ordinary users, so add, edit, + enable/disable and delete were impossible even though the admin-only + `/api/mcp/config` endpoints still supported them. Administrators (outside + static mode) now get the MCP editor under Platform provided, writing the + shared deployment configuration that applies to all users; ordinary users + keep the read-only view, and My plugins continues to manage personal + configuration. MCP queries distinguish both scope and user ID, and control + accessible names carry the localized ownership section, so same-named shared + and personal connections are distinguishable. ([#6018]) + +- **gateway:** Drain managed subagent store mutations across request cancellation. The + create/update/delete endpoints of `/api/subagents` offloaded persistent store writes + with bare `asyncio.to_thread()`: cancelling the request stopped only the awaiter, so + an already-started file/SQL store mutation could keep committing after the request + lifecycle had unwound. All three writes now route through the existing + `await_drained()` helper (wrapped as one `_run_store_mutation` helper), so an + admitted mutation reaches terminal state before the caller's cancellation propagates; + catalog reads and validation keep their existing cancellation semantics. ([#6023]) + +- **gateway:** Drain the admin-only managed-model save across request cancellation. + `PUT /api/managed-models` persisted the shared encrypted model catalog through bare + `asyncio.to_thread(_save, body)`: cancelling the request stopped only the awaiter, so + an already-started `ManagedModelStore.save()` could keep committing catalog state + after the request lifecycle had unwound. The save now drains through the existing + `await_drained()` helper, and a worker failure after cancellation is logged with the + exception type only before the consumed result discards it — storage errors can carry + credentials. Catalog reads and connection probes keep their existing cancellation + semantics. ([#6024]) + +- **config:** Numeric config fields that accepted silently-broken values now + carry range bounds and boolean rejection, mirroring the earlier + stream-bridge and database-pool guards. `sandbox.port` is bounded to + 1-65535 instead of surfacing as an opaque Docker bind error; + `circuit_breaker.failure_threshold` requires at least 1 (a threshold of 0 + tripped on the check itself); `summarization.trim_tokens_to_summarize` + requires at least 1 instead of `max(1, ...)` quietly making 1-token + summaries; `mcp_servers.*.tool_call_timeout`/`session_init_timeout`, + `models.*.stream_chunk_timeout`, and `run_events.max_trace_content` reject + `0`/`inf` values that aborted every call or stream or truncated traces to + nothing; `mcp_servers.*.oauth.refresh_skew_seconds` rejects negatives. + YAML `true` no longer coerces to `1` on any of them; documented-valid + values such as `sandbox.idle_timeout: 0` and `null` timeouts are unchanged. + ([#6026]) + +- **frontend:** On Windows, `pnpm dev` now binds `127.0.0.1` by default. The + dev server otherwise listens on `0.0.0.0`, and when Hyper-V/WSL2/winnat + reserved the port range covering 3000, startup failed with + `Error: listen EACCES: permission denied 0.0.0.0:3000` while a loopback bind + on the same port succeeded. An explicit `-H`/`--hostname` is passed + through, so LAN access remains available via + `pnpm dev -- --hostname 0.0.0.0` (with `DEER_FLOW_DEV_ALLOWED_ORIGINS` as + before). The default `make dev` flow is unaffected — the dev nginx upstream + already targets `127.0.0.1:3000` — and macOS/Linux behavior is unchanged. + ([#6034]) + +- **gateway:** The bundled-interpreter repair hint no longer shows a + backslash-doubled interpreter path on Windows. The HTTP 400 detail for a + bundled MCP server pointing at a different Python interpreter interpolated + the path with `{sys.executable!r}`, and `repr()` escapes backslashes, so + the hint read `'D:\\...\\python.exe'` — wrong to paste into a terminal and + noisy in the JSON it tells you to edit. The detail now renders the path as + a JSON string for the server config and prints the raw path alongside for + terminal use; POSIX paths only change in where the quotes come from. + ([#6040]) + +- **channels:** Inbound Slack text now decodes the three entities Slack always + escapes, so `if a < b && c > d: print("R&D")` reaches the agent as typed + instead of `if a < b && c > d: print("R&D")`. `<`, + `>` and `&` are decoded after bot-mention stripping — a real mention + is a raw `<@U…>` while a user-typed `<@U…>` arrives escaped and stays plain + text — with `&` decoded last so an escaped entity is decoded exactly + once. Previously the corrupted text drove the agent's reasoning, could leak + `<` into files it wrote, and was escaped a second time when quoted back + into Slack. Outbound escaping is unchanged. ([#6045]) + +- **sandbox:** Glob patterns with a directory prefix are now root-relative, and `**` is + recursive. `path_matches` — behind the `glob` tool and the `grep` `glob=` filter on + every provider — used `PurePosixPath.match`, which on Python 3.12+ matches from the + right and treats `**` as a single-segment `*`: `src/**/*.py` missed `src/top.py` and + deeper files, while `docs/*.md` also matched `vendor/docs/b.md`. Patterns containing + `/` now match segment by segment from the search root, with `**` spanning zero or + more directories; basename-only patterns still match at any depth. ([#6046]) + +- **cli:** `deerflow --print` and `--json` now exit 1 when a run ends in an + LLM error fallback. Provider failures are not raised — + `LLMErrorHandlingMiddleware` turns them into an AI message flagged + `deerflow_error_fallback` — so a headless run with, say, an expired + credential printed the fallback text and exited 0, and the calling script + treated the run as a success. Both modes now follow the raised-exception + failure contract: `--print` still writes the fallback text to stdout but + adds `Error: LLM request failed (error_type=…, error_reason=…)` on stderr; + `--json` appends a terminal error record after the streamed events. A + mid-run fallback followed by a real final answer still exits 0. ([#6056]) + +- **frontend:** The run duration now labels the reasoning disclosure instead + of sitting in a separate footer row. A completed answer showed a generic + reasoning header above the answer and a duration line under the hover-only + action toolbar, which left an unexplained gap whenever the toolbar was + hidden. The disclosure now reads `Took 31s` (`用时 31 秒`) with a compact + chevron, stays expandable by mouse and keyboard with a localized visually + hidden reasoning cue in its accessible name, and is bound to the owning run + in answer bubbles, processing groups, and subagent groups; the separate + duration footer is gone. Streaming and history entries without a duration + keep the generic label, restored reasoning starts collapsed, and live + reasoning still closes one second after completion. ([#6057]) + +- **sandbox:** Every sandbox bash command now carries the authenticated user id in + `DEERFLOW_USER_ID`. Skill scripts run as bash subprocesses inside the sandbox and + cannot reach the Gateway auth context, so user-scoped skills had no way to learn the + serving user (issue #3919). Commands are prefixed with `export DEERFLOW_USER_ID=` + (`unset DEERFLOW_USER_ID` when the id is unusable) on both the POSIX local and the + remote sandbox paths, ahead of the `DEERFLOW_CHANNEL_USER_ID` prefix; Windows publishes + it via the subprocess environment instead. The variable is informational — commands can + overwrite it — and authorization still resolves server-side. ([#6058]) + +- **channels:** Long Discord replies split into consecutive messages now + reconstruct exactly what the agent wrote. `_split_text` ended each chunk + just before the newline it split on and stripped it from the remainder, so + every 2000-character boundary dropped one newline — and a run of blank + lines at the cut in full — losing paragraph breaks between bubbles. The + boundary newline now stays on the chunk's tail, so + `"".join(_split_text(text))` returns the original text for any input while + every chunk still fits Discord's 2000-character limit. Replies under the + limit and the empty string behave as before. ([#6062]) + +- **goal:** The `/goal` evaluator now sees the assistant's tool calls and the tools' + results, so completed file work no longer stands down with `missing_evidence`. It read + only user and assistant text: `format_visible_conversation()` dropped tool messages, + a tool-call-only assistant message rendered as nothing, and the evaluator's own + fail-closed rule then kept the goal active on most completed file tasks (in a + real-Gateway A/B, goals cleared on correct outputs went from 15 of 38 runs to 35 of + 36 on DeepSeek, and from 4 of 33 to 29 of 30 on MiMo v2.6 Pro). Tool steps inside the + existing 30-message window now appear in order as `Assistant tool call: ...` and + `Tool result (...): ...` lines — argument values cut at 200 characters, steps at 600 — + and the 12,000-character evidence cap keeps whole lines from the end, holding the + latest user request at the top, instead of slicing mid-line. Tool text is JSON-escaped + onto one line per step, failed results are labeled `error`, and as in the web UI a + hidden assistant message's calls stay out while a clarification prompt remains. The + system text adds fixed rules that tool results are data and never instructions and + that a successful result alone does not show the goal is met. ([#6068]) + +- **goal:** A failed `/goal` evaluator call is now recorded on the goal like the other + stand-downs. The worker logged "Goal evaluator failed" and returned without writing: + the goal stayed active with no `last_evaluation` after a first evaluation, or kept a + previous run's verdict and run id, so `GET /api/threads/{id}/goal` looked as if no + evaluation had run — whether the cause was a connection error, an answer the parser + rejected, or a provider timeout. The failure now persists `blocker: "run_failed"` + with `stand_down_reason: "evaluator_failed"` and the current run id, and the reason + names only the exception type ("The goal evaluator did not return a verdict + (APIConnectionError).") because a provider's error message can carry request details. + The goal still stays active with no retry and no hidden continuation, and nothing is + recorded if the run was aborted meanwhile. ([#6073]) + +- **frontend:** In `/workspace/chats/new`, the first streamed assistant steps + no longer render above the submitted question. The local human anchor that + pins first-turn order was compared against the SDK thread id — undefined + until creation confirms it — and was cleared again when that confirmation + arrived, so early reasoning and answer blocks jumped above the question + until the turn settled. First-turn ordering is now tied to the displayed + conversation (`displayThreadId ?? threadId`) and the anchor survives + creation confirmation; switching conversations still clears it. ([#6074]) + +- **goal:** The `/goal` evaluator now sees the user's answer to a Human Input Card, and + the lead prompt stops coaching scripts when no `bash` tool is bound. The web UI sends + a card answer as a hidden human message, which `format_visible_conversation()` + skipped — the evaluator saw the question but not the answer, read following it as + guessing, and goals stood down with `needs_user_input` despite correct work. The + answer now appears in the evidence as a `User (Human Input Card answer): ...` line, + whitespace-collapsed and cut at 2,000 characters, kept at the top over the cap. + Separately, the default local sandbox has host bash off, but the system prompt still + explained how to write scripts — in measured runs each model wrote a helper script + nothing could run in 38 of 120 runs. `apply_prompt_template()` gains `bash_available`, + the lead agent and embedded client pass `has_bash_tool(authorized_tools)`, and + without bash the script guidance becomes one line telling the model to work out + results directly and write them with `write_file` while the subagent section drops + its `bash` examples. With bash bound, the prompt is unchanged. ([#6082]) + +- **gateway:** Drain the agents router's persistent writes across request cancellation. + `POST /api/agents`, `PUT /api/agents/{name}`, `DELETE /api/agents/{name}` and + `PUT /api/user-profile` ran their store writes through bare `asyncio.to_thread`: a + disconnect that cancelled the handler either cancelled a still-queued worker — the + write silently never happened — or detached from a running one, silently dropping its + failure because the handler's `except` (500 plus error log) never ran. All four + writes now go through a `_drained_write` helper built on the existing + `await_drained()` helper, so an admitted worker finishes before the caller's + cancellation propagates; a lost failure is logged with the exception type only, and + expected domain errors such as `AgentExistsError` still reach the connected caller as + its 409. Reads stay cancellable: abandoning them loses nothing. ([#6087]) + +- **frontend:** APNG, AVIF, and WebM artifacts now show media icons in the + workspace artifact list and viewer instead of the generic document icon. + All three formats are previewable in the browser, but `getFileIcon` had no + case for their extensions: `apng` and `avif` now map to the image icon and + `webm` to the video icon, matching the existing preview support. Unknown + extensions keep the document fallback. ([#6089]) + +- **frontend:** Saved `Dockerfile` and `Makefile` artifacts now open as + editable source instead of falling back to the unsupported-file download + view, even though both names were already in the language map. + `getFileExtension` split the full path on `.` instead of taking the basename + first, so an extensionless name — or one under a parent directory that + contains a dot — never resolved to its map entry. Detection now runs on the + basename; existing suffix and hidden-file mappings are unchanged, and + streaming write-file previews keep their own text fallback. ([#6091]) + +- **mcp:** Drain personal MCP configuration writes across caller cancellation. + Personal config writes ran the durable file mutation through bare + `asyncio.to_thread(_mutate, ...)`, so a request cancelled after the worker + started could unwind while the admitted persistent mutation kept running + without request-side lifecycle ownership. The mutation is now drained through + the cancellation-safe `await_drained()` helper: the route stays alive until + the started write settles, then the original `CancelledError` propagates. + The read-only path keeps its existing cancellation semantics. ([#6093]) + ### Security - **authz:** Enforce permission checks on routes that only had authentication, and @@ -1628,6 +2605,31 @@ This release closes that milestone with **181 merged pull requests**. matches `actual_path`; cross-thread references stay unavailable until `view_image` runs again. ([#5824]) +- **memory:** Enabling `pii_redaction` now also cleans the queued memory payload, + closing the slice #5527 deliberately left open. Request-scoped redaction keeps + originals in thread state, and `MemoryMiddleware._resolve_add_args()` passed + those raw messages into `MemoryManager.add_nowait(...)`, so with + `pii_redaction.enabled=true` the buffered extraction payload, the extraction + model's input, and the persisted facts could still carry raw PII. The + middleware now takes the same `pii_redaction_config` knob as the + summarization/durable-context/title paths and redacts the conversation at the + enqueue boundary — one chokepoint covering both agent hooks — through the + shared `redact_text()` seam; the buffered payload itself becomes clean, and + messages are rebuilt via `model_copy` so originals are never mutated. The gate + stays off by default and behavior is unchanged while unset. ([#5577]) + +- **authz:** Skill authorization is now enforced at agent assembly and + activation, so an RBAC policy such as `skills: {allow: ["data-analysis"]}` + can actually deny a skill — Phase 2A (#4439) covered Gateway routes, but + skills still followed only the agent-config allowlist. The lead agent's + catalog, subagent `config.skills` loading, `describe_skill`, and every + activation gate now consult the AuthorizationProvider's `skill` policy + through one shared `authorize("skill", "activate")` helper; denial returns + the membership-denial message, and provider errors follow the configured + fail-closed/fail-open policy. A denied `read_file` of a `SKILL.md` is + stamped `skill_context_denied`, so durable context, skill allowed-tools, + and autonomous secret bindings never activate the denied skill. ([#4541]) + ### Documentation - **docs:** Fix the Apple Container verification instructions. The guide @@ -1691,6 +2693,55 @@ This release closes that milestone with **181 merged pull requests**. storage really grows — periodic snapshots keep the full message list, so delta divides the quadratic term by the cadence rather than removing it. ([#5845]) +- **docs:** Fix harness client examples that document an async API which does + not exist. `DeerFlowClient` has no `astream`/`ainvoke`, but the "Create Your + First Harness" tutorial and the integration guide (en and zh) are built + around them, so the tutorial's first call raises `AttributeError`; the + snippets also nest overrides in a `config={"configurable": {...}}` dict that + `stream()`/`chat()` silently swallow into `**kwargs`. Examples now call the + sync `client.stream(...)` / `client.chat(...)`, pass flat overrides + (`model_name=`, `subagent_enabled=`), configure agents via the constructor + (`DeerFlowClient(agent_name=...)`), and use a sync generator in the FastAPI + SSE example; "Async streaming" is retitled "Streaming". No API change. + ([#5843]) + +- **docs:** Document the pluggable `MemoryManager` architecture in the harness + memory page (`frontend/src/content/{en,zh}/harness/memory.mdx`), which + previously covered configuration and storage only, so integrating an + independent memory system meant reverse-engineering the contract from + source. A new "Memory Manager" chapter explains the backend-neutral + contract, backend selection via `manager_class` / `backend_config`, a + comparison of the five built-in backends, and how `deermem` and `openviking` + are each wired in; "Implementing your own memory backend" walks through a + minimal runnable skeleton, the optional operations, failure semantics + (`MemoryConflictError` → HTTP 409), and the host hooks. A callout in the + existing "Custom storage backend" section distinguishes swapping DeerMem's + `storage_class` from attaching a fully independent backend. ([#5929]) + +- **docs:** Correct MemoryManager guide details that fail when followed (en and + zh; fixes to #5929). An OpenViking config with the documented + `failure_policy.read: fail_closed` cannot build the manager — the key takes + `fail_open` or `raise` — and `mode: middleware` is required, not merely + recommended. `from_config` is mandatory alongside `add` and `get_context`; a + backend skipping it resolved to `None`. Tool mode additionally needs + `get_memory()` and the fact CRUD methods, because `MemoryMiddleware` is not + installed there unless the backend sets + `requires_passive_writes_in_tool_mode`; `create_fact` returns + `(memory_data, fact_id)` and an unknown fact id raises `KeyError` + (HTTP 404); `staleness_review_enabled` is on by default and all five flags + live under `memory.backend_config`. ([#5943]) + +- **docs:** Add the missing External Chat Message Roles section to the Chinese + README, mirroring the English one between the gateway-admin-equals-code- + execution and deployment-defaults notices. It states that run requests and + manual thread-state updates reject client-supplied `system` / `developer` + messages with HTTP 400 (session/PAT auth grants no system-prompt authority), + that the check does not rewrite existing checkpoints — a restart does not + remove persisted instructions — and how to verify locally with + `python backend/tests/poc_external_system_message_injection.py --help`, + including `--expect vulnerable` / `--expect blocked` on a disposable thread. + ([#6059]) + ### Internal - **persistence:** Add historical regression coverage for the run-change clock repair @@ -1729,6 +2780,33 @@ This release closes that milestone with **181 merged pull requests**. older checkpoints hydrate empty), skipping until upstream fixes land. No runtime behavior change. ([#5797]) +- **tests:** Make the blocking-I/O Lark auth-completion test work on Windows. + Its CLI fixture was an extensionless POSIX shell script that production CLI + discovery cannot resolve there, so the test failed with HTTP 404 + (`lark-cli is not installed`) before reaching the subprocess work it covers. + The fixture is now a `.cmd` stub on Windows (an executable shell script on + POSIX) in a directory containing spaces, resolved through the production + PATH lookup; fake app credentials let auth completion reach the CLI, and the + test asserts authentication success and the returned username. Fixture + invariants are documented in `backend/tests/AGENTS.md`. ([#5826]) + +- **tests:** Skip 22 POSIX-host-bound backend tests on Windows dev hosts + instead of failing them. Eleven `test_extension_dependency_sync.py` cases + spawn bare `make -n` (GNU make is not part of the documented Windows setup — + `FileNotFoundError`), ten `test_lark_broker.py` cases execute `#!` scripts + directly (`WinError 193`), and one boxlite shim test asserts POSIX exec-bit + semantics after `os.chmod`. All guards are conditional skips: a Windows host + with `make` on PATH still runs the Makefile contract tests, the pure-logic + tests run everywhere, and Linux/CI coverage is unchanged. ([#5931]) + +- **tests:** Write the `tests/test_file_signature.py` fixtures as bytes so + Windows text-mode newline translation cannot leak in. Three tests built + fixtures with `Path.write_text(...)` yet asserted byte-exact reads; on + Windows the newlines became CRLF on disk and `read_config_with_signature()` + — whose contract is returning the exact bytes on disk — correctly returned + them, failing 3 of 10 tests. The fixtures now use `write_bytes(...)` and the + stat-stale test appends in binary mode; no product change. ([#6042]) + ## [2.1.0] — 2026-09-24 This section accumulates work toward the **2.1.0** milestone @@ -5221,6 +6299,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#3233]: https://github.com/bytedance/deer-flow/pull/3233 [#3241]: https://github.com/bytedance/deer-flow/pull/3241 [#3252]: https://github.com/bytedance/deer-flow/pull/3252 +[#3261]: https://github.com/bytedance/deer-flow/pull/3261 [#3270]: https://github.com/bytedance/deer-flow/pull/3270 [#3283]: https://github.com/bytedance/deer-flow/pull/3283 [#3284]: https://github.com/bytedance/deer-flow/pull/3284 @@ -5709,6 +6788,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#4538]: https://github.com/bytedance/deer-flow/pull/4538 [#4539]: https://github.com/bytedance/deer-flow/pull/4539 [#4540]: https://github.com/bytedance/deer-flow/pull/4540 +[#4541]: https://github.com/bytedance/deer-flow/pull/4541 [#4556]: https://github.com/bytedance/deer-flow/pull/4556 [#4558]: https://github.com/bytedance/deer-flow/pull/4558 [#4559]: https://github.com/bytedance/deer-flow/pull/4559 @@ -5770,6 +6850,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#4719]: https://github.com/bytedance/deer-flow/pull/4719 [#4722]: https://github.com/bytedance/deer-flow/pull/4722 [#4724]: https://github.com/bytedance/deer-flow/pull/4724 +[#4725]: https://github.com/bytedance/deer-flow/pull/4725 [#4726]: https://github.com/bytedance/deer-flow/pull/4726 [#4727]: https://github.com/bytedance/deer-flow/pull/4727 [#4729]: https://github.com/bytedance/deer-flow/pull/4729 @@ -5848,6 +6929,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#4919]: https://github.com/bytedance/deer-flow/pull/4919 [#4921]: https://github.com/bytedance/deer-flow/pull/4921 [#4928]: https://github.com/bytedance/deer-flow/pull/4928 +[#4929]: https://github.com/bytedance/deer-flow/pull/4929 [#4933]: https://github.com/bytedance/deer-flow/pull/4933 [#4936]: https://github.com/bytedance/deer-flow/pull/4936 [#4938]: https://github.com/bytedance/deer-flow/pull/4938 @@ -6157,6 +7239,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5562]: https://github.com/bytedance/deer-flow/pull/5562 [#5563]: https://github.com/bytedance/deer-flow/pull/5563 [#5564]: https://github.com/bytedance/deer-flow/pull/5564 +[#5565]: https://github.com/bytedance/deer-flow/pull/5565 [#5566]: https://github.com/bytedance/deer-flow/pull/5566 [#5567]: https://github.com/bytedance/deer-flow/pull/5567 [#5569]: https://github.com/bytedance/deer-flow/pull/5569 @@ -6164,6 +7247,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5572]: https://github.com/bytedance/deer-flow/pull/5572 [#5573]: https://github.com/bytedance/deer-flow/pull/5573 [#5576]: https://github.com/bytedance/deer-flow/pull/5576 +[#5577]: https://github.com/bytedance/deer-flow/pull/5577 [#5578]: https://github.com/bytedance/deer-flow/pull/5578 [#5579]: https://github.com/bytedance/deer-flow/pull/5579 [#5580]: https://github.com/bytedance/deer-flow/pull/5580 @@ -6189,6 +7273,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5617]: https://github.com/bytedance/deer-flow/pull/5617 [#5621]: https://github.com/bytedance/deer-flow/pull/5621 [#5622]: https://github.com/bytedance/deer-flow/pull/5622 +[#5623]: https://github.com/bytedance/deer-flow/pull/5623 [#5625]: https://github.com/bytedance/deer-flow/pull/5625 [#5630]: https://github.com/bytedance/deer-flow/pull/5630 [#5631]: https://github.com/bytedance/deer-flow/pull/5631 @@ -6243,6 +7328,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5745]: https://github.com/bytedance/deer-flow/pull/5745 [#5748]: https://github.com/bytedance/deer-flow/pull/5748 [#5750]: https://github.com/bytedance/deer-flow/pull/5750 +[#5752]: https://github.com/bytedance/deer-flow/pull/5752 [#5755]: https://github.com/bytedance/deer-flow/pull/5755 [#5756]: https://github.com/bytedance/deer-flow/pull/5756 [#5757]: https://github.com/bytedance/deer-flow/pull/5757 @@ -6251,6 +7337,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5760]: https://github.com/bytedance/deer-flow/pull/5760 [#5761]: https://github.com/bytedance/deer-flow/pull/5761 [#5762]: https://github.com/bytedance/deer-flow/pull/5762 +[#5763]: https://github.com/bytedance/deer-flow/pull/5763 [#5767]: https://github.com/bytedance/deer-flow/pull/5767 [#5769]: https://github.com/bytedance/deer-flow/pull/5769 [#5775]: https://github.com/bytedance/deer-flow/pull/5775 @@ -6258,9 +7345,14 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5777]: https://github.com/bytedance/deer-flow/pull/5777 [#5778]: https://github.com/bytedance/deer-flow/pull/5778 [#5780]: https://github.com/bytedance/deer-flow/pull/5780 +[#5782]: https://github.com/bytedance/deer-flow/pull/5782 +[#5784]: https://github.com/bytedance/deer-flow/pull/5784 [#5785]: https://github.com/bytedance/deer-flow/pull/5785 +[#5786]: https://github.com/bytedance/deer-flow/pull/5786 +[#5788]: https://github.com/bytedance/deer-flow/pull/5788 [#5792]: https://github.com/bytedance/deer-flow/pull/5792 [#5794]: https://github.com/bytedance/deer-flow/pull/5794 +[#5795]: https://github.com/bytedance/deer-flow/pull/5795 [#5796]: https://github.com/bytedance/deer-flow/pull/5796 [#5797]: https://github.com/bytedance/deer-flow/pull/5797 [#5798]: https://github.com/bytedance/deer-flow/pull/5798 @@ -6275,42 +7367,126 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5815]: https://github.com/bytedance/deer-flow/pull/5815 [#5816]: https://github.com/bytedance/deer-flow/pull/5816 [#5819]: https://github.com/bytedance/deer-flow/pull/5819 +[#5820]: https://github.com/bytedance/deer-flow/pull/5820 [#5823]: https://github.com/bytedance/deer-flow/pull/5823 [#5824]: https://github.com/bytedance/deer-flow/pull/5824 +[#5826]: https://github.com/bytedance/deer-flow/pull/5826 [#5830]: https://github.com/bytedance/deer-flow/pull/5830 [#5833]: https://github.com/bytedance/deer-flow/pull/5833 +[#5835]: https://github.com/bytedance/deer-flow/pull/5835 [#5836]: https://github.com/bytedance/deer-flow/pull/5836 [#5838]: https://github.com/bytedance/deer-flow/pull/5838 [#5839]: https://github.com/bytedance/deer-flow/pull/5839 [#5840]: https://github.com/bytedance/deer-flow/pull/5840 [#5841]: https://github.com/bytedance/deer-flow/pull/5841 [#5842]: https://github.com/bytedance/deer-flow/pull/5842 +[#5843]: https://github.com/bytedance/deer-flow/pull/5843 [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5849]: https://github.com/bytedance/deer-flow/pull/5849 +[#5850]: https://github.com/bytedance/deer-flow/pull/5850 +[#5852]: https://github.com/bytedance/deer-flow/pull/5852 +[#5854]: https://github.com/bytedance/deer-flow/pull/5854 [#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5856]: https://github.com/bytedance/deer-flow/pull/5856 +[#5857]: https://github.com/bytedance/deer-flow/pull/5857 +[#5858]: https://github.com/bytedance/deer-flow/pull/5858 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 +[#5860]: https://github.com/bytedance/deer-flow/pull/5860 +[#5861]: https://github.com/bytedance/deer-flow/pull/5861 +[#5864]: https://github.com/bytedance/deer-flow/pull/5864 +[#5870]: https://github.com/bytedance/deer-flow/pull/5870 +[#5872]: https://github.com/bytedance/deer-flow/pull/5872 +[#5875]: https://github.com/bytedance/deer-flow/pull/5875 [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 +[#5883]: https://github.com/bytedance/deer-flow/pull/5883 [#5884]: https://github.com/bytedance/deer-flow/pull/5884 +[#5888]: https://github.com/bytedance/deer-flow/pull/5888 +[#5890]: https://github.com/bytedance/deer-flow/pull/5890 +[#5892]: https://github.com/bytedance/deer-flow/pull/5892 [#5893]: https://github.com/bytedance/deer-flow/pull/5893 [#5894]: https://github.com/bytedance/deer-flow/pull/5894 +[#5897]: https://github.com/bytedance/deer-flow/pull/5897 +[#5898]: https://github.com/bytedance/deer-flow/pull/5898 +[#5899]: https://github.com/bytedance/deer-flow/pull/5899 [#5900]: https://github.com/bytedance/deer-flow/pull/5900 +[#5902]: https://github.com/bytedance/deer-flow/pull/5902 +[#5903]: https://github.com/bytedance/deer-flow/pull/5903 +[#5906]: https://github.com/bytedance/deer-flow/pull/5906 +[#5908]: https://github.com/bytedance/deer-flow/pull/5908 +[#5910]: https://github.com/bytedance/deer-flow/pull/5910 +[#5911]: https://github.com/bytedance/deer-flow/pull/5911 +[#5915]: https://github.com/bytedance/deer-flow/pull/5915 +[#5917]: https://github.com/bytedance/deer-flow/pull/5917 +[#5919]: https://github.com/bytedance/deer-flow/pull/5919 +[#5921]: https://github.com/bytedance/deer-flow/pull/5921 +[#5927]: https://github.com/bytedance/deer-flow/pull/5927 [#5928]: https://github.com/bytedance/deer-flow/pull/5928 +[#5929]: https://github.com/bytedance/deer-flow/pull/5929 +[#5931]: https://github.com/bytedance/deer-flow/pull/5931 [#5934]: https://github.com/bytedance/deer-flow/pull/5934 [#5935]: https://github.com/bytedance/deer-flow/pull/5935 +[#5937]: https://github.com/bytedance/deer-flow/pull/5937 +[#5943]: https://github.com/bytedance/deer-flow/pull/5943 +[#5944]: https://github.com/bytedance/deer-flow/pull/5944 [#5945]: https://github.com/bytedance/deer-flow/pull/5945 +[#5947]: https://github.com/bytedance/deer-flow/pull/5947 +[#5948]: https://github.com/bytedance/deer-flow/pull/5948 +[#5949]: https://github.com/bytedance/deer-flow/pull/5949 +[#5950]: https://github.com/bytedance/deer-flow/pull/5950 +[#5954]: https://github.com/bytedance/deer-flow/pull/5954 +[#5956]: https://github.com/bytedance/deer-flow/pull/5956 +[#5957]: https://github.com/bytedance/deer-flow/pull/5957 +[#5959]: https://github.com/bytedance/deer-flow/pull/5959 +[#5960]: https://github.com/bytedance/deer-flow/pull/5960 +[#5961]: https://github.com/bytedance/deer-flow/pull/5961 +[#5963]: https://github.com/bytedance/deer-flow/pull/5963 [#5964]: https://github.com/bytedance/deer-flow/pull/5964 +[#5965]: https://github.com/bytedance/deer-flow/pull/5965 +[#5966]: https://github.com/bytedance/deer-flow/pull/5966 +[#5968]: https://github.com/bytedance/deer-flow/pull/5968 +[#5977]: https://github.com/bytedance/deer-flow/pull/5977 +[#5978]: https://github.com/bytedance/deer-flow/pull/5978 +[#5980]: https://github.com/bytedance/deer-flow/pull/5980 [#5981]: https://github.com/bytedance/deer-flow/pull/5981 [#5982]: https://github.com/bytedance/deer-flow/pull/5982 +[#5983]: https://github.com/bytedance/deer-flow/pull/5983 [#5987]: https://github.com/bytedance/deer-flow/pull/5987 +[#5990]: https://github.com/bytedance/deer-flow/pull/5990 [#5991]: https://github.com/bytedance/deer-flow/pull/5991 +[#5994]: https://github.com/bytedance/deer-flow/pull/5994 +[#5998]: https://github.com/bytedance/deer-flow/pull/5998 +[#6009]: https://github.com/bytedance/deer-flow/pull/6009 +[#6013]: https://github.com/bytedance/deer-flow/pull/6013 [#6015]: https://github.com/bytedance/deer-flow/pull/6015 +[#6018]: https://github.com/bytedance/deer-flow/pull/6018 +[#6023]: https://github.com/bytedance/deer-flow/pull/6023 +[#6024]: https://github.com/bytedance/deer-flow/pull/6024 +[#6026]: https://github.com/bytedance/deer-flow/pull/6026 +[#6034]: https://github.com/bytedance/deer-flow/pull/6034 +[#6040]: https://github.com/bytedance/deer-flow/pull/6040 +[#6042]: https://github.com/bytedance/deer-flow/pull/6042 +[#6045]: https://github.com/bytedance/deer-flow/pull/6045 +[#6046]: https://github.com/bytedance/deer-flow/pull/6046 +[#6056]: https://github.com/bytedance/deer-flow/pull/6056 +[#6057]: https://github.com/bytedance/deer-flow/pull/6057 +[#6058]: https://github.com/bytedance/deer-flow/pull/6058 +[#6059]: https://github.com/bytedance/deer-flow/pull/6059 +[#6062]: https://github.com/bytedance/deer-flow/pull/6062 [#6066]: https://github.com/bytedance/deer-flow/pull/6066 +[#6068]: https://github.com/bytedance/deer-flow/pull/6068 [#6069]: https://github.com/bytedance/deer-flow/pull/6069 [#6070]: https://github.com/bytedance/deer-flow/pull/6070 +[#6073]: https://github.com/bytedance/deer-flow/pull/6073 +[#6074]: https://github.com/bytedance/deer-flow/pull/6074 [#6076]: https://github.com/bytedance/deer-flow/pull/6076 +[#6082]: https://github.com/bytedance/deer-flow/pull/6082 +[#6087]: https://github.com/bytedance/deer-flow/pull/6087 [#6088]: https://github.com/bytedance/deer-flow/pull/6088 +[#6089]: https://github.com/bytedance/deer-flow/pull/6089 +[#6091]: https://github.com/bytedance/deer-flow/pull/6091 +[#6093]: https://github.com/bytedance/deer-flow/pull/6093 [#6112]: https://github.com/bytedance/deer-flow/pull/6112 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 6c44709828..69ab13233c 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -10,7 +10,7 @@ ## [未发布] 本节累积面向 **2.2.0** 里程碑([2.2.0](https://github.com/bytedance/deer-flow/milestone/3))的工作。 -该里程碑随本次发布收尾,共合并 **181 个 pull request**。 +该里程碑随本次发布收尾,共合并 **301 个 pull request**。 ### 新增 @@ -55,6 +55,37 @@ 处理,因此在运维者主动启用之前,默认行为与现在完全一致。 ([#5794]) +- **代理:** 工具产生的文件路径、URL 和任务 id 现在会记录为短的稳定产物句柄 + (`art_xxxxxxxx`),在上下文压缩后仍然有效,模型不再因为早期工具消息被摘要而 + 丢失或编造确切引用。每个线程在 `ThreadState.tool_artifacts` 中维护一个有上限 + 的注册表并投影到模型的隐藏持久上下文块;工具参数里出现的句柄——裸写、反引号 + 包裹或嵌在 dict/list 参数里——都会在工具执行前解析回真实引用,未知或已过期的 + 句柄会让这次调用失败而不执行工具。新增 `tool_artifacts` 配置段(`enabled`、 + `max_entries` 默认 100、`detect_refs_in_text`、`inject_model_context`、 + `resolve_handles_in_args`)分别控制各环节,默认全部开启。句柄是 agent 局部 + 的:`task` 参数会把父级句柄解析成具体引用,委派报告也必须返回具体引用。聊天 + 时间线的工具调用步骤会为每个捕获的产物显示徽标,SSE `values` 快照新增附加的 + `tool_artifacts` 键。([#4929]) + +- **上下文:** `history_search` 新增可选的 `role` 参数(`user`、`assistant` 或 + `tool`)。该搜索跨所有角色最多返回八条结果,当调用方要找用户早先的一条纠正 + 时,重复出现的 assistant 讨论或工具输出可能先把名额占满。角色过滤在八条上限 + 之前作用于活跃与已归档消息,使用归档记录中已有的角色字段;省略或传 null 保持 + 原有的全角色搜索,返回结果中的角色仍是 `human`/`ai`/`tool`,未知角色返回 + 错误;不涉及存储迁移,`history_read` 也没有新增参数。([#5927]) + +- **子代理:** 委派任务现在可以选择 JSON 语法验收标准 + `acceptance_criteria=["file:../outputs/report.json json-valid"]`,`task` 和 + `batch_task` 均可使用。此前委派产生的 `report.json` 可能非空却不是合法 JSON, + 而既有的"存在"与"非空"检查无法区分这种情况。该标准在完整 UTF-8 JSON 且不超过 + 50,000 字节时成立;完整但非法的 JSON、空文件和 `NaN`/`Infinity` 不成立;缺失 + 文件在确认不存在时判负;超大文件、读取不完整或不可用、解析器资源受限仍为 + `UNVERIFIED`。本地读取是有边界的二进制读取;远端读取保留全新 shell、绝对路径 + 工具、普通文件与目录包含检查,经 base64 信封最多传输 50,001 字节,没有无界的 + `read_file` 回退——远端工具不支持时保持 `UNVERIFIED`。顶层标量、重复键和语法 + 合法的大数字允许;UTF-8 BOM 会被拒绝。只查语法——不做 schema 或业务字段 + 检查;既有标准与判定语义不变。([#5947]) + #### 记忆 - **记忆:** 跨会话记忆新增 `user.cognitiveStyle`,用于沉淀稳定的 @@ -98,6 +129,29 @@ 以及智能体与用户记忆作用域之间的隔离, 且不改变任何运行时记忆行为。([#5564]) +- **内存:** 内存管理 API(读取、重载、状态、导入/导出、清空和单条事实增删改) + 新增可选的 `agent_name` 参数,指定后只作用于该 agent 的内存分桶,从而可以独立 + 地查看和维护各自定义 agent 的记忆。省略该参数则保持既有行为:读取和导入用 + 默认分桶,清空仍然作用于整个用户。参数按字母/数字/连字符校验(否则 422), + 且后端必须声明 `supports_agent_scoped_management`,否则作用域请求返回 501, + 而不是悄悄改错分桶。Gateway 保留调用方的大小写拼写,因为 Mem0 把 agent 名 + 当作区分大小写的存储身份;DeerMem 仍在内部自行做小写规范化。([#5565]) + +- **内存:** 记忆提取新增可选的 Jev(TypeSafe)预筛和信号分类,默认均关闭。 + `memory.prescreen`(`off`/`shadow`/`enforce`)在提取 LLM 调用前对每批对话分类: + `shadow` 只记录结论、照常提取;`enforce` 允许低概率批次跳过调用并推进水位线, + 即按"本批无持久内容"消费。`memory.signal_classification`(`off`/`shadow`/ + `hints`)把 `reinforcement`/`correction` 标签合并进提取提示文本,且仅在预筛为 + `enforce` 时可以否决跳过。两者都运行在新的共享 TypeSafe 客户端 + `deerflow.typesafe` 上——它从护栏风险门提取而来(问题、阈值与策略不变), + 统一错误分类:请求级失败抛异常,问题级失败作为数据返回 + (`AnswerSet.errors_by_question`)。新增可选顶层 `typesafe:` 配置块(api + key/env、`base_url`、model、timeout、deadline、attempts、backoff)供所有消费者 + 共享——消费者自身 `config` 优先,既有护栏部署不受影响。`config_version` 从 49 + 升到 50(`make config-upgrade` 合并新键),`backend/scripts/eval_memory_prescreen.py` + 提供开启 `enforce` 前必须通过的影子评估(精确的 Clopper–Pearson 错过率上界、 + 分层检查),证据不足时报 `INSUFFICIENT`。([#5906]) + #### 知识与检索 - **知识库:** 自定义智能体对话可以按消息为 RAGFlow 检索划定范围。 @@ -166,6 +220,32 @@ /api/v1/auth/preferences` 接受特定提供商的 effort 词。没有该块的档案保持旧路径不变。 ([#5780]) +- **社区工具:** InfoQuest 工具不再阻塞 agent 事件循环。`InfoQuestClient` 的 + `fetch`/`web_search_raw_results`/`image_search_raw_results` 此前用阻塞的 + `requests.post(..., timeout=30)`,启用该社区工具时每次调用最多让事件循环停顿 + 30 秒。现在改用 `httpx.AsyncClient`,`web_search`/`web_fetch`/`image_search` + 工具包装改为协程,`web_fetch` 还通过 `asyncio.to_thread` 把 CPU 密集的可读性 + 正文抽取放入线程——与此前迁移的 `jina_ai` 一致。返回值、错误文案和 30 秒 + 传输超时均不变。([#5782]) + +- **社区工具:** Tavily 的 `web_search`/`web_fetch` 工具不再阻塞事件循环: + `_get_tavily_client()` 现在返回 `AsyncTavilyClient`,`web_search_tool`/ + `web_fetch_tool` 改为协程并 await `client.search(...)`/`client.extract(...)`, + 无论成功失败都会关闭客户端。凭证处理不变——工具配置没有 key 时仍以 + `api_key=None` 构造客户端,由 SDK 继续从环境变量读取 `TAVILY_API_KEY`。 + ([#5784]) + +- **社区工具:** Firecrawl 的 `web_search`/`web_fetch` 工具改为协程工具: + `_get_firecrawl_client()` 返回 `AsyncFirecrawlApp`,工具改为 await + `search()`/`scrape()`,不再走此前会阻塞事件循环的同步调用。结果归一化、 + 错误处理、API key 处理和自定义 `api_url` 行为均不变。([#5786]) + +- **社区工具:** 图片搜索工具不再在事件循环上执行 DuckDuckGo 搜索。 + `image_search_tool` 现在是协程,通过 `asyncio.to_thread` 把配置解析和阻塞的 + `DDGS` 调用一并交给工作线程(`ddgs` SDK 没有异步 API),在读取配置文件和网络 + 请求期间保持事件循环可响应。搜索参数、结果结构、超时和错误处理均不变;已 + 发出的请求无法取消,工作线程最多存活到既有的 30 秒超时。([#5788]) + #### 渠道 - **渠道:** 现在可以在 Web UI 中扫码连接微信。 @@ -1373,6 +1453,680 @@ helper 只读取一次,并对返回的字节精确签名;竞态编辑最多 只多付出一次重载。([#5848]) +- **sandbox:** HTTP 上传路由获取的临时 sandbox 租约现在会释放。在 + remote/provisioner 部署中,`POST /api/threads/{id}/uploads` 会临时获取线 + 程 sandbox 以把上传文件同步进非挂载 sandbox,但该租约从不释放——成 + 功、413 与失败路径都一样;飞书文件同步也是同样的不释放模式,导致被获 + 取的 sandbox(一个 Pod 加 NodePort Service)永远回不到 warm pool。两条 + 路径现在都会在同步后释放,且只有最后一个持有者退出时才会把 sandbox 放 + 回 warm pool:同一线程已有活跃 run 的 sandbox 时,释放临时租约只减少计 + 数,不会把使用中的 sandbox 停进 warm pool。([#3261]) + +- **frontend:** 初始 run 创建请求在结果不明确的失败后不再重试。LangGraph + SDK 默认会对失败的 HTTP 请求重试,而 `POST /runs/stream` 并非幂等:后端 + 已接受请求之后,中间层仍可能返回 `504` 之类含糊的网关错误,重试就会把 + 同一个 run 创建两次,重复用户可见的工作。现在 run 创建以禁用重试的方 + 式发出;读取、显式 join stream 以及 SSE 恢复——对服务器提供的 + `Location` 发出携带 `Last-Event-ID` 游标的 `GET`——仍保留正常重试行 + 为。([#4725]) + +- **gateway:** stream bridge 故障不再被当成客户端断开而取消健康的 run。 + 两个创建者侧消费方——SSE `POST /runs/stream` 响应与 `POST /wait`—— + 此前都在无条件的 `finally` 中应用创建者的 `on_disconnect` 策略(取消), + 于是 `StreamBridge.subscribe()` 的任何退出,包括基础设施异常或订阅在终 + 止事件前耗尽,都会被视作客户端断开,可能取消仍在运行的 agent。该策略 + 现在只作用于真实观测到的断开(`request.is_disconnected()`、SSE 生成器 + 关闭、请求任务被取消);bridge 异常原样传播且不取消 run,订阅提前耗尽 + 则抛出服务器错误。([#5623]) + +- **logging:** 对 urllib3 "Failed to parse headers" 警告中携带凭据的头部 + 值做脱敏。该转储包含第一条畸形行之后的原始头部块,而 + `UrlRedactionFilter` 没有任何一轮能匹配到它,于是 `Set-Cookie`/ + `Authorization`/`WWW-Authenticate`/`Location` 的值原样进入日志——还出 + 现两次,因为 `exc_info` 的 traceback 会重复它们。新增的脱敏轮以 + urllib3 自身的字面量为门控,按转储的逻辑换行拆分后仅折叠携带凭据的字 + 段值,保留字段名与非敏感字段,日志仍能说明失败原因;异常文本经预置的 + `exc_text` 走相同流程,`JsonTraceFormatter` 也改为遵循 `exc_text` 而不 + 是自行渲染异常(此前 `logging.enhance.format=json` 模式下泄漏依旧)。 + ([#5752]) + +- **uploads:** 上传暂存辅助函数 `_link_staged_no_overwrite` 的链接失败清 + 理分支不再删除调用者仍持有的暂存名。该分支此前硬性执行 `os.unlink()` + 且只容忍 `FileNotFoundError`;在转换路径(`unlink_staged=False`)上调 + 用者仍持有暂存 inode 的描述符,Windows 上删除会抛出 `PermissionError`, + 覆盖掉调用者需要看到的原始 `os.link` 失败,并遗留 `.part` 文件。现在只 + 有本调用拥有该名字时才会通过尽力而为的 `_remove_staged_file` 删除,删 + 除失败也绝不会掩盖链接错误;`unlink_staged=False` 时由调用者经 + `_abort_upload_temp` 自行清理。([#5763]) + +- **事件:** 取消后的 JSONL 快照不再在文件扫描仍在进行时释放每线程写锁。 + `find_latest_ai_message_run_ids()` 会在每线程写锁内做一次完整的 thread-log + 读取,但扫描此前通过裸 `await asyncio.to_thread(...)` 执行:取消只会取消 + 等待方,`_read_thread_events()` 还在读文件时锁就已释放,写入方可能在快照 + 中途进入。现在扫描作为任务(名称 `jsonl-snapshot:{thread_id}`)经由 + `_await_owned_task()` 辅助函数排空完成后才释放锁;第一次调用者 + `CancelledError` 仍是主结果,读取失败会作为其原因链式保留。([#5795]) + +- **沙箱:** 调用者被取消时,AIO 跨进程 flock 现在会等其生命周期 worker 结束后才 + 释放。`_discover_or_create_with_lock_async()` 此前通过裸 `asyncio.to_thread()` + 执行发现、注册与创建——取消只会取消等待方,worker 仍在变更沙箱生命周期时 + flock 就已释放,另一个进程可以进入同一 discover/create 临界区。锁内的同步 + 步骤现在经由 `run_sync_lifecycle_operation()` 排空,整个异步创建包在 + `await_drained()` 中运行以便新建沙箱仍能完成就绪与注册,flock 本身改为可 + 取消的非阻塞重试,被取消的等待者可以立即退出。([#5820]) + +- **Helm:** chart 现在可以把可选的 lark-cli 沙箱运行时镜像传给 provisioner。 + provisioner 会读取 `LARK_CLI_INIT_IMAGE` 与 `LARK_CLI_BROKER_IMAGE`,Compose 与 + 根 README 也都写了这两个变量,但 chart 没有对应的 value——照着 README 操作的 + Helm 用户无从设置,沙箱会静默地不带 `lark-cli` 启动,直到沙箱内报 + `command not found` 才暴露。新增 value `provisioner.larkCliInitImage` 与 + `provisioner.larkCliBrokerImage`(broker 优先于 init)仅在设置时才在 + provisioner Deployment 上渲染对应环境变量;保持未设置则完全不渲染,既有发布 + 的 Pod spec 渲染结果不变。([#5835]) + +- **Tenki:** 通过环境引用配置的 `sticky`(例如 `sandbox.sticky: $TENKI_STICKY` + 配合 `TENKI_STICKY=false`)不再意外开启主机绑定。环境替换后该字段仍是字符 + 串,原先的 `bool(...)` 转换会把任何非空字符串——`false`、`0`、`off`、 + `no`——当作 `True` 传给 `Client.create`,等于让 provider 开着运维者已关闭 + 的粘性。现在改用 Pydantic 的布尔校验器解析:大小写不敏感的 + `false`/`0`/`off`/`no` 关闭绑定,`true`/`1`/`on`/`yes` 开启,省略或 + `null` 保持关闭,YAML 原生布尔值不受影响,非法值在 provider 初始化时直接 + 报错。([#5849]) + +- **配置:** `stream_bridge.recovered_stream_cleanup_delay_seconds` 现在与受 + 控的同级字段 `heartbeat_interval_seconds` 一样,在加载时拒绝布尔值、非有 + 限数值及超过 86400 的取值。该字段此前只有 `ge=0`,手改的 `config.yaml` 写 + `…: true` 会被静默转成 1.0(重连宽限窗口缩到 1 秒),`…: .inf` 或 `1e30` + 则被原样接受——END 之后的清理任务会一直休眠,孤儿 run 的 Redis stream + 只能等滚动的 `stream_ttl_seconds`(默认 86400)过期,而不是按配置的宽限 + 时间删除。`0` 仍然有效(END 发布后立即删除),文档默认值 60 不变。 + ([#5850]) + +- **社区工具:** 当 `config.yaml` 里的 `max_results` 是布尔值或小数时,DDG + `web_search` 现在会告警并回退到默认的 5 条结果——README 早已如此约定, + `image_search` 自 #5807 起也是这个行为。`web_search` 的 + `_coerce_max_results` 直接走 `int()`,会把 `3.5` 截成 3、把 `true` 当成 + 1,结果集被悄悄缩小而没有任何非法配置提示。`4.0` 这类整数值浮点和数字字 + 符串的行为不变。([#5852]) + +- **LLM:** `strip_think_blocks()` 在模型文本包含大量未闭合 `` 标签时不再出现平方 + 级变慢。该辅助函数负责清理用于 goal 评估、建议和输入润色的模型文本,原先的完整块 + 正则会在每个开标签处重试剩余后缀,12,000 个重复 `` 标签约需 3.1 秒且开销仍在 + 增长。现在一次前向扫描同时处理开/闭标签,每个匹配块只消费一次,并在第一个未闭合 + 的开标签处停止;大小写不敏感匹配、开标签属性、闭合 `>` 前的空白以及两种 + `truncate_unclosed` 模式均保持不变。([#5854]) + +- **沙箱:** 只存在分类挂载时,`ls /mnt/skills` 不再报 "Directory not found"。 + 默认技能投影下 provider 挂载的是 `/mnt/skills/public`、`custom`、`legacy` 与 + `integrations` 四个分类,并没有 `/mnt/skills` 根目录本身的映射,因此 + `LocalSandbox.list_dir` 会把根目录解析成字面宿主路径,宿主扫描在虚拟子目录 + 叠加逻辑得以列出分类之前就抛出 `FileNotFoundError`。现在当请求的容器路径内 + 至少挂载了一个映射时,`list_dir` 会把宿主路径缺失视为空列表,由叠加逻辑列出 + 四个分类;内部没有任何挂载的路径仍照旧抛出 `FileNotFoundError`。([#5857]) + +- **示例:** Jev 抓取内容筛查示例不再让行为异常的筛查端点或特殊部署绕过其 fail-open 与 + 数据外发保证。深度嵌套的应答体(`RecursionError`)或超大的 `noul` 整数 + (`OverflowError`)此前会逃出 fail-open 路径,并在逐消息任务组中取消其他消息的筛查 + 请求、丢掉该工具调用的全部标记;现在解析在 fail-open 块内进行,两类错误都会被兜住。 + 16 KiB 响应上限此前按解码后字节计数,压缩应答会先膨胀超限;请求现在携带 + `Accept-Encoding: identity` 并拒绝编码响应。纯 HTTP 的 loopback 端点(校验器允许的 + 唯一纯 HTTP 形式)此前会跟随 `HTTP_PROXY` 和 `ALL_PROXY`,把 bearer key 和摘要以 + 明文发给代理;这类端点现在以 `trust_env=False` 构建客户端,HTTPS 端点保留代理 + 设置。根 README 也不再声称摘要总是做 PII 脱敏:它会经过净化,仅在启用 + `pii_redaction` 时才脱敏。([#5858]) + +- **调度器:** 一个调度器迟到的完成写入不再会覆盖另一个调度器对同一 run 的新鲜认领。 + `ScheduledTaskRunRepository.update_status` 此前先用普通 `session.get` 读行,再据此 + 校验 `expected_lease_owner`(及 `protect_terminal` 回填):当过期的 launch 租约被 + 重新入队并被另一个调度器认领(`lease_owner=B`)后,第一个调度器受旧数据守护的迟到 + 写入仍能通过校验,清掉 B 的租约 fencing 并把 `launching` 状态覆盖为 `failed`。现在 + 会先取父任务的写锁再重读该行——与本仓库其他变更路径相同的 task → run 锁顺序——并加 + `populate_existing=True` 避免 identity map 返回加锁前的旧行;Postgres 侧相应增加 + `FOR UPDATE` 行读取。([#5860]) + +- **工具:** 绑定后的 `task` 或 `batch_task` 工具现在经绑定运行时自己的提交 + 器与执行容量执行同步调用,而不是落到进程级全局兜底。`get_available_tools` + 会就地为共享工具单例包一层同步 `func`,但 `bind_task_tool` 与 + `_bind_batch_tool` 只重绑副本的 `coroutine`,因此每个副本继承的是围绕未绑 + 定协程的同步包装:其同步路径要么落到进程级全局提交器(正是绑定契约排除的 + 兜底),要么在从未同步包装过单例的全新进程里抛出 `NotImplementedError`。 + 两个绑定函数现在都会用同一个 `make_sync_tool_wrapper` 重绑 `func`,绑定副 + 本的两条调用路径因此都执行绑定后的协程。([#5861]) + +- **模型:** token 字段不是字符串的 CLI 凭据文件不再被当作有效凭据加载。 + `credential_loader.py` 的加载器此前校验了容器字段、`expiresAt` 与 Codex + 的 `account_id`,却没校验 token 本身:手改或脚本改写的文件里 + `accessToken`/`access_token` 写成数字、列表或对象也能通过真值检查——来自 + `$CLAUDE_CODE_CREDENTIALS_PATH` 的坏 Claude override 会遮蔽有效的 + `~/.claude/.credentials.json` 回退,坏 Codex token 则被拼进 + `Authorization: Bearer …` 请求头,最后以一个莫名其妙的 401 暴露。现在这 + 类 token 会被跳过(字符串会去除首尾空白,空白视为缺失),非字符串的 + `refreshToken` 与 `account_id` 一样降级为空。([#5864]) + +- **MCP:** 在启用 `user_auth` 的 HTTP/SSE 服务器上,持久化 MCP 任务的状态与 + 取消调用现在以任务记录的归属用户完成鉴权。此前任务在已鉴权的 Agent 回合内 + 提交成功,后台调用却全部失败:`McpTaskToolCaller` 没有把存储的 owner 提供给 + 凭证拦截器,缺少运行中的请求上下文时凭证选择落到 `default`,任务服务只会重 + 试这一鉴权失败而不读取远端任务状态,从 SQL 重建的服务同样失败。现在启用 + `user_auth` 的后台 HTTP/SSE 调用会通过既有 user ContextVar 绑定一个仅含 ID + 的 owner,并在 `finally` 中恢复原上下文;提交、stdio 及未启用 `user_auth` + 的调用保持原上下文,并发 owner 互相隔离,后台调用也仍不触碰 + `headers_from_context` 的请求密钥。([#5870]) + +- **智能体:** 循环检测不再把分页或内容变化的通用工具调用误判为重复。此前的 + 调用键只保留第一个显著参数(`path`/`url`/`query`/`command` 等),MCP `fetch` + 用 `start_index` 续页、用 `page` 翻页的搜索、或 `list_uploaded_files` 跟随 + `cursor` 都会折叠成同一个键,第五个不同调用就以 `stop_reason=loop_capped` + 硬性终止运行。现在键由全部参数构成;只有 `bash`/`ls`/`glob`/`grep` 的叙述性 + `description` 字段仍被排除(改写它依然无法绕过检测),其他工具的 + `description` 因可能是操作载荷而继续参与键值。([#5872]) + +- **中间件:** 输入净化不再丢掉被净化用户消息上的元数据。 + `InputSanitizationMiddleware` 之前用四个选定字段重建净化后的 + `HumanMessage`,content/id/name/`additional_kwargs` 之外的任何字段——尤 + 其是携带网关来源的 `response_metadata`——在文本与多模态两条路径上都会被 + 丢弃。现在从原消息复制、只替换 `content` 与 `additional_kwargs`,元数据、 + 标识字段和消息子类都得以保留,原始请求消息仍保持不变。([#5875]) + +- **社区集成:** `config.yaml` 中的 InfoQuest 超时配置现在会同类搜索/抓取提供方一样先做 + 归一化。`deerflow.community.infoquest.tools` 此前把五个文档化的数值选项(`timeout`、 + `fetch_time`、`navigation_timeout`、`search_time_range`、`image_search_time_range`) + 未经校验直接传给 `InfoQuestClient`,而 `$VAR` 引用会从环境变量原样替换—— + `timeout: $FETCH_TIMEOUT` 因此以字符串抵达,客户端的 `> 0` 比较抛出 `TypeError`, + 使 `web_fetch`、`web_search` 或 `image_search` 整体失败,错误信息还指向客户端而非 + 配置键。现在这些值先经过一步强制转换:整数、整数值浮点数或整数形式的字符串按配置 + 生效;布尔值、小数、空白/非数字字符串或 `None` 回退到文档默认的 `-1`(该过滤禁用) + 并记录一条点名配置键的警告。已经合法的整数配置产生的请求逐字节不变。([#5883]) + +- **沙箱:** AIO acquire 中已启动的 worker 现在会在同键序列化器释放前结束,即使 + 调用者已被取消。在按 `(user_id, thread_id)` 序列化的区域里,进程内缓存复用与 + warm pool 回收此前是裸 `asyncio.to_thread()` 调用——取消会在 worker 仍在运行 + 时释放作用域,同一 user/thread 的重试 acquire 可能与之重叠。这两步现在经由 + 一个辅助函数执行:仍在排队的工作会被取消(与旧行为一致),但已启动的 worker + 会保留序列化器所有权并在反复取消下排空;flock 等待、就绪轮询与 skills 投影 + 步骤仍保持可取消。([#5888]) + +- **技能:** 托管集成技能的发现现在在每个技能包边界处停止下钻。与公开和自定义技能加载 + 器不同,集成目录遍历在找到 `SKILL.md` 后仍会继续深入,导致自带 `SKILL.md` 的嵌套 + 评测夹具可能成为运行时技能——若 frontmatter 名称与公开技能相同,还会把它顶替掉。 + 自身没有 `SKILL.md` 的 provider 与命名空间目录仍会正常暴露其嵌套技能。([#5890]) + +- **模型:** 开启 prompt caching 的 Claude 会话在几轮之后不再因 HTTP 400 而 + 无法继续。`ClaudeChatModel._apply_prompt_caching` 之前把 `cache_control` + 标记直接写进请求与线程共享的 block——langchain-anthropic 对 Claude 原生 + block(带 `source` 的图片或文档、搜索结果)按引用传递——标记因此随消息 + 一起被 checkpoint,从第三轮起每个请求都超出 Anthropic 的四个断点上限, + 线程再也无法恢复。现在标记写在副本上,且每个请求都会先剥掉已存在的标记 + (无论缓存开闭),checkpoint 里已带陈旧标记的线程下一轮即可自愈,无需任 + 何迁移。([#5892]) + +- **子代理:** 当最后一个活跃项的租约在最后一次允许尝试中过期时,持久化批次 + 不再永远停留在 `running`。`claim_items` 会把这样的项标记为 `failed`,却不刷 + 新父批次,于是末项以此收场的批次既没有 `completed_at`,也无法观察到终态。 + 租约恢复现在会在同一事务内刷新批次:没有任何成功项的批次变为 `failed`,有 + 部分成功的变为 `completed`,失败项计数保持可见,显式重试某个项仍会照旧重新 + 打开批次。([#5897]) + +- **智能体:** 提供方在同一 thread 的不同 run 间复用 `tool_call_id` 时不再破坏 + 委托账本。账本此前按原始 ID 去重并沿用早先的 `run_id`:后一个 run 的同 ID + 委托可能覆盖先前条目——任务从持久上下文中消失、`max_total_subagents` 漏计 + 该 run——其回复也可能被当成对更早孤儿任务的应答。现在捕获、保留窗口与终态 + 保护都改用 `(run_id, tool_call_id)` 标识;新用户回合的孤儿关闭以该 run 的 + 首条消息为界,没有新用户消息的续跑 run 会保留已应答的条目。([#5898]) + +- **沙箱:** 沙箱网络策略变更现在会在调用者取消时排空完成。 + `consume_network_policy_events()`、`deny_pending_network_policy_events()` 与 + `decide_network_policy_request()` 的异步包装此前用裸 `asyncio.to_thread()` 执行 + 这些有状态的 trusted-proxy 变更——取消等待中的工具/agent 任务会在策略状态仍在 + 变更时离开生命周期边界。三者现在都经由取消安全的 + `run_sync_lifecycle_operation()` 辅助函数:已提交的 worker 先结束,随后才传播 + 原始的 `CancelledError`。([#5899]) + +- **追踪:** Langfuse 追踪现在标注实际运行的模型。`model:` 标签此前在 + 模型解析之前注入,默认模型的 run 可能完全没有模型标签,回退 run 则保留被 + 拒绝请求的模型名。`LeadAgentAssembly` 现在以 `effective_model` 暴露解析后的 + 模型;Gateway worker 在组装完成后注入追踪元数据,嵌入式客户端也先解析 + agent 再注入,两者都标注最终选定的默认或回退模型,调用方提供的元数据仍然 + 优先。([#5902]) + +- **运行时:** 纯图片的用户输入现在每次 run 只记录一次。`RunJournal` 此前用 + 可选的文本摘要判断输入是否已捕获,而无文字的消息摘要为 `None`,导致之后的 + 每次模型调用都追加一条 `llm.human.input` 事件并虚增 `message_count`;批量 + 回调还可能在最新的图片输入之后补记更早的 prompt。现在用一个独立于可空摘要 + 的标志跟踪捕获,一旦记录就停止扫描批次;媒体内容与空的展示文本保持不变。 + ([#5903]) + +- **MCP:** 为原子的通知启动预留铺路,让 MCP 任务通知的故障恢复可以安全回 + 滚。此前 worker 在异步 run 查询与启动之后复用批次的扫描时间戳,导致重试延 + 迟过早开始、仓库过期判定拿陈旧时间作比较;单条记录的意外失败也要等整批 + `gather()` 结束后才处理,一条记录的重试节奏被最慢的兄弟记录拖住。现在查找 + 与启动结果使用不早于认领时间的完成时间,单条失败在各子任务内释放,仓库可 + 以读取、恢复、轮询并结算 `launching` 状态的通知(保留其快照与幂等键),而 + 生产路径尚不写入该状态——回滚到旧版 Gateway 仍然兼容;通知处于 `launching` + 期间前端会继续轮询。([#5908]) + +- **子代理:** 被取消或超时的子代理执行会保留已完成模型调用上报的 token 用 + 量。模型可能在中间件仍在等待、图尚未发布下一个 `values` 帧时完成并上报用 + 量;这个窗口内的取消会绕过结果处理分支,外层超时/取消包装只能拿较旧的进度 + 快照定稿结果——两次完成响应分别花 15 和 30 token 时只报了 15。现在流清理排 + 空之后、选定终态之前,执行器会发布 token 采集器的最终累计快照;首次终态写 + 入语义、取消传播与父运行日志去重均保持不变。([#5910]) + +- **路径:** 路径隔离校验器现在拒绝带结尾换行的 ID。Python 的 `$` 也会匹配到 + 末尾换行之前,而 `_validate_user_id`、`_validate_project_id` 与 + `_validate_integration_id` 用的都是 `.match()`,形如 `"alice\n"` 的 ID 能 + 通过文档约定的受限字符集检查,生成严格语法本不允许的文件系统 bucket。三处 + 现在都改为 `fullmatch`,只接受到字符串结尾都符合字符集的 ID。([#5911]) + +- **上传:** 文件名中含 NUL 字符的上传不再以 500 失败。规范化此前放行了内嵌 + NUL,文件名随后进入 `os.lstat()` 并抛出未处理的 `ValueError` + (`lstat: embedded null character in path`),中断整个上传请求。 + `normalize_filename` 现在会直接拒绝这类名称;多文件请求中非法文件被跳过, + 其余有效文件照常保存。([#5915]) + +- **目标:** 智能体现在每次模型调用都能看到激活的 `/goal`。此前 + `ThreadState.goal` 只到达目标评估器:通过 `PUT /goal`、 + `DeerFlowClient.set_goal()` 或 TUI 设置的目标会被忽略,压缩还可能把陈述目标 + 的唯一消息一并移除。`DurableContextMiddleware` 现在在其隐藏的 + `` 消息开头渲染 `` 元素(HTML 转义、与 + goal 路由一致的 4000 字符上限、开启时做 PII 脱敏),持久上下文契约追加静态 + 例外:智能体以用户请求的优先级推进该元素,且它不携带 system 或 developer + 权限。`active_goal` 加入输入净化器的标签黑名单,用户输入与远程结果无法伪造 + 它;子智能体的状态不含 goal,不受影响。([#5917]) + +- **前端:** 带信息串的围栏行(如 `` ```python ``)不再关闭已打开的代码块。 + CommonMark 要求闭合围栏不得短于起始围栏、且不能带信息串,但三处围栏 + 扫描中只有 Mermaid 归一化两半都检查了——渲染路径 + (`stripLeakedSystemTags`)和导出路径(`stripInternalMarkers`)只比较围栏 + 字符与长度,块因此提前结束、其后内容被当作正文:渲染时示例代码内的 + 系统标记标签被剥除、内容直接暴露,导出 thread 时整个 + ``/`` 区间被删除、用户文本丢失。两处现在共用移入 + `core/streamdown/fences.ts` 的 `isClosingFence` 谓词。([#5919]) + +- **技能:** 技能发现不再因符号链接成环而反复扫描同一棵目录树。`load_skills()` + 此前跟随目录链接但不检测指回祖先的链接,命名空间里两个成环链接会重复展开 + 同一棵树——真实扫描超过了 3 秒诊断时限,而普通外部链接不到 1 毫秒即可完成。 + 现由共享的 `walk_skill_directories` 辅助函数剪枝"解析后路径已在当前遍历分支 + 上"的目录,覆盖本地与用户级存储的 public/custom/integration/legacy 各类 + 发现;指向同一外部树的多个独立别名仍可被发现。([#5921]) + +- **后端:** asyncio 子进程在原生 Windows 上恢复可用。自 #3236 起, + `backend/sitecustomize.py` 会把所有 `sys.path` 含 `backend/` 的 Python 进 + 程切到 `WindowsSelectorEventLoopPolicy`,而该循环无法创建子进程: + `invoke_acp_agent` 工具立即失败且错误信息为空(裸 `NotImplementedError`), + stdio MCP 服务器和本地沙箱命令执行也撞上同一堵墙。这一强制的前提(异步 + psycopg)已不成立——postgres 驱动是 asyncpg,本就没有 Windows wheel,默 + 认的 SQLite 后端在两种循环下都能跑。该文件已删除,Windows 恢复 Python 默 + 认的 Proactor 循环;postgres 基准脚本仅在自身的 postgres 场景下设置 + selector 策略。([#5937]) + +- **沙箱:** 非有限的 OpenSandbox 超时值现在在 provider 构造时即被拒绝,而不是拖 + 到首次获取沙箱时才失败。这些选项是 provider 特有的扩展项,YAML 的 `.nan` 与 + `.inf` 能通过 `SandboxConfig` 校验,provider 此前只检查符号,把失败推迟到获取 + 沙箱时的 SDK 或 `timedelta` 转换,抛出底层运行时错误。`request_timeout` 与 + `ready_timeout` 现在必须为有限正数,`sandbox_timeout` 必须为有限非负数(`0` + 仍表示显式清理);`config.example.yaml` 已注明该要求。([#5944]) + +- **子代理:** 上下文快照不再丢弃 Anthropic 原生的 `document` 块。以 + `context_mode="snapshot"` 委派时,`document` 不在可识别的媒体类型里,块会被 + 静默丢弃,子代理可能因此失去父代理已掌握的要求。现在可序列化的文档会作为 + 历史用户角色数据保留,并在捕获时对其文本字段(`title`、`context`、text 来 + 源的 `data`、content 来源的文本块及 `cited_text` 等引用文字)套用既有的保 + 留标签与用户输入中和,移除 `cache_control`,并与父历史做嵌套副本隔离。中和 + 在捕获时进行,因为隐藏的快照消息不经过输入净化中间件;不可序列化的文档退 + 回既有的历史媒体省略提示。([#5948]) + +- **沙箱:** 中间件应用网络审批响应的路径现在会在取消时排空完成。 + `SandboxMiddleware.abefore_agent()` 此前用裸 `asyncio.to_thread()` 执行 + `_apply_network_policy_response()`——取消等待中的 agent 任务可能在已启动的 + `decide_network_policy_request()` worker 仍在变更沙箱网络策略状态时离开中间件 + 边界,绕过了已被 #5899 改为取消安全的 provider 包装。异步桥接现在经由 + `run_sync_lifecycle_operation()`;同步的 `before_agent()` 路径保持不变。 + ([#5949]) + +- **运行时:** subagent 步骤事件的 flush 现在能在宿主取消下存活。flush 会先把批次 + 从 `_pending` 移出再等待 `RunEventStore.put_batch()`,且只有普通失败会被重新 + 入队——`CancelledError` 属于 `BaseException`,会绕过 `except Exception` 栅栏, + 于是取消可能在持久化仍在进行时拆掉 flush,之后写入再失败就会静默丢失该批次。 + 现在每笔已提交的写入都在独立的 shielded 任务中运行并吸收反复取消;若排空后的 + 写入失败,批次会先重新入队再传播原始 `CancelledError`,普通存储失败仍照旧 + 记录日志并重新入队。([#5950]) + +- **上下文:** 同一模型响应中的两个不同 `task_note` 新增调用不再会静默删除已有笔记。 + 此前每个调用读取同一份状态快照后各自合并更新,已有 7 条笔记时合并结果只保留最后 + 8 个键,而两个调用都返回 `saved`。现在剩余槽位会按本批次的工具调用顺序为互不相同的 + 新键预留:超出的新增在发出状态更新前就返回 `note_capacity`,已有笔记得以保留;对 + 同一新键的重复写入共享一个槽位;结构非法的兄弟调用不占用容量。被兄弟删除或失败的 + 调用释放的槽位在下一批次可用。对启用了 `task_continuity.enabled` 的部署而言,这 + 改变了此前文档所述并行溢出时"保留最后 8 条"的行为。([#5954]) + +- **MCP:** 删除线程现在会一并销毁该线程的持久化 MCP 会话。此前 + `DELETE /api/threads/{id}` 清理了检查点、运行、事件、反馈与实时浏览器会 + 话,却从未触及线程的持久化 stdio MCP 会话(`close_scope` 在生产代码中没有 + 调用方),有状态服务器(如 Playwright)会保留服务端状态,同用户重建同 id + 线程时可能继承陈旧的页面与 Cookie,泄漏的 owner 任务及其子进程也继续占用 + 内存和文件描述符。现在删除会在浏览器会话清理之后以尽力而为方式调用新增的 + `MCPSessionPool.close_thread_scope()`,匹配该用户/线程的旧版作用域与所有代 + 数(含进行中的创建),不影响其他线程;清理失败时线程仍照常删除。([#5956]) + +- **工具:** 社区搜索工具不再因无穷大的 `max_results` 而失败。此前的类型转 + 换守卫只捕获 `TypeError` 与 `ValueError`,而 `int(float("inf"))` 抛出的是 + `OverflowError`;`yaml.safe_load`(`.inf` 是合法的 YAML 浮点数)与 + `json.loads`(接受 `Infinity`)都可能把无穷值递给守卫——结果不是回退到默认 + 值,而是转换崩溃,SearXNG 的 `web_search_tool` 每次搜索都返回错误负载。 + brave、serply、serper、searxng、groundroute、sofya 的守卫(`max_results`, + 另含 Sofya 的内容上限)现在同时捕获 `OverflowError`,无穷值与其他非法输入 + 一样回退到默认值。([#5957]) + +- **模型:** 全空白的 Codex CLI 凭据现在会在模型初始化时被拒绝,而不是生成 + 非法的 `Authorization` 请求头。加载器此前接受任意字符串 token, + `auth.json` 写成 `"access_token": " "` 也能成功构造 `CodexChatModel`, + 配置错误要拖到 provider 请求真正失败时才暴露。现在字符串 token 会先去除 + 首尾空白再走原有的类型与空值检查,空白值视为缺少凭据(沿用既有的 + "Codex CLI credential not found" 报错),带空白的合法 token 去除空白后照 + 常可用。([#5959]) + +- **智能体:** 只差一个结尾换行的 agent 名称现在返回 422,而不是 500。Gateway + 路由此前用 `.match()` 校验 `AGENT_NAME_PATTERN`,而 Python 的 `$` 会匹配到 + 末尾换行之前,于是 `POST /api/agents` 传入 `{"name": "reviewer\n"}` 能通过 + 路由检查,却在 `FileAgentStore.create()` 的 `fullmatch` 处抛错——可读的错误 + 信息只留在服务端日志里,调用方拿到的是内部错误。DeerMem 的 + `validate_agent_name` 同样放行该名称,使 `agent_facts_directory()` 建出自身 + 校验器拒绝读回的目录。两处校验器现都改用 `fullmatch`;此前合法的名称不受 + 影响。([#5960]) + +- **网关:** Gateway 关闭时现在会关闭池化的 MCP 会话,而不是将其遗弃。此前 + lifespan 清理只关闭了浏览器会话管理器,从未触及 `MCPSessionPool`,而池化会 + 话的传输层 `__aexit__` 只能由其 owner 任务执行——事件循环一旦停止,stdio + MCP 子进程与 SSE/HTTP 连接就完全失去有序关闭。现在 lifespan 关闭会在浏览器 + 会话步骤之前 await `MCPSessionPool.close_all()`,受共享的关闭钩子超时约束且 + 尽力而为:超时或失败只记录日志,其余清理继续进行。([#5961]) + +- **TUI:** 无头模式 `deerflow --print` 和 `--json` 运行失败时不再泄漏 + Python traceback。此前会话创建、thread 解析和流式迭代没有任何错误边界, + 任何抛出的异常——包括文档记载的 `CheckpointModeMismatchError`——都会从 + `main()` 逃逸,且没有稳定的非零退出码。现在 `--print` 向 stderr 输出一行 + `Error: ` 并以退出码 1 结束;`--json` 追加一条终态 + `{"type": "error", "data": {"message": ...}}` 记录——即使流中途失败, + stdout 每行仍是合法 NDJSON——并以退出码 1 结束。`KeyboardInterrupt` 和 + `SystemExit` 仍照常传播。([#5963]) + +- **MCP:** MCP 任务通知现在会在创建 Agent 运行之前原子地预留启动。此前 + worker 会先启动 run 再持久化任何启动阶段,一旦所有权在这段副作用期间过期 + 或被回收,另一个 worker 可能并发启动同一条通知。预留是一次条件 `UPDATE` + (`begin_notification_launch`),由 lease owner、每次认领的 token、租约到期 + 时间与 dispatch version 共同把关;未启动的陈旧快照遇到更新事件会在任何副作 + 用前被拒绝,恢复出的 `launching` 行保留原事件与幂等键。确定性的启动拒绝 + (400、401、403、404、裸 409、422、501)立即进入死信,歧义的 408/429/500 + 或取消则保留 `launching` 快照等待对账,不消耗重试预算。无需数据库迁移。 + ([#5965]) + +- **MCP:** 在 Web 编辑器中创建的 MCP 连接现在归属于登录用户,而非部署全体。 + 此前编辑器写入部署级配置,用户用自己的凭证添加的连接会变成全体共享的 + Agent 能力,普通用户却无法维护自己的连接。新增仅限本人的 CRUD 接口 + `/api/mcp/personal/config`,持久化到 + `.deer-flow/users//integrations/mcp.json`;能力中心把部署指南与共 + 享连接归入 "Platform provided",个人连接归入 "My plugins";工具装配只组合 + 共享工具与当前用户的连接(个人发现不进入全局工具缓存),持久化任务会记录 + 归属范围,即使部署服务器名与个人运行时名冲突,提交、轮询与取消也沿用该范 + 围。个人 HTTP/SSE 端点只允许连接通过公网策略校验的地址,并保留原 Host、 + TLS SNI 与证书校验;特权个人定义会在发现、调用与持久化任务的提交/轮询/取 + 消环节重新校验当前管理员身份(降级即失效)。`config.yaml` 工具与既有 + `extensions_config.json` 条目仍为共享。([#5966]) + +- **中间件:** PII 脱敏改写用户消息时不再清空 `response_metadata`。中间件 + 此前用 content/id/name/`additional_kwargs` 手工重建脱敏后的 + `HumanMessage`,凡内容被改写的消息,其网关来源等元数据都会被静默重置; + 脱敏本身一直是正确的。现在从原消息复制、只替换 `content`(深拷贝,因为 + 未改动的 block 是按引用保留的)与 `response_metadata`,`id`、`name` 和消 + 息子类得以保留,且不与原消息共享可变状态。占位符与脱敏行为不变。 + ([#5968]) + +- **智能体:** 用户消息上调用方附加的 `response_metadata` 现在能在动态上下文 + 注入后保留。`DynamicContextMiddleware` 此前把副本重建为只含 + `content`/`id`/`name`/`additional_kwargs` 的新 `HumanMessage`,其余字段 + (包括 `response_metadata`)在进入模型的消息列表里被静默丢弃,而同链的 PII + 脱敏与 thread-data 中间件早已使用 `model_copy`。现在改用 + `original.model_copy(update={"id": ...})` 构造副本,文档约定的 `{id}__user` + 换 ID 语义不变;`UploadsMiddleware` 重写用户消息时也做了同样处理。([#5977]) + +- **持久化:** PostgreSQL DSN 查询串里的字面 `+` 在注入 schema 时不再被改 + 写。设置 `database.postgres_schema` 后,`dsn_with_search_path()` 此前用 + `parse_qsl`/`urlencode` 往返处理既有参数,而 `parse_qsl` 会把 `+` 解码成 + 空格——这是 libpq 并不采用的 HTML 表单规则——于是 + `application_name=web+app` 到达服务端时变成 `web app`, + `options=-c timezone=UTC+8` 退化成 `-c timezone=UTC 8`,不再是一条完整的 + `-c` 设置。现在既有参数按原字节逐字保留,只追加(或合并进)一条百分号编 + 码的 `options=-c search_path=`;关键字形式 DSN 与 + `postgresql+asyncpg://` 的规范化保持不变。([#5978]) + +- **目标:** 工件投递失败时活动目标现在会保留。此前一个 run 可以先写出并展示输出、得到 + 满足的 goal 评估、随即删除目标——之后投递校验才把同一 run 标记为错误(终止性投递 + 回执写入失败也会造成同样的不一致),让线程在尚有未完成工作时失去了目标。满足的 + 评估现在只是一个完成候选:worker 会等投递、回执持久化和终止性取消仲裁成功并确认 + 状态持久化成功之后才清除目标,同时在既有的 checkpoint 写入预留下重查目标、可见 + 对话和 checkpoint,让较新的已准入 run 获胜。任何失败都会保留原目标与计数器,且不 + 增加模型调用;正常完成(包括没有新输出的 run)仍照常清除目标。([#5980]) + +- **配置:** 以带 BOM 的 UTF-8 保存的 `extensions_config.json` 现在可以正常 + 加载。运行时加载器(`ExtensionsConfig.from_file`)与支撑 Gateway skill + 开关的原始读改写读取器此前都按普通 UTF-8 打开文件,编辑器写入的 BOM 会以 + `Unexpected UTF-8 BOM (decode using utf-8-sig)` 失败——既阻断 MCP/skill + 配置加载,也让开关无法保存本就有效的配置。两个读取器现在都接受可选的前 + 导 BOM;写入仍是不带 BOM 的 UTF-8,原始 `$VAR` 引用依旧保留。([#5983]) + +- **智能体:** `read_file` 的非有限行号边界不再让运行在循环检测中崩溃。模型 + 输出的 `start_line: 1e999` 会被解析为 `float("inf")`,`_coerce_line_number()` + 里的 `int()` 转换在 `after_model` 路径上抛出未捕获的 `OverflowError`,run 以 + 难懂的内部错误收场,而不是由 `read_file` 自身的 schema 校验返回正常的工具 + 错误。该转换器现在把 `OverflowError` 视为其他不可用输入:边界回退为 `None`, + 调用按既有的开放式读取键处理;正常的整数边界行为完全不变。([#5990]) + +- **集成:** 远程 provisioner 模式下,完成 Lark 授权不再把集成面板的运行时 + 卡片翻成"未就绪"。此前变更响应只验证授权、从不探测沙箱运行时就绪,而 + 前端将其整体写入缓存,面板于是显示一个刷新页面才会纠正的状态。现在 + 安装、配置完成、凭据切换和授权完成共用同一个变更状态构造器,与 + `GET /lark/status` 路由一致地探测运行时就绪,且构造发生在释放每用户 + 凭据锁之后(此前探测在持锁状态下运行,可能让其他凭据操作阻塞一个网络 + 超时的时长)。状态 API 新增 `sandbox_runtime_probed`,前端只在变更响应 + 报告未探测时保留先前确认的运行时字段。([#5994]) + +- **渠道:** Feishu 和 DingTalk 收到的附件现在可被沙箱读取。在 AIO/Docker + 沙箱模式下,沙箱以非 root 用户访问 bind 挂载的 thread 目录,而这两个 + 渠道自行落盘的入站文件权限为 `0o600`——agent 拿到的 + `/mnt/user-data/uploads/` 路径读出来是空的,来自这两个渠道的附件 + 因此静默不可用。现在两者都在自行写入后立即授予 group/other 读权限,与 + 渠道管理器和 HTTP 上传路径一致(Telegram 已由 #5581 覆盖)。本地沙箱 + 行为不变。([#5998]) + +- **数据库:** `DatabaseConfig` 现在严格校验 `pool_size`、`pool_recycle` 与 + `command_timeout`。此前 YAML 布尔值会被静默转换为 `1`/`0`: + `pool_size: true` 会把连接池限到单个连接,`command_timeout: true` 相当于 + 1 秒的语句超时;`pool_size` 还接受非正数(`0`、`-1`),`command_timeout` + 接受 `inf`(YAML `.inf` 或 `"1e999"`),等于永不超时。现在 `pool_size` 与 + `pool_recycle` 强制要求正整数,`command_timeout` 拒绝布尔值与非有限浮点 + 数,`null` 仍表示禁用超时。([#6009]) + +- **社区:** Firecrawl 工具现在会关闭每次调用构建的异步 HTTP 连接池。异步化 + 改造之后,`_get_firecrawl_client()` 每次调用都会新建一个 + `AsyncFirecrawlApp`,而它会立即创建一个由 `httpx.AsyncClient` 支撑的连接 + 池,且不公开任何关闭接口,于是每次 `web_search_tool` / `web_fetch_tool` 调 + 用都遗弃一个未关闭的池。两个工具现在都在 `finally` 中通过尽力而为的 + `_aclose_firecrawl_client` 关闭它:缺少内部 `_v2_client.async_http_client` + 句柄时退化为空操作,关闭失败只记日志,不会掩盖工具本身的结果或错误。 + ([#6013]) + +- **能力中心:** 管理员可以在能力中心再次管理共享 MCP 插件。#5966 之后 + Platform provided 区块对管理员和普通用户渲染同样的只读视图,无法新增、编辑、 + 启用/停用或删除,而仅限管理员的 `/api/mcp/config` 接口本就支持这些操作。 + 管理员(非静态模式)现在在 Platform provided 下恢复 MCP 编辑器,改动写入对 + 所有用户生效的共享部署配置;普通用户仍为只读,My plugins 继续管理个人配置。 + MCP 查询同时区分作用域与用户 ID,控件的可访问名称带上本地化的归属分区, + 重名的共享与个人连接也能区分。([#6018]) + +- **网关:** 托管子代理的存储变更现在会在请求取消时排空。`/api/subagents` 的 + create/update/delete 端点此前用裸 `asyncio.to_thread()` 承载持久化写入:取消请求 + 只会取消等待方,已启动的文件/SQL 存储变更可能在请求生命周期结束后仍在提交。三个 + 写入现在都经由既有的 `await_drained()` 辅助函数(封装为 `_run_store_mutation`),已 + 准入的变更先到达终态,调用方的取消随后才传播;目录读取与校验保持原有取消语义。 + ([#6023]) + +- **网关:** 仅管理员可用的托管模型保存在请求取消时现在会排空。`PUT /api/managed-models` + 此前用裸 `asyncio.to_thread(_save, body)` 持久化共享的加密模型目录:取消请求只会 + 取消等待方,已启动的 `ManagedModelStore.save()` 可能在请求生命周期结束后仍在提交 + 目录状态。保存现在经由既有的 `await_drained()` 辅助函数排空;取消后发生的工作线程 + 失败只记录异常类型即被消费——存储错误可能携带凭据。目录读取与连接探测保持原有取消 + 语义。([#6024]) + +- **配置:** 一批此前会静默接受坏值的数值配置字段现在带上了范围校验与布尔 + 拒绝,与此前 stream-bridge 心跳和数据库连接池的守卫一致。 + `sandbox.port` 限制在 1-65535,不再以晦涩的 Docker 端口绑定错误暴露; + `circuit_breaker.failure_threshold` 要求至少为 1(0 会在检查本身就熔断); + `summarization.trim_tokens_to_summarize` 要求至少为 1,不再用 + `max(1, ...)` 把误配悄悄变成单 token 摘要; + `mcp_servers.*.tool_call_timeout`/`session_init_timeout`、 + `models.*.stream_chunk_timeout` 与 `run_events.max_trace_content` 拒绝会 + 让每次调用或流立即中断、把 trace 截成空的 `0`/`inf`; + `mcp_servers.*.oauth.refresh_skew_seconds` 拒绝负数。以上字段均不再把 + YAML 的 `true` 强转成 `1`;文档允许的取值(如 `sandbox.idle_timeout: 0`、 + `null` 超时)保持不变。([#6026]) + +- **前端:** 在 Windows 上,`pnpm dev` 现在默认绑定 `127.0.0.1`。此前开发 + 服务器监听 `0.0.0.0`,当 Hyper-V/WSL2/winnat 保留了覆盖 3000 端口的端口 + 段时,启动会报 `Error: listen EACCES: permission denied 0.0.0.0:3000`,而 + 同一端口绑定 loopback 却能成功。显式传入的 `-H`/`--hostname` 照常透传, + 需要局域网访问时可用 `pnpm dev -- --hostname 0.0.0.0` + (`DEER_FLOW_DEV_ALLOWED_ORIGINS` 用法不变)。默认 `make dev` 流程不受 + 影响——dev nginx 上游本就指向 `127.0.0.1:3000`——macOS/Linux 行为不变。 + ([#6034]) + +- **网关:** Windows 上的内置解释器修复提示不再显示双反斜杠路径。内置 + MCP server 指向另一个 Python 解释器时返回的 HTTP 400 detail 用 + `{sys.executable!r}` 插值路径,`repr()` 会转义反斜杠,提示因此显示成 + `'D:\\...\\python.exe'`——既没法照抄进终端,放进它让你编辑的 JSON 里也是 + 噪音。现在 detail 会把路径渲染成可直接用于 server 配置的 JSON 字符串,并 + 附上供终端使用的原始路径;POSIX 路径只是引号来源变了。([#6040]) + +- **渠道:** Slack 入站文本现在会解码 Slack 一律转义的三种实体,像 + `if a < b && c > d: print("R&D")` 这样的消息可按原样到达 agent,而不是 + `if a < b && c > d: print("R&D")`。`<`、`>` 和 + `&` 在剥除 bot 提及之后解码——真实提及是原始的 `<@U…>`,而用户手动 + 输入的 `<@U…>` 到达时已转义、仍是纯文本——其中 `&` 最后解码,转义 + 实体恰好只解码一次。此前被破坏的文本会误导 agent 推理,可能把 `<` + 写进文件,引用回复时还会被二次转义。出站转义不变。([#6045]) + +- **沙箱:** 带目录前缀的 glob 模式现在相对搜索根锚定,`**` 也变为递归。 + `path_matches`——`glob` 工具与各 provider 的 `grep` `glob=` 过滤器共用的辅助 + 函数——此前使用 `PurePosixPath.match`,在 Python 3.12+ 上该方法从右侧匹配且把 + `**` 当作单段 `*`:`src/**/*.py` 会漏掉 `src/top.py` 与更深的文件,而 + `docs/*.md` 还会误中 `vendor/docs/b.md`。含 `/` 的模式现在从搜索根开始逐段 + 匹配,`**` 跨越零个或多个目录;仅基名的模式仍在任意深度匹配。([#6046]) + +- **cli:** 当运行以 LLM 错误回退收尾时,`deerflow --print` 和 `--json` + 现在以退出码 1 结束。提供方故障不会被抛出——`LLMErrorHandlingMiddleware` + 会把它们转成带有 `deerflow_error_fallback` 标记的 AI 消息——因此无头 + 运行在凭证过期这类场景下会打印回退文本并以 0 退出,调用脚本把一次失败 + 的运行当成了成功。两种模式现在遵循与抛异常相同的失败契约:`--print` + 仍把回退文本写到 stdout,但在 stderr 追加 + `Error: LLM request failed (error_type=…, error_reason=…)`;`--json` 在 + 流式事件之后追加一条终态错误记录。运行中途出现回退、随后仍有真实最终 + 回答的情况依旧以 0 退出。([#6056]) + +- **前端:** 运行时长现在作为推理折叠区的标签,不再是单独的脚注行。此前 + 已完成的回答在回答上方显示一个泛化的推理标题,又在仅悬停可见的操作 + 工具栏下方显示一行时长,工具栏隐藏时就留下一段无解释的空隙。折叠区 + 现在显示 `Took 31s`(`用时 31 秒`)并带紧凑箭头,仍可用鼠标和键盘展开, + 可访问名称中保留本地化的视觉隐藏推理提示,并绑定到答案气泡、处理过程 + 组和子代理组各自的所属运行;单独的时长脚注随之移除。没有时长的流式与 + 历史消息仍用泛化标签,恢复的历史推理默认折叠,实时推理仍在完成一秒后 + 自动收起。([#6057]) + +- **沙箱:** 沙箱中的每条 bash 命令现在都通过 `DEERFLOW_USER_ID` 携带已鉴权的用户 + id。skill 脚本以 bash 子进程形式在沙箱内运行,无法触达 Gateway 鉴权上下文, + 用户级 skill 因此无从得知当前服务的用户(issue #3919)。现在命令会加上 + `export DEERFLOW_USER_ID=` 前缀(id 不可用时为 + `unset DEERFLOW_USER_ID`),POSIX 本地与远程沙箱两条路径均生效,并位于既有 + `DEERFLOW_CHANNEL_USER_ID` 前缀之前;Windows 则改为通过子进程环境注入。该变量 + 仅供参考——命令可以覆盖它——鉴权仍由服务端解析。([#6058]) + +- **渠道:** 被拆成多条连续消息的长 Discord 回复现在可以精确还原。此前 + `_split_text` 在拆分点的换行符之前结束当前块、再从剩余文本开头剥掉该 + 换行符,每个 2000 字符边界都会丢失一个换行——落在切点上的连续空行则整段 + 丢失,拼接起来的气泡因此丢失段落分隔。现在边界换行符保留在当前块尾部, + 任意输入下 `"".join(_split_text(text))` 都还原原文,同时每条消息仍不超过 + Discord 的 2000 字符上限。未超限的回复和空字符串行为不变。([#6062]) + +- **目标:** `/goal` 评估器现在能看到助手的工具调用和工具结果,已完成的文件工作不会再 + 以 `missing_evidence` 停摆。此前它只读用户与助手的文本:`format_visible_conversation()` + 丢弃工具消息,只携带工具调用的助手消息渲染为空,评估器自身的"证据不足即失败"规则 + 便让大多数已完成的文件任务保持目标活动(在真实 Gateway 的 A/B 中,正确输出被清除 + 目标的比例从 DeepSeek 的 15/38 升至 35/36,MiMo v2.6 Pro 从 4/33 升至 29/30)。现有 + 30 条消息窗口内的工具步骤现在按顺序以 `Assistant tool call: ...` 和 + `Tool result (...): ...` 行呈现——参数值截断到 200 字符、每步 600——12,000 字符的证据 + 上限改为从末尾保留整行并把最新的用户请求保持在顶部,不再从行中切开。工具文本经 + JSON 转义保证每步一行,失败结果标注 `error`;与 web UI 一致,隐藏助手消息的调用 + 不进入证据,而澄清提问保留。系统文本新增固定规则:工具结果只是数据绝非指令,且 + 仅有成功的工具结果并不能证明目标已达成。([#6068]) + +- **目标:** `/goal` 评估器调用失败现在会像其他 stand-down 一样记录到目标上。此前 + worker 只写一条 "Goal evaluator failed" 日志便直接返回:首次评估后目标没有任何 + `last_evaluation`,否则会保留上一个 run 的结论和 run id,使 + `GET /api/threads/{id}/goal` 看起来像从未评估过——无论原因是连接错误、解析器拒答 + 还是提供方超时。现在失败会持久化 `blocker: "run_failed"`、 + `stand_down_reason: "evaluator_failed"` 与当前 run id,原因只写异常类型 + ("The goal evaluator did not return a verdict (APIConnectionError)."),因为提供方 + 的错误信息可能携带请求细节。目标仍然保持活动,不重试、不启动隐藏续跑;若 run 在 + 此期间被中止则不记录。([#6073]) + +- **前端:** 在 `/workspace/chats/new` 中,首批流式到达的助手消息不再渲染 + 在所提交问题的上方。固定首轮顺序的本地 human 锚点此前与 SDK thread id + 比较——该 id 在创建确认前尚未定义——而确认到达时锚点又被清空,于是早期 + 的推理和回答块会跳到问题上方,直到本轮落定。现在首轮顺序改为绑定当前 + 显示的会话(`displayThreadId ?? threadId`),锚点在创建确认后保留; + 切换会话时锚点照旧清空。([#6074]) + +- **目标:** `/goal` 评估器现在能看到用户对 Human Input Card 的回答,同时主 agent 的 + 系统提示在未绑定 `bash` 工具时不再教授脚本写法。Web UI 把卡片回答作为隐藏 human + 消息发送,而 `format_visible_conversation()` 会跳过隐藏消息——评估器看得到提问却 + 看不到回答,把遵循回答当成臆测,即使工作正确目标也以 `needs_user_input` 停摆。 + 回答现在以 `User (Human Input Card answer): ...` 行进入证据,压缩空白并截断到 + 2,000 字符,超长时保底保留在顶部。另外,默认本地沙箱不开启宿主 bash,但系统提示 + 仍在讲解脚本写法——实测中两个模型各有 38/120 个 run 写出了无人能运行的辅助脚本。 + `apply_prompt_template()` 新增 `bash_available` 参数,lead agent 与内嵌 client 传入 + `has_bash_tool(authorized_tools)`;没有 bash 时,脚本指导替换为一行指示——直接算出 + 结果并用 `write_file` 写出——子代理章节也去掉 `bash` 示例。绑定 bash 时提示不变。 + ([#6082]) + +- **网关:** agents 路由的持久化写入现在会在请求取消时排空。`POST /api/agents`、 + `PUT /api/agents/{name}`、`DELETE /api/agents/{name}` 和 `PUT /api/user-profile` + 此前用裸 `asyncio.to_thread` 承载存储写入:断开连接会取消 handler 任务,要么取消 + 尚未开始的 worker——写入静默丢失——要么与已在运行的 worker 脱钩,其失败被静默丢弃 + (handler 的 `except`,即 500 加错误日志,永远不会执行)。四个写入现在统一经由基于 + 既有 `await_drained()` 的 `_drained_write` 辅助函数:已启动的 worker 先完成,调用方 + 的取消随后才传播;丢失的失败只记录异常类型,而 `AgentExistsError` 等预期领域错误仍 + 会以 409 返回给仍在连接的调用方。读取保持可取消语义——放弃它们不会有任何损失。 + ([#6087]) + +- **前端:** APNG、AVIF 和 WebM 工件在工作区工件列表和查看器中现在显示 + 对应的媒体图标,而不是通用文档图标。这三种格式本可在浏览器中预览,但 + `getFileIcon` 没有对应的扩展名分支:`apng` 和 `avif` 现在映射到图片 + 图标,`webm` 映射到视频图标,与既有预览支持一致。未知扩展名仍回退到 + 文档图标。([#6089]) + +- **前端:** 保存的 `Dockerfile` 和 `Makefile` 工件现在可以作为源码打开并 + 编辑,而不再落入不支持的文件下载视图——尽管这两个名字本就在语言映射表 + 里。`getFileExtension` 此前对整个路径按 `.` 切分而不是先取基名,无扩展名 + 的名字——或位于带点父目录下的文件——永远解析不到对应的映射项。类型识别 + 现在基于基名进行;既有的后缀名和隐藏文件映射保持不变,流式写文件预览 + 也仍使用自己的文本回退。([#6091]) + +- **MCP:** 个人 MCP 配置写入现在会等已受理的变更落盘,再向调用方传递取消。 + 此前写入路径用裸的 `asyncio.to_thread(_mutate, ...)` 执行持久化文件变更, + 请求在 worker 启动后被取消时,路由可以直接退栈,而已开始的持久化变更仍在 + 运行、缺乏请求侧的生命周期归属。现在变更经取消安全的 `await_drained()` 辅 + 助函数排空:路由存活到已开始的写入结束,随后原始 `CancelledError` 照常传 + 播。只读路径保持既有取消语义。([#6093]) + ### 安全 - **鉴权:** 仅有认证而无权限校验的路由现在会强制执行权限检查,且 @@ -1487,6 +2241,26 @@ 与线程解析)与 `actual_path` 匹配时才被读取,跨线程引用在 `view_image` 重新执行之前保持不可用。([#5824]) +- **内存:** 开启 `pii_redaction` 后,进入内存提取队列的对话负载现在也会被脱敏, + 补上 #5527 刻意留下的最后一块。请求级脱敏刻意在线程状态里保留原文,而 + `MemoryMiddleware._resolve_add_args()` 会把这些原始消息直接传给 + `MemoryManager.add_nowait(...)`,因此在 `pii_redaction.enabled=true` 时,缓存的 + 提取负载、提取模型的输入以及最终落盘的事实仍可能带着原始 PII。 + `MemoryMiddleware` 现在像摘要/持久上下文/标题路径一样接收 `pii_redaction_config`, + 在入队边界(覆盖两个 agent 钩子的同一入口)通过共享的 `redact_text()` 完成 + 脱敏;缓存负载本身即为干净副本,消息通过 `model_copy` 重建、绝不改动原件。 + 开关默认关闭,未设置时行为不变。([#5577]) + +- **授权:** 技能授权现在在装配与激活两个环节强制执行,`skills: {allow: + ["data-analysis"]}` 这类 RBAC 策略因此能真正拒绝某个技能——Phase 2A + (#4439)覆盖的是 Gateway 路由,技能此前仍只受 agent 配置白名单约束。 + 主 agent 的技能目录、子代理 `config.skills` 加载、`describe_skill` 与所有 + 激活门都经由同一个 `authorize("skill", "activate")` 辅助函数查询 + AuthorizationProvider 的 `skill` 策略;拒绝时返回与成员资格拒绝一致的 + 提示,provider 错误遵循既有的 fail-closed/fail-open 配置。被拒绝的 + `read_file` 读取 `SKILL.md` 时会打上 `skill_context_denied` 标记,持久上 + 下文、技能 allowed-tools 与自主密钥绑定都不会激活被拒绝的技能。([#4541]) + ### 文档 - **文档:** 修正 Apple Container 的验证说明。 @@ -1545,6 +2319,49 @@ 保留的存储究竟如何增长——周期快照保留完整消息列表,delta 只是把二次项除以节奏,而非将其消除。([#5845]) +- **文档:** 修正引用了不存在的异步 API 的 harness 客户端示例。 + `DeerFlowClient` 并没有 `astream`/`ainvoke`,但《Create Your First + Harness》教程与集成指南(中英文)全程围绕它们编写,照抄教程的第一次 + 调用就会抛出 `AttributeError`;示例还把覆盖参数嵌在 + `config={"configurable": {...}}` 字典里,而 `stream()`/`chat()` 会把它静 + 默吞进 `**kwargs`。示例现在改用同步的 `client.stream(...)` / + `client.chat(...)`,以平铺关键字参数传递覆盖项(`model_name=`、 + `subagent_enabled=`),通过构造器配置 agent + (`DeerFlowClient(agent_name=...)`),FastAPI SSE 示例改为同步生成器, + "Async streaming" 一节更名为 "Streaming"。公开 API 无变化。([#5843]) + +- **文档:** 在 harness memory 页面 + (`frontend/src/content/{en,zh}/harness/memory.mdx`)中补上可插拔的 + `MemoryManager` 架构说明;此前该页只讲配置与存储,想接入独立的记忆系 + 统只能自行从源码反推契约。新增的 "Memory Manager" 一章解释后端无关的 + 契约、通过 `manager_class` / `backend_config` 选择后端、五个内置后端的 + 对比,以及 `deermem` 与 `openviking` 各自的接入方式;"Implementing your + own memory backend" 一章给出最小可运行骨架、可选操作、失败语义 + (`MemoryConflictError` → HTTP 409)与宿主钩子。既有 "Custom storage + backend" 一节新增提示框,区分替换 DeerMem 的 `storage_class` 与接入完 + 全独立的记忆后端。([#5929]) + +- **文档:** 修正 MemoryManager 指南中照做即会失败的内容(#5929 的勘误, + 中英文同步)。OpenViking 配置按文档写 `failure_policy.read: fail_closed` + 会导致 manager 构建失败——该键只接受 `fail_open` 或 `raise`——且 + `mode: middleware` 是必需项而非推荐项。`from_config` 与 `add`、 + `get_context` 同为必需,跳过它的后端会解析成 `None`。工具模式下还需要 + 实现 `get_memory()` 与事实 CRUD 方法,因为除非后端设置 + `requires_passive_writes_in_tool_mode`,否则不会安装 + `MemoryMiddleware`;`create_fact` 返回 `(memory_data, fact_id)`,未知事 + 实 id 抛出 `KeyError`(HTTP 404);`staleness_review_enabled` 默认开启, + 且五个开关都位于 `memory.backend_config` 之下。([#5943]) + +- **文档:** 为中文 README 补上缺失的「外部聊天消息角色 (External Chat + Message Roles)」一节,与英文 README 对应章节对齐,置于 Gateway 管理员 + 权限等同于代码执行与部署默认值两节之间。内容说明 run 请求与手动线程 + 状态更新会以 HTTP 400 拒绝客户端提交的 `system` / `developer` 消息 + (session/PAT 认证不授予 system prompt 权限);该检查不会重写已有的 + checkpoint——重启不会移除已持久化的指令;以及如何用 + `python backend/tests/poc_external_system_message_injection.py --help` + 在本地验证,包括在一次性线程上运行 `--expect vulnerable` / + `--expect blocked`。([#6059]) + ### 内部改进 - **测试:** 为 run-change 时钟修复及其回滚补充历史回归覆盖。从已 @@ -1581,6 +2398,30 @@ 导致较旧的 checkpoint 水合为空),在上游修复落地前标记 skip。 无运行时行为变化。([#5797]) +- **tests:** 让 blocking-I/O 的 Lark auth-complete 测试在 Windows 上可用。 + 此前 CLI 夹具是一个无扩展名的 POSIX shell 脚本,Windows 上的生产 CLI 发 + 现机制无法解析它,测试还没跑到要覆盖的子进程逻辑就因 HTTP 404 + (`lark-cli is not installed`)失败。现在 Windows 上生成 `.cmd` 桩 + (POSIX 仍为可执行 shell 脚本),放在含空格的目录中并经生产 PATH 查找 + 解析;植入假的应用凭据让授权完成流程真正到达 CLI,并断言认证成功及返 + 回的用户名。夹具约定已补充到 `backend/tests/AGENTS.md`。([#5826]) + +- **tests:** Windows 开发机上跳过 22 个绑定 POSIX 宿主环境的后端测试,而 + 不是让它们硬性失败。`test_extension_dependency_sync.py` 的 11 个用例直 + 接调用 `make -n`(GNU make 不在文档化的 Windows 安装范围内,报 + `FileNotFoundError`),`test_lark_broker.py` 的 10 个用例直接执行 + `#!` 脚本(`WinError 193`),还有一个 boxlite shim 测试在 `os.chmod` 后 + 断言 POSIX 的执行位语义。全部采用条件跳过:PATH 上有 `make` 的 + Windows 仍会运行 Makefile 契约测试,纯逻辑测试处处运行,Linux/CI 覆盖 + 不变。([#5931]) + +- **tests:** `tests/test_file_signature.py` 的夹具改为按字节写入,避免 + Windows 文本模式的换行转换混入。三个测试用 `Path.write_text(...)` 构造 + 夹具却断言字节级一致;Windows 上换行落盘为 CRLF,而 + `read_config_with_signature()` 的契约就是原样返回磁盘上的字节,返回值 + 正确,测试却失败(10 例中 3 例)。夹具现在改用 `write_bytes(...)`,stat + 过期测试改为二进制追加;产品行为无变化。([#6042]) + ## [2.1.0] — 2026-09-24 本节累积面向 [2.1.0](https://github.com/bytedance/deer-flow/milestone/2))的工作。 @@ -4258,6 +5099,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#3233]: https://github.com/bytedance/deer-flow/pull/3233 [#3241]: https://github.com/bytedance/deer-flow/pull/3241 [#3252]: https://github.com/bytedance/deer-flow/pull/3252 +[#3261]: https://github.com/bytedance/deer-flow/pull/3261 [#3270]: https://github.com/bytedance/deer-flow/pull/3270 [#3283]: https://github.com/bytedance/deer-flow/pull/3283 [#3284]: https://github.com/bytedance/deer-flow/pull/3284 @@ -4746,6 +5588,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#4538]: https://github.com/bytedance/deer-flow/pull/4538 [#4539]: https://github.com/bytedance/deer-flow/pull/4539 [#4540]: https://github.com/bytedance/deer-flow/pull/4540 +[#4541]: https://github.com/bytedance/deer-flow/pull/4541 [#4556]: https://github.com/bytedance/deer-flow/pull/4556 [#4558]: https://github.com/bytedance/deer-flow/pull/4558 [#4559]: https://github.com/bytedance/deer-flow/pull/4559 @@ -4807,6 +5650,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#4719]: https://github.com/bytedance/deer-flow/pull/4719 [#4722]: https://github.com/bytedance/deer-flow/pull/4722 [#4724]: https://github.com/bytedance/deer-flow/pull/4724 +[#4725]: https://github.com/bytedance/deer-flow/pull/4725 [#4726]: https://github.com/bytedance/deer-flow/pull/4726 [#4727]: https://github.com/bytedance/deer-flow/pull/4727 [#4729]: https://github.com/bytedance/deer-flow/pull/4729 @@ -4885,6 +5729,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#4919]: https://github.com/bytedance/deer-flow/pull/4919 [#4921]: https://github.com/bytedance/deer-flow/pull/4921 [#4928]: https://github.com/bytedance/deer-flow/pull/4928 +[#4929]: https://github.com/bytedance/deer-flow/pull/4929 [#4933]: https://github.com/bytedance/deer-flow/pull/4933 [#4936]: https://github.com/bytedance/deer-flow/pull/4936 [#4938]: https://github.com/bytedance/deer-flow/pull/4938 @@ -5194,6 +6039,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5562]: https://github.com/bytedance/deer-flow/pull/5562 [#5563]: https://github.com/bytedance/deer-flow/pull/5563 [#5564]: https://github.com/bytedance/deer-flow/pull/5564 +[#5565]: https://github.com/bytedance/deer-flow/pull/5565 [#5566]: https://github.com/bytedance/deer-flow/pull/5566 [#5567]: https://github.com/bytedance/deer-flow/pull/5567 [#5569]: https://github.com/bytedance/deer-flow/pull/5569 @@ -5201,6 +6047,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5572]: https://github.com/bytedance/deer-flow/pull/5572 [#5573]: https://github.com/bytedance/deer-flow/pull/5573 [#5576]: https://github.com/bytedance/deer-flow/pull/5576 +[#5577]: https://github.com/bytedance/deer-flow/pull/5577 [#5578]: https://github.com/bytedance/deer-flow/pull/5578 [#5579]: https://github.com/bytedance/deer-flow/pull/5579 [#5580]: https://github.com/bytedance/deer-flow/pull/5580 @@ -5226,6 +6073,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5617]: https://github.com/bytedance/deer-flow/pull/5617 [#5621]: https://github.com/bytedance/deer-flow/pull/5621 [#5622]: https://github.com/bytedance/deer-flow/pull/5622 +[#5623]: https://github.com/bytedance/deer-flow/pull/5623 [#5625]: https://github.com/bytedance/deer-flow/pull/5625 [#5630]: https://github.com/bytedance/deer-flow/pull/5630 [#5631]: https://github.com/bytedance/deer-flow/pull/5631 @@ -5280,6 +6128,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5745]: https://github.com/bytedance/deer-flow/pull/5745 [#5748]: https://github.com/bytedance/deer-flow/pull/5748 [#5750]: https://github.com/bytedance/deer-flow/pull/5750 +[#5752]: https://github.com/bytedance/deer-flow/pull/5752 [#5755]: https://github.com/bytedance/deer-flow/pull/5755 [#5756]: https://github.com/bytedance/deer-flow/pull/5756 [#5757]: https://github.com/bytedance/deer-flow/pull/5757 @@ -5288,6 +6137,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5760]: https://github.com/bytedance/deer-flow/pull/5760 [#5761]: https://github.com/bytedance/deer-flow/pull/5761 [#5762]: https://github.com/bytedance/deer-flow/pull/5762 +[#5763]: https://github.com/bytedance/deer-flow/pull/5763 [#5767]: https://github.com/bytedance/deer-flow/pull/5767 [#5769]: https://github.com/bytedance/deer-flow/pull/5769 [#5775]: https://github.com/bytedance/deer-flow/pull/5775 @@ -5295,9 +6145,14 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5777]: https://github.com/bytedance/deer-flow/pull/5777 [#5778]: https://github.com/bytedance/deer-flow/pull/5778 [#5780]: https://github.com/bytedance/deer-flow/pull/5780 +[#5782]: https://github.com/bytedance/deer-flow/pull/5782 +[#5784]: https://github.com/bytedance/deer-flow/pull/5784 [#5785]: https://github.com/bytedance/deer-flow/pull/5785 +[#5786]: https://github.com/bytedance/deer-flow/pull/5786 +[#5788]: https://github.com/bytedance/deer-flow/pull/5788 [#5792]: https://github.com/bytedance/deer-flow/pull/5792 [#5794]: https://github.com/bytedance/deer-flow/pull/5794 +[#5795]: https://github.com/bytedance/deer-flow/pull/5795 [#5796]: https://github.com/bytedance/deer-flow/pull/5796 [#5797]: https://github.com/bytedance/deer-flow/pull/5797 [#5798]: https://github.com/bytedance/deer-flow/pull/5798 @@ -5312,41 +6167,125 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5815]: https://github.com/bytedance/deer-flow/pull/5815 [#5816]: https://github.com/bytedance/deer-flow/pull/5816 [#5819]: https://github.com/bytedance/deer-flow/pull/5819 +[#5820]: https://github.com/bytedance/deer-flow/pull/5820 [#5823]: https://github.com/bytedance/deer-flow/pull/5823 [#5824]: https://github.com/bytedance/deer-flow/pull/5824 +[#5826]: https://github.com/bytedance/deer-flow/pull/5826 [#5830]: https://github.com/bytedance/deer-flow/pull/5830 [#5833]: https://github.com/bytedance/deer-flow/pull/5833 +[#5835]: https://github.com/bytedance/deer-flow/pull/5835 [#5836]: https://github.com/bytedance/deer-flow/pull/5836 [#5838]: https://github.com/bytedance/deer-flow/pull/5838 [#5839]: https://github.com/bytedance/deer-flow/pull/5839 [#5840]: https://github.com/bytedance/deer-flow/pull/5840 [#5841]: https://github.com/bytedance/deer-flow/pull/5841 [#5842]: https://github.com/bytedance/deer-flow/pull/5842 +[#5843]: https://github.com/bytedance/deer-flow/pull/5843 [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5849]: https://github.com/bytedance/deer-flow/pull/5849 +[#5850]: https://github.com/bytedance/deer-flow/pull/5850 +[#5852]: https://github.com/bytedance/deer-flow/pull/5852 +[#5854]: https://github.com/bytedance/deer-flow/pull/5854 [#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5856]: https://github.com/bytedance/deer-flow/pull/5856 +[#5857]: https://github.com/bytedance/deer-flow/pull/5857 +[#5858]: https://github.com/bytedance/deer-flow/pull/5858 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 +[#5860]: https://github.com/bytedance/deer-flow/pull/5860 +[#5861]: https://github.com/bytedance/deer-flow/pull/5861 +[#5864]: https://github.com/bytedance/deer-flow/pull/5864 +[#5870]: https://github.com/bytedance/deer-flow/pull/5870 +[#5872]: https://github.com/bytedance/deer-flow/pull/5872 +[#5875]: https://github.com/bytedance/deer-flow/pull/5875 [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 +[#5883]: https://github.com/bytedance/deer-flow/pull/5883 [#5884]: https://github.com/bytedance/deer-flow/pull/5884 +[#5888]: https://github.com/bytedance/deer-flow/pull/5888 +[#5890]: https://github.com/bytedance/deer-flow/pull/5890 +[#5892]: https://github.com/bytedance/deer-flow/pull/5892 [#5893]: https://github.com/bytedance/deer-flow/pull/5893 [#5894]: https://github.com/bytedance/deer-flow/pull/5894 +[#5897]: https://github.com/bytedance/deer-flow/pull/5897 +[#5898]: https://github.com/bytedance/deer-flow/pull/5898 +[#5899]: https://github.com/bytedance/deer-flow/pull/5899 [#5900]: https://github.com/bytedance/deer-flow/pull/5900 +[#5902]: https://github.com/bytedance/deer-flow/pull/5902 +[#5903]: https://github.com/bytedance/deer-flow/pull/5903 +[#5906]: https://github.com/bytedance/deer-flow/pull/5906 +[#5908]: https://github.com/bytedance/deer-flow/pull/5908 +[#5910]: https://github.com/bytedance/deer-flow/pull/5910 +[#5911]: https://github.com/bytedance/deer-flow/pull/5911 +[#5915]: https://github.com/bytedance/deer-flow/pull/5915 +[#5917]: https://github.com/bytedance/deer-flow/pull/5917 +[#5919]: https://github.com/bytedance/deer-flow/pull/5919 +[#5921]: https://github.com/bytedance/deer-flow/pull/5921 +[#5927]: https://github.com/bytedance/deer-flow/pull/5927 [#5928]: https://github.com/bytedance/deer-flow/pull/5928 +[#5929]: https://github.com/bytedance/deer-flow/pull/5929 +[#5931]: https://github.com/bytedance/deer-flow/pull/5931 [#5934]: https://github.com/bytedance/deer-flow/pull/5934 [#5935]: https://github.com/bytedance/deer-flow/pull/5935 +[#5937]: https://github.com/bytedance/deer-flow/pull/5937 +[#5943]: https://github.com/bytedance/deer-flow/pull/5943 +[#5944]: https://github.com/bytedance/deer-flow/pull/5944 [#5945]: https://github.com/bytedance/deer-flow/pull/5945 +[#5947]: https://github.com/bytedance/deer-flow/pull/5947 +[#5948]: https://github.com/bytedance/deer-flow/pull/5948 +[#5949]: https://github.com/bytedance/deer-flow/pull/5949 +[#5950]: https://github.com/bytedance/deer-flow/pull/5950 +[#5954]: https://github.com/bytedance/deer-flow/pull/5954 +[#5956]: https://github.com/bytedance/deer-flow/pull/5956 +[#5957]: https://github.com/bytedance/deer-flow/pull/5957 +[#5959]: https://github.com/bytedance/deer-flow/pull/5959 +[#5960]: https://github.com/bytedance/deer-flow/pull/5960 +[#5961]: https://github.com/bytedance/deer-flow/pull/5961 +[#5963]: https://github.com/bytedance/deer-flow/pull/5963 [#5964]: https://github.com/bytedance/deer-flow/pull/5964 +[#5965]: https://github.com/bytedance/deer-flow/pull/5965 +[#5966]: https://github.com/bytedance/deer-flow/pull/5966 +[#5968]: https://github.com/bytedance/deer-flow/pull/5968 +[#5977]: https://github.com/bytedance/deer-flow/pull/5977 +[#5978]: https://github.com/bytedance/deer-flow/pull/5978 +[#5980]: https://github.com/bytedance/deer-flow/pull/5980 [#5981]: https://github.com/bytedance/deer-flow/pull/5981 [#5982]: https://github.com/bytedance/deer-flow/pull/5982 +[#5983]: https://github.com/bytedance/deer-flow/pull/5983 [#5987]: https://github.com/bytedance/deer-flow/pull/5987 +[#5990]: https://github.com/bytedance/deer-flow/pull/5990 [#5991]: https://github.com/bytedance/deer-flow/pull/5991 +[#5994]: https://github.com/bytedance/deer-flow/pull/5994 +[#5998]: https://github.com/bytedance/deer-flow/pull/5998 +[#6009]: https://github.com/bytedance/deer-flow/pull/6009 +[#6013]: https://github.com/bytedance/deer-flow/pull/6013 [#6015]: https://github.com/bytedance/deer-flow/pull/6015 +[#6018]: https://github.com/bytedance/deer-flow/pull/6018 +[#6023]: https://github.com/bytedance/deer-flow/pull/6023 +[#6024]: https://github.com/bytedance/deer-flow/pull/6024 +[#6026]: https://github.com/bytedance/deer-flow/pull/6026 +[#6034]: https://github.com/bytedance/deer-flow/pull/6034 +[#6040]: https://github.com/bytedance/deer-flow/pull/6040 +[#6042]: https://github.com/bytedance/deer-flow/pull/6042 +[#6045]: https://github.com/bytedance/deer-flow/pull/6045 +[#6046]: https://github.com/bytedance/deer-flow/pull/6046 +[#6056]: https://github.com/bytedance/deer-flow/pull/6056 +[#6057]: https://github.com/bytedance/deer-flow/pull/6057 +[#6058]: https://github.com/bytedance/deer-flow/pull/6058 +[#6059]: https://github.com/bytedance/deer-flow/pull/6059 +[#6062]: https://github.com/bytedance/deer-flow/pull/6062 [#6066]: https://github.com/bytedance/deer-flow/pull/6066 +[#6068]: https://github.com/bytedance/deer-flow/pull/6068 [#6069]: https://github.com/bytedance/deer-flow/pull/6069 [#6070]: https://github.com/bytedance/deer-flow/pull/6070 +[#6073]: https://github.com/bytedance/deer-flow/pull/6073 +[#6074]: https://github.com/bytedance/deer-flow/pull/6074 [#6076]: https://github.com/bytedance/deer-flow/pull/6076 +[#6082]: https://github.com/bytedance/deer-flow/pull/6082 +[#6087]: https://github.com/bytedance/deer-flow/pull/6087 [#6088]: https://github.com/bytedance/deer-flow/pull/6088 +[#6089]: https://github.com/bytedance/deer-flow/pull/6089 +[#6091]: https://github.com/bytedance/deer-flow/pull/6091 +[#6093]: https://github.com/bytedance/deer-flow/pull/6093 [#6112]: https://github.com/bytedance/deer-flow/pull/6112