diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d30d4de0..22f6cd4983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,268 @@ 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)). -As of 2026-10-21, the milestone includes **829 merged pull requests**. +As of 2026-10-28, the milestone includes **1068 merged pull requests**. ### Added +- **plugins:** Add an administrator-only `GET /api/plugins/diagnostics` + endpoint returning a redacted snapshot of the Gateway process's retained + extension diagnostics. Each item carries a `source` restricted to dotted + identifiers of at most 256 characters (anything else comes back as + `[redacted]`), a `level` limited to `debug`/`info`/`warning`/`error`, and a + fixed category `summary` — extension loading, registration, router + mounting, or service start/stop — selected by message prefix, never the raw + exception text, which stays in the Gateway logs. Responses are sent with + `Cache-Control: private, no-store`. ([#7612]) + +- **extensions:** Give the agent-teams example extension durable team public + state: three new model tools — `publish_team_state`, `read_team_state`, and + `update_team_state` — let members store objectives, plans, findings, + questions, and requirements that survive outside the shared message window. + Publishes are idempotent per member `request_id`, updates use + `expected_revision` optimistic concurrency so a stale writer receives the + current entry instead of overwriting, entries carry creator/editor and + source attribution, and state is bounded to 20 entries and 16,000 UTF-8 + JSON bytes. Active state is injected into each member's shared context + ahead of recent messages, and the team browser page shows the state + snapshot with links back to source conversations. ([#7611]) + +- **models:** Recognize `kimi-k3.1-pro` and `kimi-k3.1-pro-highspeed` under + the `moonshot-cn` managed preset with required thinking, preserved reasoning + history, vision support, and native `reasoning_effort` values + `low`/`high`/`max` (default `max`; unsupported values fall back to the + default). At the official `https://api.moonshot.cn/v1` endpoint, + `PatchedChatDeepSeek` now omits `thinking` and the fixed sampling keys + `temperature`, `top_p`, `n`, `presence_penalty`, and `frequency_penalty` + from the final request — including copies supplied through `extra_body`, + without mutating caller-owned mappings — so Moonshot applies its fixed + values. The endpoint check matches the effective request model including an + `extra_body.model` override; Kimi K2.5, unknown IDs, and other endpoints + are unchanged. ([#7566]) + +- **jina:** Emit one bounded DEBUG completion summary per Jina crawl — a + `Jina crawl completed` line with an `outcome` (`success`, `error`, or + `cancelled`), a classified `reason` (`success`, `empty_response`, + `response_limit`, `invalid_configuration`, `nonretryable_http`, + `attempt_exhaustion`, `retry_after_unfit`, `budget_exhaustion`, + `request_failure`, or `cancellation`), HTTPX `post`/`stream` attempts + counted at call start, and monotonic `elapsed_seconds`/`backoff_seconds` + that measure actual waits, including a partially cancelled sleep. Enabled by + `log_level: debug` in root `config.yaml`; INFO emits nothing. Summaries + contain no URL, credentials, headers, bodies, or exception text, and a + failing logging handler is swallowed so diagnostics never mask a result or + cancellation; returns and error logs are unchanged. ([#6504]) + +- **runtime:** Carry the web UI language (Settings > Appearance, `en-US` or + `zh-CN`) into runs as request-only `context.locale`: the Gateway validates it + against a finite allowlist and strips legacy `configurable` copies so it + never becomes a checkpoint default, `DynamicContextMiddleware` projects it + as a hidden `` human message that input + sanitization stops untrusted input from forging, and the lead agent writes + replies in it unless the user explicitly requests another language. Bundled + DeerMem stamps PII-redacted queued message copies with `deerflow_ui_locale` + and writes new facts and summary prose in the preferred language without + touching stored facts, JSON keys, or memory backend APIs. Runs without a + supported locale keep inferring language from the latest message. ([#7294]) + +- **batch:** Add browser review of persisted subagent batch results: a new + owner-checked `GET .../subagent-batches/{batch_id}/items/{position}` route + serves `SubagentBatchService.read_browser_batch_item`, a fixed projection of + report, acceptance, timing, and evidence fields that never exposes prompts + or execution specs and works without a live batch worker or provider fetch. + Its evidence snapshot whitelists `knowledge_sources` v1 RAGFlow fields and + re-bounds legacy rows at read time (100 sources, 512-character names, + 256-character IDs, 16,000-character text, up to 100 valid pages), counting + omissions instead of returning oversized data. The workspace batch panel + gains a per-item result dialog that renders the report with cited evidence + and refreshes after a retry or status change. ([#7006]) + +- **knowledge:** Ship the optional `deerflow-extension-knowledge-platform` + example extension (phase 3 of the knowledge work): operator-configured + platform connections that reuse the knowledge-management + `ManagementProvider` factories, plus hierarchical logical spaces (up to 500 + spaces, 200 members each, 1 MiB policy JSON) whose + `blocked`/`viewer`/`editor`/`manager` members inherit resources, operations, + tools, and processing defaults from ancestors and can only narrow them. + Routes under `/api/extensions/knowledge-platform` provide + administrator-only policy snapshots and atomic revision-checked policy + replacement (`409` on stale, written under `BEGIN IMMEDIATE`) plus + space-routed management operations that stack host plugin authorization for + both namespaces, an administrator floor, the space grant, and the existing + adapter and upstream grants. Space policy governs only these routes — it + does not reroute `knowledge_search` or grant chat retrieval — and SQLite + failures return a sanitized `503`. ([#7417]) + +- **runtime:** Publish an authoritative `run.terminal` trace event for + completed runs: the owning worker's finalization confirms it as a run-scoped + singleton through the event store's atomic `put_if_absent` + (`confirm_terminal_event`) after the final commit ACK and Journal drain and + before evidence-receipt capture, carrying the final status (`success`, + `error`, `interrupted`, or `timeout`) plus `protocol_version: 1` metadata. + Confirmation requires `RunManager.terminal_event_status` — an attributable + final ACK owned by this worker — so a matching row read or rollback's + intermediate ACK grants no authority, and a conflict or unknown result fails + the seal without changing business status. Terminal acknowledgment also now + requires an explicitly successful durable `update_status` write, and the run + event stream contract documents the additive event. ([#7134]) + +- **projects:** Add a `search_project_documents` shelf tool: document names + and summaries are matched first over the whole shelf, and only when that + yields too few hits does a content scan run under decoded-character budgets + (`projects.search_max_chars_per_document`, default 1,000,000 per document; + `projects.search_max_total_chars`, default 8,000,000 whole shelf; both read + per call, an invalid combination resetting them to the defaults). Content + hits carry original-text character offsets and neutralized excerpts that + feed `read_project_document(offset=...)`, and search never triggers + document conversion on the fly. The shelf is materialized in one snapshot + query shared by both tiers so a concurrent upload or trash cannot duplicate + or skip a match, and results report `stop_reason`, skipped counts, and + capped truncated/unscanned ID lists so partial coverage is never claimed as + complete. Config version bumps to 62. ([#7286]) + +- **channels:** Add thread-scoped lifecycle helpers to the Feishu pending-card + registry — `discard_thread` retires every pending card registered for a + thread, `prune_expired` drops TTL-expired entries, and `get_pending_entry` + reads an entry without consuming it — and wire retirement into the + conversation lifecycle: `ChannelManager` looks up the old thread on `/new` + and calls the new `Channel.on_thread_replaced` hook, which Feishu implements + by discarding that thread's registry entries so stale card button clicks are + rejected instead of lingering. ([#7357]) + +- **plugins:** Add the `deerflow-extension-report-sections` example extension: + a stateful `save_section` model tool stores a long report section by section + (outline at index 0, chapters at 1..N, 24,000-character cap per section) in + the plugin's own SQLite file scoped per user and thread, and a final + `section_index=999` call assembles every stored section into one markdown + document delivered as a tool artifact while only a short status line + re-enters the model context, keeping the full report out of the context + budget. Assembly drops a leading heading that restates the section title and + refuses assembled documents over the 60,000-byte cap, leaving the sections + stored. ([#7371]) + +- **knowledge:** Add the optional `deerflow-extension-knowledge-management` + extension, which ingests documents into a WeKnora v0.8.3 base and reports + provider-owned processing snapshots without touching core retrieval. + Routes under `/api/extensions/knowledge-management` cover capabilities, + ingestion, status, reprocess, and cancel, each requiring a non-internal + administrator principal plus an operator grant in `config.yaml -> plugins:` + for the exact user, connection, base, and operation. Snapshots normalize + upstream state (unrecognized statuses stay `unknown`) without inventing + task IDs or per-stage success, writes are never retried automatically, + chunk overrides on parent-child bases fail with `422`, missing credentials + map to `503` via `ManagementCredentialUnavailable`, and logs keep only + exception types, HTTP status, and fixed categories. ([#7293]) + +- **extensions:** Add `deerflow extensions list --json`, which serializes the + existing `list_configured()` inventory as a JSON array in configuration order + with only `name`, `package`, `entry_point`, `enabled`, and `required`; an + unconfigured package stays `"-"`, an empty inventory prints `[]`, and the + default table output is unchanged. The whole inventory is validated before any + stdout is written, so configuration errors still exit non-zero with diagnostics + on stderr and no partial output, and listing never resolves entry points or + exposes private plugin config or source metadata. ([#7351]) + +- **workflows:** Add Phase 4 multi-Gateway coordination to the workflows + extension: a shared PostgreSQL `storage_url` opts out of the SQLite + single-owner lock, so a workflow started on one Gateway can be inspected, + approved, resumed, or canceled on another, and recovery after lease expiry + stays session-authorized rather than unattended — the recovering user + supplies a fresh capability. The host adds the `extension_execution_leases` + table (migration `0048_extension_execution_leases.py`); the public API adds + `ExecutionFence`, and `AgentRuns.start`/`cancel` check user, holder, epoch, + and expiry under a row lock inside the admission or cancellation + transaction, failing closed without run heartbeats or a cross-process + stream bridge. SQLite mode keeps the Phase 3 single-owner contract. ([#7138]) + +- **agents:** Add the read-only `inspect_run_status` builtin tool, which returns + a content-free JSON snapshot of the current run — token usage totals, message + and tool-call counts, pending/in-progress todo counts, whether a compaction + summary exists, and context fill (`used_input_tokens`, `limit_tokens`, + `fraction`) computed from the last model-reported input against the effective + model's configured `context_window`. Counts are scoped to the current run when + pre-run message IDs are recorded under + `CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY` and labeled `visible_state` + otherwise (if compaction removed every pre-run ID the uncertain label is kept + rather than claiming run scope); when a `task`/`batch_task` dispatch message + has folded a finished subagent's usage into its own, its input count is + excluded from the fill estimate. The tool never returns conversation or todo + text. ([#7297]) + +- **community:** Migrate the You.com `web_search` tool to a coroutine over + `httpx.AsyncClient`; the sync `httpx.Client` version ran on the default + executor under `ainvoke`, holding a pool thread for the whole network + round-trip so parallel provider queries queued behind each other. The API + key and configured `max_results` are now resolved in a single + `_resolve_tool_settings` read through `asyncio.to_thread`, and the + `User-Agent`'s cold package-metadata read runs off-loop only on its first + call, later calls hitting the in-memory cache. + `backend/tests/blocking_io/test_youcom_search_async.py` pins the + loop-thread request, the off-loop config lookup, and the client factory + used — coverage a thread assertion alone would miss. ([#7186]) + +- **tools:** Let the Exa `web_search` and `web_fetch` tools target an + Exa-compatible provider through a per-tool `base_url` config entry (highest + precedence) or the `EXA_BASE_URL` environment variable, falling back to the + SDK default `https://api.exa.ai` when neither is set. Overrides must be + absolute `http://`/`https://` URLs with a host and valid port and no query + string or fragment; whitespace and trailing slashes are trimmed, and an + invalid value raises a configuration error that does not echo the configured + value and never falls back to another host. Because a custom backend fetches + pages in its own network, `web_fetch` screens a configured base URL through + `validate_delegated_backend_url` and refuses one that resolves to a loopback, + private, or unverifiable address unless the tool entry sets + `network_isolation_confirmed: true`. The API key is sent to the configured + host in the `x-api-key` header. ([#7306]) + +- **community:** Add `deerflow.community.jsonify.tools:dataset_rows_tool`, a + read-only tool that returns rows from maintained Jsonify datasets so the + agent reads a current table instead of scraping. The operator maps dataset + names to serve URLs in the tool entry (`datasets`, `max_rows`); the model + selects by name and never sees the URL — the credential must be https + without userinfo, pass `validate_public_http_url` before any request, and + is redacted from error text, while transport failures report only the + exception type. Paging follows `has_more`/`next_offset` with the dataset + version pinned after the first page, stops on an empty page or a cursor + that does not advance, clamps rows to `max_rows` with a `truncated` flag, + and strips the `_parent_row_id`/`_source_key` system columns. ([#7253]) + +- **models:** Add scoped model controls to Web chats: a read-only + `POST /api/models/resolve` endpoint matches the caller's direct input + against English and Chinese selection phrases and returns a decision + scoped to `task`, `turn`, or `conversation`, authorizing the resolved + choice with `model:use` so a recognized but denied name becomes + `unavailable` instead of degrading into task text. Models gain up to 16 + `aliases`; chat pages resolve ambiguous results through a human-input + prompt and keep conversation choices per-thread. Explicit selections + travel only through the run-only context keys `model_selection_requested` + and `subagent_model_name`, skip the graceful default-substitution + fallback at assembly, and `task_tool` accepts a per-child `model_name` + re-authorized at dispatch. ([#7249]) + +- **community:** Migrate the GroundRoute `web_search` and `web_fetch` tools + to coroutines over `httpx.AsyncClient`; both previously used a sync + `httpx.Client`, so a tool invoked through `ainvoke` held a + default-executor thread for the whole network round-trip and parallel + provider queries queued behind each other. Each call resolves its API key + and configured `max_results` in one `_resolve_tool_settings` read (also + serving `web_fetch`, replacing the removed `_get_api_key` wrapper) and + runs it through `asyncio.to_thread` to keep the config file off the event + loop. `backend/tests/blocking_io/test_groundroute_search_async.py` pins + the loop-thread request, the off-loop lookup, and the client factory + used. ([#7183]) + +- **deploy:** Let production Docker commands select a root dotenv file with + `DEER_FLOW_COMPOSE_ENV_FILE`, as in `DEER_FLOW_COMPOSE_ENV_FILE=... make + up`. The launcher resolves one path, relative selectors from the checkout + root, and feeds the same file to Compose `--env-file` interpolation, the + Gateway and provisioner `env_file` entries in `docker/docker-compose.yaml` + (now `${DEER_FLOW_COMPOSE_ENV_FILE:-../.env}`), the secret-resolution + probe, and `UV_EXTRAS` build-extra discovery, never sourcing it as shell + code. A missing, directory, or unreadable explicit selector fails before + Docker is invoked, while `make down` still stops the stack after the file + is removed; unset or empty selectors keep the optional repo-root `.env` + default. ([#6522]) + - **mcp:** Cache fully wrapped tools per unchanged server, selectively rediscovering only missing or changed configurations after binding reconciliation. Preserve successful zero-tool results separately from failures @@ -689,6 +947,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. binding, and regenerate/resume retain the source turn's accepted scope. This is a default, not an access-control policy: operator allowlists and dataset availability are still enforced. ([#5579]) + - **config:** Operators can now overlay core system prompts without source patches. `lead_prompt_overlay.prepend/append` wraps the assembled lead prompt, `subagents.agents..prompt_overlay` extends built-in and @@ -808,6 +1067,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `` injection gains a `Thinking Style:` line (plus optional `category: cognitive` facts), and the field is editable in Settings → Memory and exposed through the Gateway memory API. ([#3182]) + - **memory:** The sample-memory loader can now bulk-load registered users. It previously wrote the legacy shared memory path while authenticated users read per-user storage, so a successful load still left Settings showing no @@ -816,6 +1076,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. from the configured persistent database and backs up each user's existing memory before replacing it. Non-persistent database modes are rejected. ([#4066]) + - **memory:** Memory search and injection can rank candidates by lexical relevance instead of confidence alone. Confidence-only injection could prefer unrelated facts over ones useful to the current task; opt-in `retrieval_relevance_enabled` @@ -824,6 +1085,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. vector storage, or persisted-format changes; the tokenizer handles CJK bigrams without new dependencies. Legacy `get_context` implementations keep working. ([#5251]) + - **memory:** Add an opt-in tolerant `MarkdownMemoryStorage` (`memory.storage_class: markdown`) for the user-memory summary store. Corrupt or partially written files no longer raise `MemoryStorageCorruption` and crash @@ -832,6 +1094,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. still loads. The on-disk JSON format and the JSON UI are unchanged, and writes remain journaled JSON — this ships read-path tolerance only; a full Markdown write path is a follow-up. ([#5545]) + - **memory:** Add a deterministic DeerMem scope-isolation benchmark under `backend/scripts/benchmark/deermem_scope_isolation`. The opt-in validate/run/report workflow exercises lifecycle durability, concurrent @@ -897,6 +1160,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. batch subagents — out-of-allowlist selections fail closed, and scope ids never reach model inputs or tracing. Off by default via `knowledge_base.scope_selection_enabled` (config version 41). ([#5238]) + - **ragflow:** RAGFlow retrieval results now carry verifiable source citations. Clicking an inline knowledge citation, a document-title link in Sources, or the answer's knowledge sources list opens the retrieved excerpt, dataset and @@ -910,6 +1174,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. display as unavailable. These are retrieval-time evidence snapshots, not a live full-document viewer; existing direct `knowledge_search()` callers keep the string return type. ([#5551]) + - **community:** The image search tool exposes the `color` and `license_image` filters. Both were wired into the request path but unreachable from the tool signature, so agents could not narrow results to a palette or to @@ -969,6 +1234,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. markdown over plain HTTP, or through Unbrowse's hosted cloud browser for JavaScript-heavy pages (`render: auto|never|always`). One JSON-RPC POST per fetch, no new dependencies. ([#5981]) + - **models:** Administrators can manage shared models from Settings → Models without editing server configuration. New administrator-only `GET/PUT /api/managed-models` and `POST /api/managed-models/test` endpoints @@ -978,6 +1244,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. to new effective configuration snapshots; YAML profiles remain read-only and win name conflicts. The first version supports OpenAI-compatible Chat Completions endpoints; existing model authorization still applies. ([#5596]) + - **models:** Add a declarative reasoning capability contract for model profiles. A model can declare an optional `reasoning:` block (`thinking: unsupported|optional|required`, `on_disable_request`, `dialect`, `history`, and @@ -1094,6 +1361,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. option that preserves an unexpired binding command; credentials are saved on the backend and only the WeChat channel restarts. New WeChat connections default to QR login, with manual token entry still available. ([#5582]) + - **scheduler:** Push scheduled-task outcomes to the owner's bound IM identities via a durable `notification_deliveries` outbox and `NotificationDeliveryWorker`. Activates only when `channel_connections.enabled` @@ -1120,6 +1388,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. unfiltered, and provider resolution errors follow `authorization.fail_closed`. Admin management endpoints stay `require_admin_user`-gated; runtime activation authorization remains #4541's layer. ([#5489]) + - **middleware:** Add deterministic, opt-in PII redaction for model-bound context (`pii_redaction.enabled`, default off). `PiiRedactionMiddleware` rewrites PII in genuine user messages and remote-content tool results (same allowlist as the @@ -1130,6 +1399,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. per model call with value-deduplicated counters), externalized tool-output copies hold redacted text too, and subagents inherit the middleware automatically. Raw text stays in thread state; default behavior is unchanged. ([#5527]) + - **frontend:** Gate composer send on the `runs:create` permission. Both chat routes resolve `canCreateRuns` and the shared composer checks it at the top of `submitThreadMessage`, the single choke point for the submit button, Enter-to-send, @@ -1138,6 +1408,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. draft text kept for retry; while streaming the button remains the `runs:cancel` stop affordance as before. Absent or null permission lists stay permissive for mixed deployments. Mirrors the stop gating from #5294. ([#5528]) + - **guardrails:** New opt-in TypeSafe (Jev) risk gate for tool calls, `deerflow.guardrails.typesafe:TypeSafeGuardrailProvider`, selected through `guardrails.provider.use`. Each probed call sends one yes/no question with the @@ -1148,6 +1419,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. network requests; provider errors follow `guardrails.fail_closed`. This is the only guardrail provider that sends tool arguments to a third party — assess egress before enabling. `config_version` moves 46 → 47. ([#5712]) + - **authz:** Gate plugin actions and management operations through the configured authorization provider. `POST /api/plugins/{ns}/actions/{name}` now requires a `plugin_action`/`invoke` decision on `/`, checked @@ -1208,6 +1480,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `/api/plugins`, and the frontend loads plugin pages, workspace pages and conversation actions at runtime. The `deerflow-extension-bookmarks` example exercises the path end to end; no plugin ships enabled by default. ([#5647]) + - **plugins:** Support manifests and static asset directories for full-stack plugins. `BrowserAssets(module, root, manifest="ui_manifest.json")` (extension-api 0.2.3) joins `BrowserModule`: a versioned manifest names the @@ -1216,6 +1489,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. policy, and immutable caching. Packaged entries load as native credentialed module graphs, so a plugin can split its JavaScript and resolve CSS/images with relative imports; existing inline modules keep working. ([#5685]) + - **extensions:** Extension routes can request a run-evidence reader bound to the authenticated request user. New `resolve_run_evidence_reader(request)` / `require_run_evidence_reader(request)` APIs return an immutable user-scoped @@ -1223,6 +1497,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. scope-bound cursor validation preserved. The existing global reader stays available to trusted Gateway-lifetime services. `deerflow-extension-api` is bumped to 0.2.2. ([#5727]) + - **plugins:** New standalone `deerflow-extension-jev-context` example plugin (`community.jev-context`) prunes stale tool results from long conversations. Through the lead agent's `before_model`/`abefore_model` hooks it asks Jev @@ -1232,6 +1507,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. environment-only Jev key; failed requests preserve the original history. Pruning is lossy and persists in graph state — disabling the plugin does not restore omitted text. ([#5731]) + - **plugins:** New standalone `deerflow-extension-jev-classify` example plugin (`community.jev-classify`) contributes a `classify_texts` model tool that labels `{id, text}` items against 2-32 caller-supplied categories and returns @@ -1240,6 +1516,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. endpoint asked for a validated JSON label list. Calls are batched (default 10 per request) with bounded concurrency and a 25s deadline; failures are explicit per item, and nothing is retried. ([#5735]) + - **extensions:** Packaged extensions can now invoke the host model under operator-granted limits. `plugins[].host_access.model_invocation` maps logical roles to models with shared per-installation concurrency, bounded @@ -1248,6 +1525,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the host model factory and tracing with inline Draft 2020-12 schema validation in child processes; ungranted extensions still get `None`, and grants follow the restart-required plugin lifecycle. ([#5796]) + - **examples:** Add `deerflow-extension-jev-screening`, an opt-in packaged extension that flags injected instructions in fetched content as advisories. A `TOOL_VISIBLE` middleware classifies each text message in visible @@ -1321,6 +1599,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. them as resume targets. The resume head, its ancestor chain, and checkpoints with live pending-writes rows are protected. No production trigger yet — the invocation wiring lands separately. ([#5308]) + - **storage:** Add a content-addressed blob store contract (`deerflow/storage/`), the storage-abstraction half of #4189 item 2. `BlobRef` (sha256, size, kind, content type) plus a tiered `BlobStore` interface ship with a `local_fs` default @@ -1386,7 +1665,6 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. latest turn's edit pencil is shown disabled with a tooltip instead of hidden, and the message toolbar also appears on keyboard focus. A `/goal` refused because a run is still going keeps the draft and says so. ([#6556]) - ### Changed - **tool output:** Superseded `write_file` payload elision is now opt-in. The @@ -1399,9 +1677,24 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and host authorization already permit, including when the declaration is empty. Parsing, validation, export, skill-activation permissions and the existing secret and user isolation boundaries are retained. ([#6935]) - ### Fixed +- **projects:** Drain the staging discard in `_convert_and_publish` through + `await_drained`, so repeated cancellation of the reader no longer abandons + the unlink worker while it still holds single-flight ownership, and route + `_remove_staging` through `cleanup_failed_atomic_write` so best-effort + cleanup warnings never replace the conversion's outcome — result, original + error, or cancellation. A leftover staging file stays for the retention + sweep, which retries it after its existing age guard. ([#7659]) + +- **events:** Isolate `MemoryRunEventStore` records from callers by deep-copying + `content` and `metadata` on write and returning detached snapshots from `put`, + `put_batch`, `put_if_absent`, `list_messages`, `list_events`, and + `list_messages_by_run`. Previously a caller that mutated a write input or a + returned record — including nested values — changed stored events and all + later reads; the `RunEventStore` contract now documents the snapshot + guarantee so every backend must detach records too. ([#7423]) + - **benchmarks:** Reject negative and NaN sandbox idle-timeout values before constructing any provider, and apply the same validation to BoxLite/AIO health-check skip windows. Valid zero, positive, and positive-infinity values @@ -1410,11 +1703,46 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. that its provider consumes them. The idle-reaping scenario rejects infinity before provisioning because it must sleep past the configured timeout. ([#7651]) +- **runs:** Drop a run's membership in the prior thread's `_runs_by_thread` + bucket when a `MemoryRunStore.put` replacement snapshot carries a different + `thread_id`, pruning the bucket when it empties before indexing the run under + the new thread. Previously the run stayed indexed under the old thread too, + so per-thread queries and thread cleanup saw it in both. The move happens + only after the replacement snapshot is built, so a failed copy leaves the + prior row and its index intact. ([#7622]) + - **tui:** The `/threads` and `/model` pickers show thread titles and model names exactly as written. Labels were parsed as Textual markup, so a title such as `Fix [/bold] in parser` exited the TUI with `MarkupError` whenever the picker opened, and `[a]` vanished from a title. ([#7631]) - + +- **docker:** Pin the nginx image to `nginx:1.31.2-alpine` in both + `docker/docker-compose.yaml` and `docker/docker-compose-dev.yaml` instead of + the floating `nginx:alpine` tag. From 1.31.3 the alpine builds ship musl + 1.2.6, whose `pwrite()` issues `pwritev2(RWF_NOAPPEND)`; the default seccomp + profile of older Docker releases (and EOL kernels such as CentOS 7) does not + allowlist it, so the master died with `pwrite() "/run/nginx.pid" failed + (1: Operation not permitted)` and the container restart-looped, leaving + `make up` / `make docker-start` never healthy. 1.31.2 is the last alpine + build on musl 1.2.5, whose `pwrite()` still uses `pwrite64`; a regression + test now rejects any floating nginx tag in either compose file. ([#7643]) + +- **mcp:** Raise `NotImplementedError` naming the MIME type when an MCP tool + result contains an inline `AudioContent` block in `_convert_call_tool_result`. + Previously such blocks fell through to the catch-all text conversion, so the + audio's Python repr — including its base64 payload — was stringified into + model-visible tool text; text, image, resource-link, and embedded-resource + blocks convert as before. ([#7628]) + +- **skills:** Read the trigger evaluator's child stdout through EOF instead of + stopping at process exit in `run_single_query` + (`skills/public/skill-creator/scripts/run_eval.py`), parsing each line as it + arrives and appending a newline at EOF so a final JSON event without a + trailing newline still parses. Previously output still buffered when the + child exited was appended to the buffer but never parsed, so a completed + run's closing `result` event was dropped and the query scored as + untriggered. ([#7043]) + - **channels:** DingTalk AI cards no longer fetch an access token on the Gateway event loop. Card creation and card streaming went through dingtalk-stream's synchronous `get_access_token()`, which posts with @@ -1429,7 +1757,26 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. working Node/npm toolchain now use the existing link-preserving Python fallback on later fetches without repeating the failed subprocess probe; document-specific non-zero exits still retry Node on later pages. ([#5973]) - + +- **artifacts:** Parse `write-file:` URLs through the exported guarded helper + `parseWriteFileArtifact` in `ArtifactFileDetail` and + `loadArtifactContentFromToolCall` instead of a bare `new URL` plus + `decodeURIComponent`. The panel restores its selection from session storage + with only a string type-check, so a stale or hand-edited URL such as + `write-file:%E0%A4%A` threw `URIError` (or `TypeError` for + `write-file://[`) during render; a rejected URL now degrades to the raw + string, and content loading returns `undefined`. ([#7609]) + +- **readability:** Probe Node-based Readability extraction once and remember + the answer: `ReadabilityExtractor.extract_article` consults readabilipy's + `have_node` under a lock, caches the result, and treats a missing Node + executable (`FileNotFoundError`) as environment-level by setting + `_readability_js_launch_failed`, so later extractions go straight to the + pure-Python fallback. Previously every fetch paid the failing subprocess + launch and warning before falling back; document-specific non-zero exits + (`CalledProcessError`) keep Node enabled since only the current document + failed. ([#7617]) + - **scripts:** `make dev` and `make start` no longer run the checkout `.env` as a shell script. `serve.sh` sourced it, so `$$` became the shell's PID, backticks and `$(...)` ran commands, and an unquoted value with a space ran @@ -1445,7 +1792,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. streaming) resolved the virtual path (`realpath`), resolved the outputs root and checked the file with `stat` inline, stalling other channels and requests on network-backed storage. They now do it in a worker. ([#7594]) - + - **channels:** A streamed Feishu reply no longer rewrites the same thread bindings on every chunk. Each outbound to the running card stored all of the reply's aliases again (about five `set_thread_id` writes per chunk: a @@ -1453,7 +1800,48 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. although nothing but `updated_at` changed. Non-final chunks now store each alias once per reply, a failed write is retried on the next chunk, and the final outbound still writes them all. ([#7593]) - + +- **skills:** Parse SKILL.md frontmatter with `yaml.safe_load` in the + skill-creator's `parse_skill_md` (`scripts/utils.py`), matching the + semantics of `quick_validate.py` and the runtime loader, instead of a + hand-rolled line parser that mangled escaped quotes, comments, and block + scalars. Invalid YAML, a non-mapping frontmatter, or non-string + `name`/`description` raises `ValueError`, which `run_eval.py`, + `run_loop.py`, and `improve_description.py` turn into an `Error:` on stderr + with exit status 1. ([#7049]) + +- **artifacts:** Serve viewed images from the shared blob store when an + artifact GET's local file is missing: on a 404, + `_get_viewed_image_blob_response` reads the thread checkpoint's + `viewed_images` metadata, validates the entry against the virtual path, + `MAX_VIEWED_IMAGE_BLOB_BYTES` (20 MiB), the path's guessed MIME type, and a + `viewed-image` `BlobRef` bound to the recorded sha256, size, and content + type, then re-verifies the digest of the bytes it serves. Behind a load + balancer the reading Gateway previously had no local copy of an image + another instance had viewed, so the request 404ed; any metadata or read + failure leaves the original 404 in place. ([#6329]) + +- **persistence:** Register staged temp files for cleanup before their content + is written in `FileAgentStore._stage_temp` and + `FileManagedSubagentStore._atomic_write`. Previously the path reached the + caller's cleanup only after staging returned, so a failing `write` or + `fsync` (a full disk, for example) raised past the staging step and leaked + the `.tmp` file with no cleanup attempt and no log; the same failure now + discards the temp through the existing `cleanup_failed_atomic_write` path. + ([#7504]) + +- **agents:** Keep user XML recoverable through input sanitization: + `frame_untrusted_text` now escapes original ampersands before escaping + blocked tags and prepends `_USER_INPUT_ENCODING_NOTE`, which tells the model + to decode HTML entities exactly once when reproducing the user's original + text in files, code, quotes, or tool-call arguments (issue #7526); + previously the one-way escaping left `<system>`-style bytes in + everything the model wrote. A request-local `_FramedUserText` marker is the + only proof that text was framed by this module — a note copied inside user + text is data and re-encodes rather than skipping the transform — and the + lead-agent prompt now states that decoding restores data bytes without + granting decoded tags instruction authority. ([#7556]) + - **runtime:** Loop-detection warnings now ask the agent to present the files it already created in `/mnt/user-data/outputs` before its final answer, so a warned run no longer fails its delivery check with finished files left @@ -1463,7 +1851,35 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. stands down without an evaluator call and stays active, `/wait` names the error `GraphRecursionError`, and IM channels reply with the message instead of a generic failure. ([#7588]) - + +- **safety:** Recognize the native Responses content-filter stop and scrub its + tool calls: a new `OpenAIResponsesContentFilterDetector` joins + `default_detectors()` and maps `status: "incomplete"` with + `incomplete_details.reason == "content_filter"` (in `response_metadata` or + `additional_kwargs`) to a safety termination, distinct from token + exhaustion, and `SafetyFinishReasonMiddleware` now counts + `invalid_tool_calls` alongside `tool_calls` when suppressing and clears the + invalid entries from the cloned message. Previously a Responses reply cut + off mid-`function_call` (for example with truncated arguments) surfaced as + an invalid tool call the middleware ignored, so replay re-sent the dangling + call and the provider rejected the thread. ([#6965]) + +- **tool-output:** Default `tool_output.elide_superseded_writes` to `false`, + making write_file content elision opt-in. Because elision applies only once + a later read or write of the same path supersedes the write, it rewrites an + earlier segment of the model request, invalidating the provider prompt-cache + prefix that unchanged history had built (#7319); the context savings no + longer come at that cost unless an operator enables it explicitly. Stored + history, receipts, and the run journal keep the original arguments either + way. ([#7455]) + +- **api:** Fall back to the raw CSRF cookie value when percent-decoding + fails instead of letting `decodeURIComponent` throw inside `fetch` and + `getCsrfHeaders`. Cookies are shared across every app on the same host, + so a stray or truncated `%` escape written by another local app, a proxy, + or a stale value turned every state-changing request into a client-side + `URIError` before the gateway could answer 403. ([#7589]) + - **channels:** Connecting Slack, Discord, Feishu, DingTalk, WeCom, Buzz or QQ now opens a dialog that keeps the `/connect` command on screen with a copy button, its expiry time and the binding status, instead of a toast that @@ -1472,6 +1888,56 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. reports whether the goal is in progress, why it stopped, or that it was met, as the web goal bar does. ([#7591]) +- **skills:** Keep a failed temporary-file discard from replacing an + already-decided skill publication outcome. After the validated directory + descriptor commits, `replace_main` removes its `.host-mutation-*` temporary; + that cleanup previously tolerated only `FileNotFoundError`, so any other + `OSError` propagated and masked the publish result or turned a decided + outcome into a cleanup failure. The unlink failure now logs a warning, and + a leftover temp is still caught later by `assert_temporary_absent`. ([#7586]) + +- **sandbox:** Translate virtual and host roots only where they start a + path, not inside a longer path or URL. Translation previously checked only + the right-hand segment boundary, so `/mnt/user-data` embedded in an artifact + URL had the thread's host directory spliced in, and a mount at `/data` + rewrote `/var/data/app.log` to `/var/mnt/data/app.log` in model-visible + output. A shared `PATH_START_BOUNDARY` lookbehind, derived with the scanner + from `_PATH_START_BLOCKERS`, now rejects a root preceded by a word + character, `.`, `~` or `-` in the bash command rewrite, `LocalSandbox` + command/content resolution, and both reverse masking paths; `/` stays + allowed so `file:///mnt/user-data` keeps mapping. ([#7582]) + +- **checkpoints:** Validate cached checkpoint history before replaying its + pending writes. `valid_history_entry` now requires each write to be a + `(str, str, ...)` triple and, on `CachedHistorySaver` reads, to belong to + the requested channel; the previous shape check stopped at a list-typed + `writes` field, so a pending write stored as a three-character string + unpacked during delta materialization and silently invented a channel + value. Entries that fail the check are a miss on read and skipped on + write, so the authoritative saver rebuilds the history, and cache + diagnostics go through a guarded `_warn` so a logging failure cannot + change fallback behavior. ([#7548]) + +- **docker:** Bundle the Lark CLI broker's stdlib-only helper modules in + the sidecar image. The image previously copied only `lark_broker.py`, but + that module imports `cleanup_failed_atomic_write` from + `deerflow.utils.atomic_write`, so the standalone broker failed at import + without a harness installation; the Dockerfile now also copies + `atomic_write.py` and `file_revision_metadata.py` into + /opt/broker/deerflow/utils/. ([#7542]) + +- **streaming:** Make Redis stream END publication idempotent in one Lua + script. Owner and recovery retries now confirm an existing END marker + instead of appending a second one, events cannot be appended once END is + the stream tail, and an unowned recovery END for a cleaned-up stream is a + successful no-op instead of an error. Ordinary events check only the tail + (`XREVRANGE` with `COUNT 1`) to stay O(1); the new + `stream_bridge.legacy_end_scan_enabled` (default `true`, restart-required) + additionally scans the whole retained window when the tail is not END, + covering buffers written by the old writer, where END can precede late + data, and should be set `false` only after old writers stop and legacy + buffers drain. ([#7572]) + - **community:** Async community tools (Jina, Crawl4AI, Browserless, Tavily, SearXNG, Firecrawl, InfoQuest, RAGFlow, LightRAG, Jsonify and the browser automation tools) no longer read `config.yaml` and `extensions_config.json` on the @@ -1487,6 +1953,42 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. rejects. The Anthropic examples in `config.example.yaml` and the configuration docs follow the same model.([#7585]) +- **i18n:** Stop a malformed locale cookie from breaking locale bootstrap + and `formatTimeAgo`. `getLocaleFromCookie` previously decoded the raw + value unguarded, so malformed percent-encoding threw `URIError`, and + split each cookie on every `=`, truncating values that contain one; it now + splits on the first `=` only and returns the raw value when decoding + fails, which `normalizeLocale` maps to the default locale. `formatTimeAgo` + also normalizes the cookie locale instead of casting the raw value to + `Locale`, so a malformed-but-known-prefixed cookie no longer skips + normalization. ([#7553]) + +- **setup-agent:** Keep a failed temporary-file discard from replacing the + publish outcome of the `setup_agent` SOUL.md write. `_publish_text` + removed its staging temporary in a `finally` with `unlink(missing_ok=True)`, + so an `OSError` from that cleanup propagated and masked the publish + result; the discard now goes through `cleanup_failed_atomic_write`, which + keeps the original error and logs the leftover temp. ([#7564]) + +- **subagents:** Drain the subagent final cleanup tail across caller + cancellation. Sandbox lease release and extension task-stop notification + previously ran as plain awaits, so a cancelled caller skipped the + remaining steps, and lease release or task-stop ownership could unwind + half-finished; the tail now runs through `await_drained`, which keeps + capacity and execution-done ownership until the cleanup settles before + letting the cancellation through. The synchronous timeout path also + always shares a result holder with the worker and publishes its FAILED + outcome before requesting cancellation. ([#7546]) + +- **skills:** Bound Gemini video operation polling in the public + `video-generation` skill. `_generate_video_gemini` previously polled the + long-running operation in an unbounded `while True` loop, so one that + never completed polled forever; the new `_poll_gemini_operation` caps + polling at 240 attempts of 3 seconds (12 minutes) and surfaces an + operation error, a completed operation with no generated sample (naming + `raiMediaFilteredReasons`), or a sample without a video URI as diagnosed + failures instead of hanging. ([#7579]) + - **auth:** The public auth routes (`/register`, `/setup-status`, `/providers`, `/oauth/{provider}` and `/callback/{provider}`) no longer read `config.yaml` and `extensions_config.json` on the event loop. Each request @@ -1494,7 +1996,27 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `config.yaml` there after an edit, stalling other requests and streams meanwhile. The read now runs in a worker, as `/login/local` already did. ([#7576]) - + +- **skills:** Keep a secret required when an optional duplicate declaration + precedes it. `parse_required_secrets` dropped every declaration of a name + after the first, so an earlier `optional: true` entry hid a later required + one and the skill silently ran without the secret; duplicates now keep + the first position and a required declaration upgrades the entry to + required. The export manifest applies the same merge rule and reads + `optional` fail-closed (`is True`), matching the parser's treatment of + non-boolean values as required. ([#7569]) + +- **tests:** Isolate backend tests from ambient host configuration and + nested Make controls. Parallel-make coverage strips the outer runner's + `MAKEFLAGS`, `MAKELEVEL`, `MAKEOVERRIDES`, `TEST_JOBS` and shard + variables so the copied project exercises its own defaults; mounted-grep + tests reset the `_get_custom_mounts` cache to resolve the fixture's + mounts; OpenViking manager tests run against an isolated CLI config and + loopback server with ambient `OPENVIKING_*` variables cleared; the + Postgres takeover fence test skips before importing optional drivers. + New coverage pins that the compaction flush hook captures nothing when + memory is disabled. ([#7567]) + - **sandbox:** Stopping the Gateway no longer destroys AIO or E2B sandboxes before in-flight runs drain. When the first sandbox use built the provider on the event-loop thread, the provider's own SIGTERM/SIGINT handler destroyed @@ -1505,6 +2027,17 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. still cleans up. AIO acquire now refuses once shutdown has begun, as E2B already did. ([#7560]) +- **memory:** Deduplicate Mem0 context records inside the read-policy + boundary. Context injection deduplicated after `list_memories` returned, + so a remote record with an unhashable `id` (a list or object) raised a + raw `TypeError` outside the policy; the new + `_deduplicate_context_records` runs within `_read_or_fallback` and reports + the unusable ID as a value-redacted `Mem0APIError`, letting + `failure_policy.read` decide the outcome: `fail_open` falls back to empty + context and `fail_closed` raises `MemoryReadError`, with no partial + context even when the invalid ID appears on a later page. First-occurrence + ordering and character budgets are unchanged. ([#7561]) + - **frontend:** SSO sign-ins now return to the page that asked for them, such as a shared chat link, instead of always opening the default workspace. The sign-in pages used to send every signed-in request to `/workspace`, which @@ -1513,7 +2046,18 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `next` checks now also reject control characters: browsers delete tab, LF and CR from a URL, so `?next=%2F%0A%2F%2Fevil.example` could send a sign-in to another site. ([#7467]) - + +- **persistence:** Keep LF bytes in skill and agent text writes on every + host. Text mode translates newlines to `os.linesep`, so on Windows every + caller-supplied LF reached disk as CRLF while readers saw LF; a skill + history record's `prev_content` described a revision no API view could + return. Ten writers that persist or judge caller text now pass + `newline=""`: the local and user-scoped skill writers, the skill history + JSONL and skill-states JSON, `FileAgentStore` SOUL.md/config.yaml + staging, the managed-subagent store, setup_agent's SOUL.md publish, the + USER.md route, and the temp copies the frontmatter validator and static + scanner judge. ([#7501]) + - **authz:** Authenticated Gateway requests no longer read `config.yaml` and `extensions_config.json` on the event loop. Resolving route permissions checked both files (a `stat` and a full sha256 of each) for every request, @@ -1521,6 +2065,61 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. streams meanwhile. The read now runs in a worker, as the plugin authorization path already did. ([#7557]) +- **subagents:** Report active cancellation to subagent lifecycle + observers. An `asyncio.CancelledError` previously bypassed the terminal + branches of async execution, so task-stop hooks ran before any status was + published and observers never saw the abort; the executor now publishes + `CANCELLED` (surfacing as `aborted` to task-lifecycle hooks) with the + collector's final usage and tool receipts after stream teardown, then + propagates. Synchronous timeouts still report FAILED: `execute()` shares + one result holder with the worker and publishes the failure before + requesting cancellation, `execute_async()` publishes `TIMED_OUT`, and the + first terminal write wins, so a late cancellation cannot overwrite a + timeout. ([#7483]) + +- **client:** Keep a decided upload outcome when a conversion capture cannot be + discarded. `upload_files` stages convertible sources in a + `deerflow-upload-source-` temp directory and deletes each capture as it + goes; on Windows a capture still open at that point made the plain `unlink` + raise `PermissionError`, turning an unsafe-destination skip, or an upload + that had already succeeded, into a failure for the whole batch. Per-file + discards and the `finally` directory cleanup now go through + `cleanup_failed_atomic_write` and `ignore_cleanup_errors=True`, so later + files still upload and the reported `skipped_files` and `files` lists + stand. ([#7517]) + +- **utils:** Build one-shot chat models off the event loop. + `run_oneshot_llm_detailed` called `create_chat_model` inline, so provider + construction, which can read local credential files, ran on the loop; it + now resolves the model through `run_assembly` on the dedicated assembly + pool before awaiting inference, preserving request context. The utility + guidance records the offload contract for the input-polish, + follow-up-suggestion, and project-file-summary callers, with the + standalone skill-moderation and goal-evaluator constructors as separate + sites outside it; coverage is + `tests/blocking_io/test_oneshot_llm_offloop.py`. ([#7484]) + +- **channels:** Report failed attachment delivery in the same conversation. + `Channel._on_outbound` previously logged skipped or failed `send_file` + attempts without telling the user. Failed filenames now get a locale-aware + reply (built by `attachment_delivery_text`, with sent/failed counts and no + host paths or provider errors) sent as a plain follow-up message, and a + channel still on the base `send_file` implementation now says attachments + are unsupported and points to the DeerFlow conversation instead of + silently dropping them. WeCom replies on the original WebSocket frame and + clips the notice to `_WECOM_MAX_CONTENT_BYTES`, skipping the feedback when + the reply context has expired rather than falling back to the proactive + API. ([#7532]) + +- **skills:** Exclude pull requests from GitHub issue research. The + `github-deep-research` skill's `get_issues` returned the raw GitHub issues + endpoint response, which includes pull requests, so PRs showed up in issue + research results; items carrying a `pull_request` key are now filtered out + and the client follows additional pages until it fills the requested + limit, with per-page requests capped at 100 and a non-positive limit + returning nothing. SKILL.md now states that `issues` returns repository + issues only and directs pull-request research to `prs`. ([#7516]) + - **memory:** Automatic context compaction no longer blocks the event loop while it saves the messages it is about to summarize to memory. With the mem0, Honcho or OpenViking backend, that save is a synchronous HTTP request, @@ -1535,12 +2134,329 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. examples set it to `100`, so delete that line from `config.yaml` to follow the top-level value. GitHub channel runs are unchanged. ([#7521]) +- **gateway:** Admit cross-process runs when a producer lease fails to start. + `start_run` treated `start_producer_lease` returning False on a + cross-process bridge as fatal and raised, surfacing a 500 for an otherwise + healthy run whenever the backend (Redis) had a momentary blip, even though + the bridge rolls back the partial lease registration and leaves nothing + tracked. Admission now logs a warning and proceeds: an untracked producer + falls back to the historical conservative recovery path of waiting for the + real END or the retained-stream TTL instead of a synthesized one, and the + worker re-attempts its own lease independently. ([#7136]) + +- **projects:** Delete read-only documents when purging trash. On Windows the + read-only attribute makes `Path.unlink` fail with `PermissionError` + (`WinError 5`) where POSIX needs only a writable parent, so a restored + archive or a mode-preserving sync left a permanently deleted document + unpurgeable: the purge re-raised, the row deletion rolled back, and the + trashed document came back with the same error on every retry. The purge's + `_unlink_document_file` now clears the attribute (`chmod` with + `S_IWRITE`, skipping symlinks) and retries the unlink once, mirroring + `config/paths.py::_clear_read_only_tree`; `FileNotFoundError` still counts + as already removed and any other error propagates so the row stays + retryable. ([#7499]) + +- **frontend:** Count code points when previewing table cells and memory + facts. `artifact-table-preview` cut a cell at 120 UTF-16 code units + (`value.slice(0, 120)`) and gated the expand control on `value.length`, + and `truncateFactPreview` on `memory-settings-page` measured and cut fact + previews at 140 units, so an emoji spanning two units could be split at + the cut and render as U+FFFD, and a cell that already fit the + 120-code-point budget still opened the expand control. Both previews now + share `truncateToCodePoints` in `core/utils/text`, which measures and + cuts by code points (matching `briefReason` in + `core/scheduled-tasks/format.ts`), and the expand gate counts + `Array.from(value)`. ([#7503]) + - **sandbox:** The AIO provider's async acquire no longer checks the thread's skill directory on the event loop. Picking the sandbox ID ran a filesystem `stat` and path resolution inline on every thread acquire, which stalls the loop when `DEER_FLOW_HOME` is on slow or network storage. It now runs in a worker like the rest of acquire. ([#7507]) +- **runtime:** Backfill a terminal event after a confirmed recovery takeover. + A takeover committed `status=error` on the Run row but wrote no matching + `run.terminal` evidence. The new `RecoveryTerminalPublisher` converts a + committed takeover claim into a one-shot `RecoveryTerminalTarget` and + publishes one error terminal marked `recovered`, best-effort backfilling + the zero-delivery `run.delivery` receipt `RunManager` used to write. + Publication is fenced against Run deletion and identity drift + (`RunEventLifecycleRejected` maps to `CONFLICT`); DB stores evaluate the + guard in the active event session to avoid a second pool checkout, the + memory store fences guarded writes and deletions with per-thread locks, + and publication failures are logged while the committed RunRow stands; + scheduled-task run reconciliation publishes the same targets. ([#7500]) + +- **projects:** Make `read_file_chunks` cancellation-safe around its + file-I/O worker handoffs. Cancelling a project document read had three + unsafe windows: a cancellation landing between the open worker completing + and the result reaching the coroutine leaked the handle, the cleanup close + could start while an admitted read was still running, and a repeated + cancellation let the caller escape before the close worker settled. + Admitted open/read/close operations are now gated with `await_drained` so + cancellation is delivered only after the operation settles, the opened + handle is retained in a list cell the moment the worker returns it (the + cleanup close always has the handle, and an open failure surfaces + `FileNotFoundError` unchanged), and waiting for the next chunk via the + `yield` stays immediately cancellable. ([#7496]) + +- **persistence:** Treat a peer's database creation as success during + PostgreSQL auto-create. Two Gateways booting the same missing database + both reach `CREATE DATABASE` in `_auto_create_postgres_db` and only one + wins; the loser aborted startup with `duplicate_database` (`42P04`) even + though the winner's catalog row was committed and the target was usable. + The helper now checks the SQLSTATE through the shared `_has_sqlstate` + walker (following `orig`, `__cause__`, and `__context__`, like the + missing-database `3D000` check beside it) and logs that the database was + created by a peer process before falling through to its retry; every + other failure, permission denied included, still propagates + untouched. ([#7489]) + +- **scheduler:** Keep manual-trial launch origins stable across retries. A + manual trial's synthetic queued row omitted `scheduled_for`, and + `_launch_provenance` used the current clock for manual triggers, so a + queue-drain retry at a later time derived a different origin. That origin + is part of `start_run`'s per-occurrence idempotent input: the retry either + conflicted with the first attempt or submitted a different request. The + synthetic row now carries the admitted `scheduled_for`, and + `_launch_provenance` reads the occurrence time for every trigger, falling + back to `now` only when a legacy caller supplies none. ([#7492]) + +- **gateway,integrations:** Keep temp cleanup from replacing the caller's + outcome. The artifact router's `_replace_artifact_atomically` closed the + staging descriptor and unlinked the temp file in a `finally`, so a locked + or already-removed temp turned a published artifact edit into a cleanup + error, and the Lark CLI's `_write_json_file_atomic` did the same with a + bare `unlink`. Both now use `cleanup_failed_atomic_write`, which keeps + the published case silent, warns when a leftover temp cannot be removed, + and never lets a cleanup failure replace the write's result. ([#7505]) + +- **frontend:** Allow non-ASCII local parts in the auth password forms. The + sign-in, register, admin-setup, and change-password forms relied on the + browser's `type="email"` constraint validation, which rejects any + non-ASCII local part before the submit handler runs, so an account + created through the API, an administrator, or OIDC (the backend identity + schema deliberately accepts such addresses) could never sign in through + the password form. The forms are now `noValidate` and run the permissive + `isValidEmail` helper from `core/auth/email` in their handlers (exactly + one `@`, non-empty local part and domain, no whitespace, strictly weaker + than what the backend accepts), submit the trimmed address, and restore + the JS password minimums (6 for sign-in, 8 for register) that dropping + native validation would have lost. ([#7498]) + +- **uploads:** Invalidate edited markdown companions. Companion ownership + recorded the Markdown's device, inode, size, and ctime only, so on Windows + an in-place edit that kept the same size (ctime is the birth time there) + still validated and served the modified file as the converted companion. + `markdown_identity` now also carries `st_mtime_ns`, so an edited companion + fails `resolve_companion` validation; legacy four-field records fail + closed with their own debug diagnostic rather than being backfilled, and + re-uploading or re-converting registers a fresh association. These + metadata checks detect ordinary edits and do not guarantee content + integrity against a writer that can restore timestamps. ([#7348]) + +- **checkpoints:** Isolate cached history payloads from state readers. The + memory checkpoint cache's `_copy_entry` shared the snapshot seed between + the cache and its readers (copying only the writes list), so a reader + mutating a nested message payload poisoned the seed later used to + materialize checkpoints. Entries are now deep-copied on both ingest and + read, with copies made outside the registry lock because user payloads + can implement `__deepcopy__` that re-enters the cache, and `set_many` + checks `enabled` before copying. Deep-copying whole entries is + synchronous on the caller's thread (including `aget_many` on the event + loop), with cost documented as growing with payload size and O(N²) + across a warm scan of N checkpoints. ([#7259]) + +- **frontend:** Docs search now works on desktop and mobile. The build never + generated the Pagefind index that Nextra's `` loads, and the custom + docs header rendered no search box: `pnpm build` now runs `search:index` + (pagefind 1.5.2, pinned) to write `public/_pagefind/`, and the landing + header and its mobile sheet take an optional `search` slot that only the + docs layout fills. Each page's `` is rewritten from its + `/{lang}/` route before indexing (the root layout pins one lang, so zh + pages were tokenized as English and multi-character words never matched) + and `DocsHtmlLang` syncs the live `` so queries hit the right + index; the results panel is restyled with app tokens and kept above the + mobile sheet, which now closes after a result navigates instead of before, + no longer reopens on Back, and no longer competes with a second offscreen + `` for Cmd/Ctrl+K. ([#6715]) + +- **sandbox:** `write_file` now rejects a payload that is the tool-output + elision placeholder and returns an error telling the model to `read_file` + the target path and write the real content; nothing is written, and both + the sync and async tools preflight the check. That placeholder is what the + budget middleware substitutes for a superseded write payload in the + model-bound request, so the model could copy it out of its own history and + submit it as content, silently replacing a real deliverable with a ~200-byte + placeholder (#7319). The match is whole-payload (surrounding whitespace + allowed) so payloads that merely quote the wording still write, and the + template moves to `WRITE_ELISION_PLACEHOLDER_TEMPLATE` in + `deerflow.constants`, shared by the middleware and the guard so the two + cannot drift. ([#7374]) + +- **skills:** Presentations generated by `ppt-generation` now honor photo + orientation: `generate.py` runs `ImageOps.exif_transpose` on each slide + image before sizing and embedding it, so shots carrying EXIF orientation + metadata no longer land sideways or mirrored in the deck (PIL ignores EXIF + orientation when decoding). Transparent images keep their alpha channel as + before. ([#7193]) + +- **config:** Atomic-write cleanup in config storage no longer masks the + caller's outcome: `ManagedModelStore`'s secret persistence and + `secret_file.py`'s `_create_exclusive`/`_publish_under_claim` discard + temporaries and unpublished claims through `cleanup_failed_atomic_write` + instead of `Path.unlink(missing_ok=True)`. Previously an unlink failure in + a `finally` block raised over the real result — replacing the error that + aborted the write, or the success of a publication, with a cleanup + exception. Cleanup failures are now logged and suppressed on every exit + path. ([#7279]) + +- **persistence:** Staged-write cleanup in the file-backed agent + and managed-subagent stores is now best-effort: `FileAgentStore` and + `FileManagedSubagentStore` discard staged temporaries through + `cleanup_failed_atomic_write` instead of `Path.unlink(missing_ok=True)`, so + an unlink failure during the `finally` cleanup can no longer raise over the + error that aborted the publish — or over a successful one — and is logged + and suppressed instead. ([#7485]) + +- **middleware:** Restored tool outputs keep ordinary file-creation + permissions: when the budget middleware recovers an externalized output + from the blob store onto a Gateway that lost its local copy, + `_write_restored_output` now creates the temporary sibling with exclusive + `open(..., "xb")` instead of `tempfile.mkstemp`, so the restored file + follows the same `0o666 & ~umask` policy (plus filesystem default ACLs) as + initial publication rather than landing at fixed `0600` that a mounted + sandbox reader under a different UID could not read. Digest-matching local + files are still reused without touching their permissions. ([#7390]) + +- **models:** `PatchedChatDeepSeek` now preserves Moonshot's cache-creation + token count: `_create_chat_result` and, for the streamed `messages-tuple` + path, `_convert_chunk_to_generation_chunk` restore the raw SDK + `prompt_tokens_details.cache_write_tokens` as + `input_token_details["cache_creation"]` on the final usage chunk. The + locked `langchain-openai` 1.2.1 conversion reads `cached_tokens` (mapped + to `cache_read`) but ignores `cache_write_tokens`, so providers such as + Kimi K3.1 lost the count before it reached the usage journal + (#7426). ([#7428]) + +- **frontend:** Chat steps now surface tool outcomes. A tool call whose + result message reports an error — via `status === "error"` or + `deerflow_tool_meta.status === "error"`, classified by the new + `core/messages/tool-error.ts` helper — gets a destructive "Failed" badge + on its step label instead of hiding the failure inside the collapsed + details panel, and the Debug details payload label uses the same + classifier. Scheduled-task steps show a pending label ("Scheduled task + operation" / "Pause this scheduled task") until a valid object response + arrives rather than announcing completion when the request is issued, and + missing, non-object, or `error`/`code`-bearing responses fall back to the + failed label. ([#7465]) + +- **utils:** The five timestamped fallbacks in `output_name_candidates()` are + now always distinct: the UTC clock is sampled once when the fallback list + is reached and each attempt offsets it by one microsecond, instead of + calling `datetime.now(UTC)` per yield. On a host clock coarser than the + microsecond stamp (such as Windows' ~15 ms system-time tick) all five + candidates previously repeated the same name, so capture tools lost their + retries and failed with `could not reserve an output filename`. ([#7071]) + +- **memory:** Out-of-range numbers in a model's memory-update response no + longer fail the whole normalization batch in the DeerMem updater. A + non-finite `extend_by_days` (`1e400` parses to `inf`; `NaN` also appears) + previously raised in `int()` and dropped every valid fact in the response — + such entries are now dropped individually, while unbounded integers beyond + float range pass through as ints for the apply path's bounding. + Consolidation `confidence` likewise defaults to `0.9` when `float()` + overflows or the value is non-finite, instead of raising, and the + defaulting is logged so a systematically mis-scaled model stays + visible. ([#7442]) + +- **scheduler:** `briefReason()` now counts code points when shortening a + scheduled-run summary for the outcome notice, instead of measuring and + slicing UTF-16 code units. A 120-character cut that landed inside an + emoji's surrogate pair left a lone high surrogate rendered as U+FFFD, and + astral characters double-counted against the budget, shortening summaries + that actually fit. ([#7452]) + +- **skills:** The data-analysis skill accepts bare-filename exports: + `_export_results` in `analyze.py` resolves an output path with no parent + directory against the current working directory (`os.makedirs(... or ".")`) + instead of raising on `dirname("")`, so targets like `report.csv` work + alongside nested relative paths (parents are created automatically) and + absolute paths; an existing file at the target is overwritten without + warning. Applies to CSV, JSON, and Markdown exports. ([#7132]) + +- **skills:** User-scoped skill storage now honors `skills.container_path`: + `get_or_new_user_skill_storage` seeds `container_path` from the resolved + app config (an explicit caller override still wins), and the + `user_should_see_legacy_skills` kwargs branch seeds it the same way. + Previously only public storage followed the configured root while + `UserScopedSkillStorage` kept the default `/mnt/skills`, so the container + registry and skill activation built paths under a mount root that no + longer exists once an operator moves it. ([#6770]) + +- **scripts:** `sync_labels.py` no longer crashes with `UnicodeEncodeError` + on hosts whose stdio encoding cannot represent its ✓/✗ glyphs and em dash + (Windows code pages, POSIX C locale, `PYTHONIOENCODING=ascii`) — a crash + that struck after `gh label create --force` had already run, leaving a + half-applied sync. `main()` reconfigures stdout/stderr to UTF-8 with + `backslashreplace` when redirected (consoles keep their encoding), and the + `gh` subprocess output is decoded as UTF-8 with `errors="replace"` + following `scripts/support_bundle.py` so failure messages are not mangled + by the host locale. ([#7473]) + +- **sandbox:** `LocalSandbox.write_file` no longer rewrites container paths + inside every file it stores — translation now applies only to executable + content, classified by `_content_path_translation_applies` using the shared + `is_code_file`/`is_code_path` rule plus the sandbox's own interpreter + suffixes (`.awk`, `.sed`, `.lua`, `.r`, `.tcl`, which host interpreters + open by path) or an extensionless destination whose head carries a + shebang. Documents, data files, and configs are stored verbatim; + previously a dictated `README.md` ended up holding a host path while + `read_file` masked the rewrite back to the virtual path, so the agent + confirmed content the file did not contain (#7461). The extensionless + shebang sniff opens with `O_NONBLOCK` and an `fstat` regular-file check, + so an append to a FIFO cannot block, and suffixed paths are classified + without touching the destination. ([#7462]) + +- **frontend:** Cap background "conversation finished" notification bodies at + 200 code points instead of UTF-16 code units. Both chat pages truncated the + last message's text with `String#length` and `substring`, so a cut landing + inside an astral character's surrogate pair left a lone surrogate that the + notification rendered as U+FFFD, and a body fitting 200 code points was + shortened early. The extracted `notificationBody()` in + `frontend/src/core/notification/body.ts` bounds text with `Array.from`, + keeps bodies within the budget whole, and returns the "Conversation + finished" fallback when the last message has no text. ([#7474]) + +- **frontend:** Scroll long artifact lists inside the artifacts panel by + replacing the bare `
` wrapper around `ArtifactFileList` with the + shared `ScrollArea` in `chat-box.tsx`. The panel had no scrolling container, + so files past the visible height were unreachable in both the desktop + resizable panel and the mobile Sheet drawer; every listed file, including + the final one, is now reachable and clickable. A new e2e spec exercises + short and long lists before clicking the last file. ([#7284]) + +- **runtime:** Reject delta-mode resumes from an older checkpoint when either + the selected checkpoint or the current head still has pending execution, + instead of linearizing the fork and silently discarding that work. Before + rewriting state, `_linearize_delta_checkpoint_resume` inspects the + `__pregel_tasks` channel values of both checkpoints, and for + `Command(resume=...)` input also rejects pending graph nodes other than the + synthetic `__start__` entry, raising the new `CheckpointResumeRejected` + error before any state write; the worker then skips checkpoint rollback and + edit-replay compensation because nothing was written. Full-mode resumes and + replay without pending execution keep their existing behavior. ([#7441]) + +- **sandbox:** Classify speculative prewarm acquires in the sandbox prewarm + benchmark by event state instead of monotonic-clock comparison. Each + recorded acquisition now captures `precedes_tool_request` from + `trace.tool_requested.is_set()` at acquire start, and the speculative + filter uses that flag rather than comparing + `started < tool_requested` timestamps. On coarse clocks (Windows), a + genuine prewarm acquire could be timestamped inside the same tick as the + tool request, so the strict comparison misclassified it as tool-triggered; + a quantized monotonic-clock test pins the tie classification. ([#7045]) + - **threads:** `POST /api/threads` now rejects an `assistant_id` that cannot name an agent with `422`. It used to store the value, and every later read of the thread (`GET` thread, state, history) answered @@ -1548,12 +2464,200 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. way before this fix are read with the lead agent's schema, and a branch of such a thread stores no `assistant_id`, so it uses the lead agent. ([#7445]) +- **sandbox:** Give `LocalSandbox`'s POSIX and Windows command runners + single-owner pipe-fd cleanup after a post-spawn failure. The exception path + now closes only the read ends of the stdout/stderr pipes while the `finally` + block alone closes the write ends; previously a failure after `Popen` + succeeded left both blocks closing the same descriptors, and the second + close could land on a descriptor number another thread had already claimed. + On startup failure the spawned child is also killed and reaped with a + bounded `wait`, suppressing cleanup errors so the original startup error is + preserved instead of the child being left running. ([#7430]) + +- **sandbox:** Normalize trailing slashes in `container_path` values at config + load for both sandbox volume mounts and `skills.container_path`. A + trailing-slash spelling such as `/mnt/reports/` was accepted by + `VolumeMountConfig` and only normalized by `LocalSandboxProvider` when + building path mappings, while the sandbox tool path gates compare the raw + configured value, so every ls/read/write/glob/grep/bash access to the mount + failed with a confusing `PermissionError`; the shared + `normalize_container_path()` validator keeps both layers on one spelling. + Values that normalize to nothing — the container root `/` or an empty + string — are rejected at load; an empty `container_path` previously made + the gates match every absolute path, applying one mount's `read_only` flag + to all paths. ([#7393]) + +- **channels:** Retry the runtime channel config sidecar file lock on Windows + instead of failing after roughly ten seconds. The lock used blocking + `msvcrt.LK_LOCK`, which gives up after about ten seconds and surfaced + cross-worker contention as `PermissionError` rather than serializing the + read-modify-write cycles it exists to serialize; the Windows fallback now + acquires with `LK_NBLCK` and retries only contention errors every 0.2 + seconds through `retry_until_locked`, waiting indefinitely like POSIX + `flock(LOCK_EX)` and logging a single warning after 50 contention retries. + `fcntl`/`msvcrt` are bound at module level so the fallback branch is + testable; new lock tests cover the retry cadence, POSIX `flock` behavior, + and real cross-thread contention. ([#6888]) + +- **config:** Delete thread directories containing read-only files on Windows. + `Paths.delete_thread_dir()` called `shutil.rmtree()` directly, and Windows + refuses to unlink a read-only entry (WinError 5) where POSIX only needs a + writable parent, so one read-only file under `user-data/` made the whole + thread undeletable: `DELETE /api/threads/{id}` answered 500 "Failed to + delete local thread data" and every later retry failed on the same file. On + `PermissionError` the call site now clears the read-only attribute + tree-wide (skipping symlinks) and reruns `rmtree` once; other errors, and a + tree that stays undeletable because another process holds a file open, still + raise instead of being silently skipped. ([#7446]) + +- **skills:** Reject non-finite values in data-analysis JSON exports before + opening the destination file. The CSV/Excel CLI's JSON export serialized + with `json.dump` straight into the output, so `NaN`, `Infinity`, or + `-Infinity` values — including ones nested in lists or structs — were + written as bare tokens that standard JSON parsers reject, and the partial + file replaced any previous result. `_export_results` now serializes with + `allow_nan=False` first and writes only on success, so the CLI reports an + error and preserves the existing output; finite numbers, nulls, and text + such as `'NaN'` remain valid. ([#7456]) + +- **console:** Count legacy runs' input tokens in the console usage breakdown. + Rows predating the per-model `token_usage_by_model` map fall back to the + run's `model_name`, but the fallback added only the total token count, so a + legacy run's recorded `total_input_tokens` never reached + `ConsoleUsageModelBreakdown.input_tokens` and per-model input totals + undercounted. The fallback now accumulates `total_input_tokens` together + with newer usage buckets for the same model without inferring cache hits. + ([#7411]) + +- **subagents:** Classify `tests_passed` evidence from the full recorded tool + output before truncation instead of from the bounded output tail. The bash + execution harvest stamps each entry with a `test_summary` of + passed/failed/unverified computed by `summarize_test_output()` over the + whole ToolMessage and caches it per tool call across stream chunks, so a + summary that scrolled past the 1000-character tail — a long pytest log with + the `N passed` line early, or trailing TODO stacks and warnings — no longer + erases the test evidence; the checker consumes the stamp and keeps the old + tail-only check for legacy entries. Complete, consistent Node counters take + precedence over incidental log text such as `12 passed`, while + independently shaped sibling runner summaries (pytest, Maven, Gradle, Go, + unittest) still contribute. ([#7407]) + +- **provisioner:** Declare the userdata PVC once per sandbox pod and reuse it + across mounts in PVC mode. With `USERDATA_PVC_NAME` set, the pod spec + previously published the same claim once as `user-data` and again for every + extra mount and Lark broker credential volume, which NFS CSI drivers that + cannot serve multiple volume declarations for one claim reject. The + provisioner now emits the single `user-data` volume from `_build_volumes` + and points extra and broker mounts at it with their existing `subPath`s and + per-mount `read_only` flags, keeping credential mounts broker-only; + hostPath mode keeps distinct volumes, and `SKILLS_PVC_NAME` must stay a + different claim from `USERDATA_PVC_NAME` because unrestricted threads still + declare a separate `skills` volume. ([#7274]) + - **frontend:** Accounts whose email the backend accepted but the frontend's stricter pattern did not (for example `a&b@example.com`, `a@b.c` or a Unicode address) now get the normal workspace. Their sessions used to show the gateway-offline fallback, and one such account made the admin **Settings → Users** list fail for every administrator. ([#7449]) +- **chart-visualization:** Map `generate_spreadsheet` in the chart CLI and + exit nonzero when any generation fails. `SKILL.md` already selects + `generate_spreadsheet` and the reference documents it, but + `CHART_TYPE_MAP` in `generate.js` had no entry, so the batch reported + "Unknown tool" for it; it now maps to the AntV `spreadsheet` type. The CLI + also tracks per-item failures — a missing `tool` field, an unknown tool, or + a failed chart-service request — and exits with status 1 after printing the + successful items' URLs; previously these errors were only logged to stderr + and the process exited 0. ([#7395]) + +- **scripts:** Pin the skill-review analyzer subprocess to UTF-8 on both sides + of the pipe. The analyzer prints `ensure_ascii=False` JSON, but the parent + decoded its output with the ambient locale codec and the child inherited + the ambient stdio codec, so non-ASCII skill facts crashed the review pass + on cp1252 Windows (an encode failure inside the analyzer) and produced + mojibake on cp936. `review_env()` now sets `PYTHONIOENCODING=utf-8` and + `PYTHONUTF8=1` for the child, and the parent's `subprocess.run` decodes + with `encoding="utf-8"`. ([#7432]) + +- **dingtalk:** Reject unusable access-token lifetimes before caching the + token. An `expireIn` of zero, a negative value, or anything within the + 300-second refresh margin (`_TOKEN_REFRESH_MARGIN_SECONDS`) was accepted + as-is, so the cached token's expiry landed at or before the current time + and every call fetched a fresh token; non-numeric, infinite, or oversized + values (`inf`, `10**400`) raised `OverflowError` outside the handled + `TypeError`/`ValueError` and crashed `_get_access_token`. All of these now + fall back to the default 7200-second lifetime, with the token cached once + across calls; lifetimes above the margin, such as 301 seconds, keep their + computed expiry. ([#7450]) + +- **readability:** Keep `` links when Readability.js silently degrades. On + Windows the `simple_json_from_html_string` subprocess could succeed while + returning the unextracted document instead of the article, and the + pure-Python fallback dropped every `` destination, so `web_fetch` + produced link-free Markdown. `ReadabilityExtractor.extract_article` now + treats `` or `` markup in the extracted content — which a + successful extraction always strips — as the raw-document signal and + substitutes the resolved HTML's `<body>` inner content, so anchors survive + as `[text](href)`. The input-sanitization authority-tag census exempts + the ordinary HTML `body` tag that the new body-extraction regex now + matches. ([#7412]) + +- **knowledge:** Accept case-insensitive upload media types in the + knowledge-management extension. The `POST .../files` handler in + examples/deerflow-extension-knowledge-management now strips whitespace and + lowercases the `Content-Type` header's media type before comparing it with + `application/octet-stream`; a client sending `Application/Octet-Stream; + charset=binary` or `APPLICATION/OCTET-STREAM` previously got a `415` even + though the body was a valid octet stream. ([#7397]) + +- **video:** Preserve reference image MIME types in the video-generation skill + and reject unrecognized reference image extensions. Gemini reference + payloads in `skills/public/video-generation/scripts/generate.py` previously + hardcoded `image/jpeg` for every reference, and `_guess_mime` silently fell + back to `image/jpeg` for any unmapped suffix. Both providers now derive the + MIME from the filename extension (`image/png`, `image/jpeg`, `image/webp`, + `image/gif`; case-insensitive), and a missing or unsupported extension + raises a `ValueError` naming the file and the supported set before any API + submission. Image bytes are neither validated nor converted, and + provider-side format restrictions still apply; the skill docs and README + state these limits. ([#7340]) + +- **config:** Pin an explicit app config path across `get_app_config` calls. + `reload_app_config(config_path)` now records the file's absolute path in an + `_app_config_source` pin (mirroring `_extensions_config_source`), and + `get_app_config()` resolves against that pin, so an embedded + `DeerFlowClient(config_path=...)` keeps following the explicit file across + working-directory changes instead of re-probing ambient resolution — which + could previously flip the cache to a sibling default config or raise when no + ambient config existed. The pin is recorded only after a successful load, is + cleared by an argument-less `reload_app_config()`, `reset_app_config()`, or + `set_app_config()`, and all singleton reads, reloads, resets, and injections + are serialized under a new `_app_config_lock` so a concurrent call cannot + publish a config under another file's pin. ([#7092]) + +- **uploads:** Make upload companion record removal best-effort. + `unregister_companion` and the stale-record branch of + `invalidate_overwritten_upload` previously let an `OSError` from deleting + the ownership record propagate — a Windows handle still open or a read-only + mount turned an upload whose outcome was already decided into a failed + request. Both paths now log the orphaned record through a + `_warn_record_removal_failed` helper whose own logging failures are + suppressed, and a leftover record stays safe because `resolve_companion` + validates the recorded source identity instead of resolving to the wrong + companion. ([#7409]) + +- **memory:** Keep memory sample import backups unique per run in + `scripts/load_memory_sample.py`. Backups were named only with a + second-resolution timestamp, so two imports started in the same second + silently overwrote the earlier backup — the one file standing between a + bad import and lost memory. Single-target backups now reserve a unique + `tempfile.mkstemp` sidecar (`{target.name}.bak-{timestamp}-...`) before + copying and remove the reservation if the copy fails, and `--all-users` + runs create their backup root under `.deer-flow/memory-sample-backups/` + with `tempfile.mkdtemp` before any user memory is replaced. Docs note + that backups accumulate until removed manually and POSIX backup + directories are private to the creating OS user. ([#7418]) + - **persistence:** A PostgreSQL bootstrap failure inside an existing database now reports its own error. The Gateway treated any error mentioning "does not exist", such as a missing column or relation during a migration, as a missing @@ -1562,6 +2666,72 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. error was missing from the traceback. Only a missing database (SQLSTATE `3D000`) is auto-created now. ([#7410]) +- **uploads:** Never truncate an existing upload on a same-name collision. The + shared write path `open_upload_file_no_symlink` opened destinations with + `O_WRONLY|O_CREAT` and then `ftruncate(0)`, so re-uploading `report.pdf` + opened and emptied the `report.pdf` already stored, and concurrent same-name + uploads raced on one inode — the Gateway ingestion pipeline already + refused overwrites, but the embedded-client `copy_upload_file_no_symlink` + and channel ingress paths still went through the truncating open. + Destinations are now opened with `O_EXCL`; on `FileExistsError` the next + free `_N` suffix is claimed against the directory's current contents via + `claim_unique_filename` and the open retried, so both uploads land as + `name.ext` and `name_1.ext` with their bytes intact and the existing file + is never opened. `exclusive=True` keeps raising `FileExistsError`, and + embedded-client and channel inbound-file responses now report the name the + bytes actually landed under (`filename`, `virtual_path`, `artifact_url`) + instead of the requested name. ([#7388]) + +- **scheduler:** Reconcile running scheduled occurrences from terminal durable + runs. A completion hook that failed after the durable run had already + committed its terminal status (transient database, observer, or notification + error) left the occurrence in `running` until a restart — a recurring + schedule stopped firing and a once task never reached its terminal parent + state, because single-instance `run_once` only recovered expired launching + claims. A new non-destructive + `ScheduledTaskRepository.reconcile_terminal_runs` pass selects running + occurrences whose linked `runs` row is already terminal (`success`, + `error`, `timeout`, `interrupted`) with matching scheduled metadata, and + finalizes each through the existing `finalize_occurrence` path with the + run's status, error, and goal verdict. Each candidate commits in its own + transaction so a poisoned occurrence cannot roll back the whole pass; live + runs are never taken over, and the service catches remaining pass-level + failures so admission and queue draining still run. No schema change. + ([#7269]) + +- **artifacts:** Keep sandbox paths inside CJK corner brackets whole when the + artifact registry captures handles from tool results. The quote-delimiter + tables in `artifact_registry.py` (`_REF_QUOTE_DELIMITERS`, + `_REF_TRAILING_NOISE_CHARS`, `_REF_CLOSING_DELIMITERS`) knew ASCII quotes, + backticks, curly quotes, and CJK punctuation but not `「」`/`『』`, so + `已生成「/mnt/user-data/outputs/季度 报告.pdf」。` captured the dead + handle `/mnt/user-data/outputs/季度` (the path pattern stops at the space) + and `已生成『…』。` left the `』` closer inside `real_ref`; either handle + fails on a later read. Corner brackets are now registered as wrappers — a + paired closer stays inside a filename so spaces survive, an unpaired closer + is trimmed from a bare token, and corner-trailed URLs and refs are no + longer cut. ([#7377]) + +- **skills:** Preserve transparency in composed PowerPoint slides. The + ppt-generation composition script flattened every slide image to RGB and + saved it as JPEG, so transparent regions of PNG slide art (including palette + and grayscale images carrying a `transparency` entry) turned black in the + deck. `generate_ppt` now detects an alpha channel via `img.getbands()` or + `img.info` and embeds those slides as RGBA PNG, while opaque images keep + the previous JPEG path; the skill doc states that transparent slide images + retain their alpha channel. ([#7312]) + +- **sandbox:** Close pipe write fds once when `Popen` fails in the local + sandbox command runners. On a spawn failure, `_run_windows_command` and + `_run_posix_command` released all four pipe fds in the `except` block and + then closed the two write fds again in the `finally` block; in a busy + gateway another thread can claim a freed descriptor number in between, so + the second `os.close` silently hit an unrelated descriptor — a concurrent + command's capture pipe or an HTTP socket. The `finally` block now closes + the parent's write ends only after a successful spawn + (`process is not None`), and the cleanup catches `BaseException` so a + `KeyboardInterrupt` mid-spawn still releases each fd exactly once. ([#7394]) + - **mcp:** Ordinary users can now install DingTalk, WeCom and HubSpot from **Capability Center → Plugins**. Saving one of these bundled connections as a non-administrator, including editing or enabling it later, @@ -1569,12 +2739,125 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. read a `cwd` field the request model does not declare. A bundled connection with a working directory still requires an administrator. ([#7400]) +- **gateway:** Bound the run event type filters accepted by + `GET /api/threads/{thread_id}/runs/{run_id}/events`. `list_run_events` split + the `event_types` query parameter on commas and passed the result to the + event store unbounded, so a request with an arbitrarily long filter list + drove an unbounded-cardinality store query. More than + `MAX_EVENT_TYPE_FILTERS` (100) values now return a `422` naming the limit + before the store is touched; up to 100 values, an absent filter, and the + store call itself are unchanged. ([#7370]) + +- **scripts:** Make `scripts/configure.py` parse its arguments before touching + the filesystem. The bootstrap script ignored its argv entirely, so + `--help` printed no usage and instead ran the bootstrap as a side effect — + creating `config.yaml`, `.env`, and `frontend/.env` in a fresh checkout — + and unrecognized flags were silently ignored the same way. Arguments now go + through `argparse`: `--help` prints usage and exits 0, unknown flags exit + 2, and the no-argument invocation used by `make configure` is + unchanged. ([#7404]) + +- **skills:** Stop rebuilding the thread skill view every turn on network + filesystems. Freshness checks for the projected `/mnt/skills` view digested + inode, size, and timestamp metadata, which churns or settles late on NFS + and other shared storage, so the manifest never matched and every sandbox + acquire recopied the whole view. A new `_thread_view_signature` + fingerprints projected bytes and layout instead — sha256 per file read in + bounded chunks, symlinks signed as links without opening them, directory + and executable bits included — while `_thread_projection_is_fresh` still + compares the source signature and repairs edited instructions, missing or + injected files, and changed links from the canonical skills on the next + acquire. Integrity checking now reads the projected files, so larger + packages add acquire-time read I/O; `_THREAD_PROJECTION_POLICY_VERSION` + bumps to 2 to invalidate old manifests once. ([#7333]) + +- **gateway:** Assign message IDs to run inputs that omit one before the run + record is stored and the graph is invoked, so the run record and the graph + checkpoint carry the same ID and a client rejoining an active run can + deduplicate the user's message. Keyed requests derive stable IDs via + `uuid5` over `deerflow:run-input:{thread_id}:{idempotency_key}:{index}`, + while unkeyed runs get fresh `uuid4` values; caller-supplied IDs and the + original request objects are left untouched. The idempotency identity is + computed from the pre-stamp input and retry matching accepts that form too, + so a keyed retry still reuses its run even though the stored input now holds + Gateway-issued IDs. During a rolling upgrade, older Gateway workers may + reject such a retry with 409. ([#7349]) + +- **extensions:** Make detached notification admission atomic with shutdown by + serializing `dispatch_system_model_observation`, the drain snapshot, and + `reset_extension_notify_loop` on a single `_dispatch_state_lock`. + Previously shutdown could suspend observations and snapshot + `_pending_dispatches` between the enabled check and the future's + registration, so an observation submitted right at teardown escaped the + drain and could still be pending when the loop is torn down. The done + callback discards through the same lock and is registered outside it, + because `add_done_callback` may run inline for an already-completed future. + ([#7383]) + - **examples:** The extension example's package tests pass again. Its test registry lacked `knowledge_provider()`, so it no longer matched the `ExtensionRegistry` protocol and the README's `pytest` run failed. Extension Package Check now also runs the tests of bundled examples that ship them, so a contract change that breaks them fails its pull request. ([#7364]) +- **projects:** Treat removal of a staged shelf copy as best-effort in the + project-document routes through `_discard_staged_copy`, which catches + `OSError` and logs a warning instead of letting the unlink failure win. + Previously a removal failure (a Windows handle still held elsewhere, a + read-only mount) raised out of the `finally` in + `attach_project_document_to_thread`, replacing the endpoint's real outcome + and skipping `service.aclose()`, and turned the empty-file `400` paths in + `upload_project_document` and `promote_thread_file_to_shelf` into a 500. + Leftover staged files remain the staging sweep's job. ([#7229]) + +- **scripts:** Reject a directory or a file ancestor as `--out` in + `dev_multi_instance` before any writer runs, failing in `main()` with a + `parser.error` naming the offending path. `render-config`, `secrets` and + `nginx-conf` publish the output through `os.replace`, so an existing + directory surfaced as a raw `PermissionError` traceback and a file ancestor + as `OSError` from `mkdir`, after the writers had already done work. The + `--help` text for all three commands now states the file contract. ([#7289]) + +- **browser:** Prefix an Unbrowse fetched page with a warning when the target + page responded with HTTP 400-599 even though the scraping service itself + returned success, including the reported final URL when available, and keep + the same status detail on the empty-page error instead of a bare + `Error: No content found`. Previously a fetch of a failed target was + indistinguishable from a successful one. ([#7257]) + +- **skills:** Exit the podcast-generation CLI with status 1 and write the + diagnostic to stderr when generation fails, instead of printing + `Error generating podcast` on stdout and exiting 0. SKILL.md now instructs + the model to present the MP3 only after a successful run, since an existing + audio file or a freshly written transcript does not establish that + generation succeeded. ([#7236]) + +- **projects:** Drain shelf staging disk operations through cancellation so + `stage_document_bytes` still closes and unlinks a partial file. Cleanup now + runs on `BaseException` with each `run_file_io` step wrapped in + `await_drained`, and the opened handle is retained across its + worker-to-caller handoff; waiting for the next upload chunk remains + cancellable. Previously `asyncio.CancelledError` bypassed the + `except Exception` cleanup, leaving a partial staged file behind when a + client abandoned an upload mid-stream. ([#7360]) + +- **ci:** Point `.github/labels.yml` comments at `triage.yml` instead of the + deleted `pr-labeler.yml` / `pr-triage.yml` / `issue-triage.yml` workflows, + and add `scripts/check_label_declarations.py` to `lint-check.yml`, which + fails when the workflow applies a label `labels.yml` does not declare. + GitHub rejects the whole `addLabels` batch with 422 when any name is + missing, so one undeclared label could silently drop all auto-labeling. + ([#7328]) + +- **subagents:** Keep parent context snapshots from mispairing results when + one `AIMessage` carries several tool calls with the same ID. + `ParentContextSnapshot` now marks such an ID ambiguous and unclaimable, so + its result neither completes one of the duplicated calls nor falls back to + an earlier pending call that reused the ID, and the ambiguous call is + omitted from the snapshot while earlier completed and unique calls are + retained. Previously the last duplicate won the ID in `call_positions`, so + a result could be attributed to the wrong call. ([#7372]) + - **gateway:** Run requests with a malformed body now get a client error instead of `500 Internal Server Error`. A `config.configurable` or `config.context` that is not an object, an invalid `assistant_id`, a @@ -1584,24 +2867,343 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `config.configurable` of `[]`, `""`, `0`, `false` or a list of key/value pairs used to be accepted and is now rejected with `422` too. ([#7362]) +- **uploads:** Report rejected attachments instead of dropping them silently + in `upload_files`: an unsafe filename and an unnamed multipart part are now + recorded in `skipped_files`, and `success` means at least one file was + stored (`bool(uploaded_files)`). Previously both drop arms returned + `success: true` with an empty `files` list, so the frontend gate that blocks + a chat send proceeded without the attachment the user asked for; an + all-rejected batch now fails and a mixed batch succeeds while listing what + was dropped. ([#7165]) + +- **gateway:** Close an initialized sandbox provider during Gateway shutdown: + after `langgraph_runtime` exits and the MCP session pool is torn down, the + lifespan drains `shutdown_sandbox_provider()` on a worker thread inside the + same `finally`, covering AIO sandboxes first created by a worker-thread tool + call. Previously the Gateway never shut the provider down, so its sandboxes + outlived the process. The hook never initializes a provider just to clean + it up, and failures are logged without blocking worker exit. ([#7347]) + +- **sandbox:** Keep Tenki termination ownership across provider retirement: + handles slated for close are parked in `_pending_closes` and retried by the + idle checker and later shutdown passes instead of being abandoned after one + failed close, and a sandbox created after retirement is closed by its + creating worker while in-flight acquires are counted and rejected with + `RuntimeError`. Terminal-failure callbacks now carry the originating adapter + (`expected_sandbox`), so a late failure from a replaced generation cannot + deregister a scope that `acquire` already rebuilt. `shutdown()` stays + retryable and raises `_ShutdownIncompleteError` with pending acquire and + termination counts rather than pretending to drain, and the atexit wrapper + logs that warning without a traceback. `TenkiSandbox.close` marks closed + only on an accepted termination or an authoritative already-gone error, so + ambiguous transport failures stay retryable. ([#7311]) + +- **skills:** Reject a data-analysis JSON export whose result columns contain + duplicate names, raising a `ValueError` that lists the repeated names and + suggests SQL `AS` aliases; the CLI prints a short `Error: ...` line to + stderr and exits nonzero, and any existing output file is left untouched. + Previously duplicate columns silently overwrote each other while building + the JSON records, so all but the last value for each name was lost. + `main()` also closes the database connection in a `finally`, so a rejection + does not leak it. ([#7305]) + +- **agents:** Treat a JSON `error` field inside a successful `read_file` + result as document data, marking the tool message `status="success"` with + `source="content_analysis"`, instead of classifying it as a tool error. + Reading a saved API error response no longer records a failed read or, with + the optional `tool_progress` guard enabled, blocks later reads; explicit + read failures carrying the error prefix still take precedence. ([#7369]) + +- **skills:** The skill scanner's `shell-curl-pipe-shell` rule now builds its + shell-name alternation from the shared `_SHELL_NAMES` vocabulary and widens + that set from five to ten shells (adding `ash`, `csh`, `ksh`, `mksh`, + `tcsh`), so `curl ... | ksh` and its unlisted siblings now yield the + finding instead of running a fetched script silently. Previously the + download-pipe matcher hard-coded `bash|zsh|dash|fish|sh` while the + structural `env` fallback read `_SHELL_NAMES`, even though the sandbox + audit middleware already counts `ksh` as a shell; programs that only + share a shell's prefix (`ashley`, `tclsh`) still do not match. ([#7378]) + +- **extensions:** Temp-file cleanup on failed extension and skill saves now + goes through `cleanup_failed_atomic_write`, so a failed unlink or logging + error no longer replaces the original publication error. Extension + snapshot rollback, the managed `plugins` block rewrite, the Gateway + skill-archive upload copy and its post-install discard, and custom-skill / + per-user skill-state writes all route through it; `_write_bytes_atomically` + closes the descriptor when `fdopen` fails, the plugins rewrite records the + temporary path before writing so a short write still removes it, and the + upload route discards the archive before closing the multipart form with + close errors suppressed, so a `SpooledTemporaryFile` flush failure cannot + shadow the install outcome. ([#7164]) + +- **gateway:** Thread state updates (`POST /threads/{id}/state`) honor the + request body's `checkpoint` object instead of only the deprecated + `checkpoint_id`, validating that its `thread_id` matches the URL (400 on + mismatch) and carrying `checkpoint_ns` through the state accessor chain so + updates can target a subgraph checkpoint rather than always the root. A + state-only mutation graph now binds the checkpointer explicitly + (`CONFIG_KEY_CHECKPOINTER`) so the requested namespace is read and written + on the graph itself, a namespace or checkpoint that does not resolve + returns 404, and a `title` value updates thread metadata only when the + update targets the root namespace, so subgraph titles no longer leak into + the conversation title. ([#7115]) + +- **benchmarks:** `bench_provider.py` rejects a negative `--idle-timeout` with + `SystemExit("idle timeout must be nonnegative")` during CLI validation, + before any provider is created, and the benchmark README documents that + zero disables idle reaping. A test pins the rejection for all three + registered providers (`opensandbox`, `boxlite`, `aio-docker`) by failing if + a provider factory is invoked. ([#7363]) + +- **sandbox:** AIO sandbox quarantine teardown now stays idempotent when a + peer finishes the same teardown first: `retire()` and + `retire_replaced_generations()` list the fence directory through + `_records()` and unlink records through `_drop()`, both of which treat a + missing directory or record as work already done. The teardown exclusion is + a TTL lease rather than a lock, so previously a peer's `retire()` landing + between the existence check and the listing (or between listing and + unlink) raised `FileNotFoundError` out of the unwrapped + `_retire_absent_quarantine()` call at the top of sandbox creation; the + Windows `PermissionError` from a peer `mark()` still holding the fresh + record open for `fsync` deliberately remains an error. ([#7313]) + +- **frontend:** Relative links in assistant messages (e.g. + `[Report](report.md)`) now resolve to thread artifacts: inside the message + list, a scheme-less path with no leading `/` or `..` segment that + suffix-matches exactly one artifact opens it in a new tab, while zero or + several matches render as inert text instead of a link the browser would + resolve against the chat route into a broken `/workspace/chats/<file>` + thread (422 from the Gateway). The artifact list reaches the link renderer + through `MessageArtifactPathsContext`; clarification and `present_files` + text now use the thread-aware renderer, and a reopened thread rebuilds its + artifact list from `present_files` calls in the messages, since the history + head carries messages but no `artifacts`. ([#7356]) + +- **sandbox:** The local sandbox saves a command's process group id at spawn + time (`process.pid`, since `start_new_session=True` makes the child the + group leader) and kills that saved group on timeout, instead of calling + `os.getpgid()` again inside `_terminate_process_group()`. Previously a + leader that exited between the timeout and the kill — a window that also + covered the pipe-drain thread startup — made `getpgid` raise + `ProcessLookupError`, which the same handler catches, downgrading the + timeout to killing only the direct child while the rest of the group kept + running; now only a genuinely vanished group falls back to the direct + child. ([#7353]) + +- **events:** The database run-event store treats an explicit empty + `event_types` list as an empty filter that matches nothing, so it now agrees + with the memory and JSONL stores; previously the truthiness check dropped + the `event_type IN (...)` predicate and returned every event for the run. + `event_types=None` still means unfiltered. ([#7352]) + +- **acp:** `invoke_acp_agent` now races the ACP exchange against the agent + process and fails fast when the agent exits or closes its connection, + instead of waiting out the whole `timeout_seconds` (30 minutes by default) + and suggesting a raise; after a short `_EXIT_DRAIN_SECONDS` grace for + responses still buffered in stdout, it reports the exit code and points at + installation and authentication. The tool also drains the agent's stderr + continuously into a bounded 8 KiB tail (`_StderrTail`) — the pinned SDK + spawns agents with `stderr=PIPE` and never reads it, so a verbose agent + blocked on its next stderr write stalled the invocation the same way — and + logs that tail on timeout or exit while keeping it out of model-visible + text. ([#7354]) + +- **eval:** Context-snapshot benchmark grading now distinguishes booleans from + numbers wherever it compares values: a new `_same_value()` helper rejects a + mismatch like `false` standing in for `0` and recurses through dicts and + lists, replacing comparisons that caught bool/number confusion only at the + top level of JSON checks and missed it entirely in Python checks, including + input-mutation detection. Ordinary numeric equality such as `1.0 == 1` is + unchanged. ([#7358]) + +- **subagents:** A subagent whose response the provider cut at the output + limit (`finish_reason=length`, `stop_reason=max_tokens`, `MAX_TOKENS`, + Responses `max_output_tokens`) now reaches the lead as `completed` with + `subagent_stop_reason=model_length_capped` instead of a clean `completed`, + and a tool call truncated at that boundary no longer executes with partial + arguments. `ModelLengthFinishReasonMiddleware`, previously lead-chain only, + joins the subagent chain after extensions and before Safety, records each + detected cap per context `run_id`, and exposes `consume_stop_reason` so the + executor's existing duck-typed guard collection surfaces the cap; the value + joins the subagent status contract (Python enum, contract fixture, frontend + whitelist) labeled "model output length limit". ([#7355]) + +- **sandbox:** The local sandbox's `glob()` and `grep()` now search an + unmapped virtual parent directory — such as the skills root, which exists + only as per-category mounts with no aggregate host directory — by walking + each explicitly configured child mount through `_virtual_search_mounts()` + and returning matches under their virtual paths (new `relative_prefix` in + `find_glob_matches()` and `find_grep_matches()`), with a one-match lookahead + per mount so the result cap is global across mounts. Previously resolving + the virtual parent against the host made the search fail even though the + category mounts existed; ignore rules still apply within each mount, a + broken mount warns with its virtual path and re-raises instead of walking + the host parent, and mapped roots keep their normal walk. ([#7338]) + +- **sandbox:** The sandbox audit middleware now blocks an interpreter handed a + process substitution as its first operand (`bash <(curl url)`), which runs + the fetched bytes as the program; previously only the stdin redirect + spelling (`bash < <(curl url)`) was recognized, so the operand form executed + `curl`/`wget`-fetched content through the audit unimpeded. The rule requires + the interpreter (`sh`, `bash`, `python`, `perl`, `ruby`, `node`, `php`) in + command position with the substitution (a `curl`, `wget`, `base64`, or other + listed executable) as its first operand; a script argument in front + (`bash script.sh <(curl url)`) keeps the substitution an ordinary argument, + and a noexec shell (`bash -n <(curl url)`) still only parses. ([#7290]) + +- **models:** The MindIE provider's escaped-newline decoding now protects + tilde fences (`~~~...~~~`) alongside backtick fences, in both the one-shot + `_decode_escaped_newlines_outside_fences()` and the streaming + `_EscapedNewlineStreamDecoder`, so a literal `\n` inside a tilde-fenced code + block no longer turns into a real newline. A fence closes only with its own + delimiter, and an unterminated fence extends to the end of the reply while + preserving a complete pair of the other delimiter verbatim, matching the + streaming decoder; previously only the backtick delimiter was recognized. + ([#7264]) + - **tui:** Quitting after `Ctrl+C` interrupted a run no longer hangs on the step that run is still finishing. The run thread was joined on exit, so the TUI closed but the shell prompt stayed blocked until a long tool call or model request returned, and further `Ctrl+C` only printed a traceback. Quitting now says it is waiting for that step and closes when it finishes; press `Ctrl+C` again to quit at once. ([#7326]) - + +- **goal:** A goal evaluator response that completes normally (`stop` / + `end_turn`) but contains no valid verdict now retries the evaluation once + instead of failing the goal check: `evaluate_goal_completion()` raises the + new `GoalEvaluationFormatError` for that case, while explicit refusals and + length/safety stops keep their existing failure behavior. The worker + retries only after re-reading the goal and checkpoint and confirming the + goal instance, checkpoint id, and visible conversation signature are + unchanged. Evidence capping also spends spare room in + `MAX_GOAL_CONVERSATION_CHARS` restoring shortened user input instead of + leaving long user lines truncated when the budget had room left. + ([#7345]) + +- **uploads:** Coerce a missing or non-string upload-failure `message` to an + empty string before trimming, so a gateway or proxy that omits the field + yields the clean "Upload failed" error instead of a TypeError raised while + handling the failure. ([#7342]) + - **tui:** The streamed transcript no longer drops tokens that repeat or extend the text already shown. Deltas were merged by prefix, so `ha` + `ha` rendered as `ha`, `1` + `10` as `10`, and an answer that opened with a Markdown marker lost every later standalone copy of it (closing `**`, list bullets, table pipes, closing code fences). Each delta is now appended as sent. ([#7325]) +- **artifacts:** Keep a delimited sandbox path whole in the free-text artifact + scan instead of truncating it at the first space: the old + `/mnt/user-data/\S+` match registered a handle for the prefix before the + first space whenever a tool quoted a filename with spaces, so + `/mnt/user-data/outputs/quarterly report.pdf` became + `/mnt/user-data/outputs/quarterly`, which resolved to nothing after a + compact or checkpoint reload; single quotes and backticks failed the same + way. `_classify_span` now classifies each candidate by delimiter — a span + opened with a straight, curly, or backtick quote and closed on the same + line keeps its spaces and honors escaped delimiters, an unterminated span + produces no reference rather than a truncated prefix, and bare paths keep + their conservative whitespace boundary, still bounded to one line and the + existing size cap. ([#7308]) + +- **memory:** Apply the Mem0 `read_policy` to record conversion failures by + converting records to facts inside `_read_or_fallback` for `search` and + `list_memories`: a record with malformed `categories` or `metadata` fields + or a non-finite `score` now follows the configured fail-open/fail-closed + behavior instead of raising out of the read after the client call had + already succeeded. Conversion errors surface as `Mem0APIError` naming the + offending field and a sanitized record id (`<unavailable>` unless the id is + a bounded `[A-Za-z0-9_-]` string) without echoing remote field values. + ([#7307]) + +- **memory:** Normalize malformed Mem0 HTTP responses into `Mem0APIError` so + the configured `read_policy` governs them: `Mem0Client._request` now also + catches `UnicodeDecodeError` and `RecursionError` from body decoding and + rejects a non-object JSON payload, and `_memory_results` rejects a + non-list `results` field or a non-object record — shapes that previously + surfaced as `AttributeError` or `TypeError` from downstream `.get` calls + and bypassed the fail-open empty fallback. Errors carry the operation + context, including the pagination `page` for `POST /v3/memories/` listing. + ([#7202]) + +- **frontend:** Bound Live Browser WebSocket handshakes with a 10-second + `HANDSHAKE_TIMEOUT_MS` timer in `useBrowserStream`: a stalled upgrade now + enters the bounded reconnect path and closes the socket instead of waiting + indefinitely for an error or close event that may never arrive, and a + retired attempt fences late `open`/`message` callbacks so it cannot accept + input or reset the reconnect budget. The panel reports a + `handshake_timeout` versus generic `connection_failed` closed reason as a + localized status line and updates the Live button and empty-state text. + ([#7301]) + +- **models:** Honor HTTP-date `Retry-After` hints in Claude provider retries. + `ClaudeChatModel` previously ignored a date-form hint and kept its default + delay while the error-handling middleware and the OpenAI Codex provider each + kept a private date parser; the new shared `parse_retry_after_date_ms` in + `deerflow/utils/retry_after.py` parses the HTTP-date in UTC (treating the + naive asctime form as UTC), bounds the result by the 24-hour + `MAX_RETRY_AFTER_MS` policy, and all three call sites now use it. ([#7295]) + +- **frontend:** Label a thinking-only AI step as "Thinking" in the token usage + debug view instead of "Final answer": `buildTokenDebugSteps` used + `hasContent`, which counts a non-empty content array even when every block + is an Anthropic `thinking` block, so a step carrying only reasoning tokens + was classified as a final answer. It now checks + `extractContentFromMessage`, which maps `thinking` blocks to no visible + text. ([#7175]) + +- **scheduler:** Reject `once` schedule `run_at` and `end_at` wall times that + do not exist in the task's timezone — a DST spring-forward gap — with a 422 + (`invalid_schedule` for `run_at`, `invalid_request` for `end_at`) instead of + silently shifting the instant: naive values are attached to the task's + `ZoneInfo` zone and the UTC round-trip must reproduce the submitted wall + time. Ambiguous local times keep the earlier occurrence (`fold=0`), explicit + offsets preserve the specified instant, unchanged live `once` definitions + are grandfathered on no-op edits, and a changed definition or a PATCH that + rearms a terminal task is revalidated. ([#7281]) + +- **sandbox:** Preserve the previous file when a local text revision fails on + Linux/macOS instead of truncating it on open: sandbox `write_file` now + checks write access through an open descriptor (rejecting non-regular + files) and stages a hidden `.deerflow-write-` sibling for an atomic rename + via `publish_bytes_atomically`, so a failed write leaves the old content + published and staging cleaned. The new `file_revision_metadata` module + transfers ownership, mode, ACLs (macOS) and xattrs (Linux) from the source + descriptor before content is written and refuses setuid/setgid bits, macOS + file flags, and content-bound `security.*` privileges rather than + reattaching them to new content. Append mode and other platforms keep their + in-place behavior, and hard-link aliases retain the old version after the + inode replacement. ([#7299]) + - **sandbox:** Release an E2B VM's ownership lease when acquire finds it gone (a dead cached client or a warm-pool VM reaped while parked). The lease was kept and renewed until Gateway shutdown, so every reaped VM left a tracked ID and a renewed ownership key behind. ([#7277]) - + +- **helm:** Encode each Secret name precedence chain once: the new + `deer-flow.databaseUserSecret` helper (`postgresql.external.existingSecret` + then `postgresql.existingSecret`) feeds the gateway's `DATABASE_URL`, the + postgres StatefulSet's `POSTGRES_PASSWORD`, and the `postgres-secret.yaml` + render gate, so they can no longer resolve different Secrets. Both it and + `redisUserSecret` now chain `default` instead of `or`, which returns its + last operand even when falsy: `existingSecret: false` previously rendered + references to a Secret literally named `false` and skipped creating the + chart-managed Secret, leaving the redis StatefulSet unable to start. + ([#7291]) + +- **goal:** Base the Goal no-progress counter on newly observed successful + operations instead of only the evaluator's text progress key, which reset + whenever visible evidence changed and let a run repeat the same tool work + indefinitely without tripping the stand-down stop rule. + `observe_goal_operations` pairs each tool call with its result since the + turn's initiating input and fingerprints successful operations by tool + name, argument, and output hash and byte count — excluding UI-only + sandbox `description` arguments — and `compute_no_progress_count` + increments when a tool-active turn yields no new operation hashes, while + pure-text turns keep the original comparison. Bash results with nonzero + exit markers are reclassified as errors, correcting legacy + content-analysis success stamps on old checkpoints, and seen hashes + persist in `last_evaluation` across compaction and restart. ([#7271]) + - **auth:** SSO login and callback no longer fail with a `500` when the OIDC provider answers `200` with a body that is not the expected JSON object, such as a maintenance HTML page. Discovery, token, JWKS and UserInfo bodies, a @@ -1611,6 +3213,347 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. like a failed UserInfo request instead of aborting a login whose ID token is valid. ([#7258]) +- **auth:** Treat a correctly signed JWT with wrong claims as malformed + instead of leaking a `pydantic.ValidationError`: `decode_token` caught only + `jwt.PyJWTError`, so a token without `sub`/`exp` — exactly the shape of the + OIDC state cookie, which shares `jwt_secret` and is minted by the public + OAuth start endpoint — raised out of authentication as a 500 (or a broken + WebSocket handshake) instead of returning `TokenError.MALFORMED` and a 401. + `_verify_state_signed` had the same gap and now returns `None` for such a + cookie, letting the `/api/v1/auth/callback/{provider}` handler answer 403; + an expired token that is also missing claims still reports `EXPIRED` + because `jwt.decode` fails first. ([#7266]) + +- **channels:** Treat undecodable channel state files as corruption: readers + decoding `runtime-config.json` and WeChat cursor/auth state with + `encoding="utf-8"` caught only `json.JSONDecodeError` and `OSError`, so a + `UnicodeDecodeError` escaped and stopped `ChannelService.start()` from + launching any IM channel, aborted the one-time legacy-file import, and took + down the WeChat channel's own start. `ChannelRuntimeConfigStore._load`, + `_read_legacy_file`, and `WechatChannel._load_state`/`_load_auth_state` now + name `UnicodeDecodeError` next to the existing handlers, giving the same + corrupt-file recovery the channel store already had: a warning, the + in-memory fallback, and the file left in place. ([#7273]) + +- **scripts:** Reject a directory passed to `support_bundle --out` with the + CLI's clean `error: ...` message and exit 2 instead of a raw + `PermissionError` traceback from inside `zipfile.ZipFile` (the Windows + failure mode): `create_support_bundle` raises the `ValueError` the CLI + already maps, and the `--out` help text now says "Output zip file path". + ([#7256]) + +- **workspace:** Stop inferring file creation or deletion from a truncated + workspace snapshot: a scan bounded by `max_scanned_files` leaves absence + unknown, so `get_changed_paths()` reports a path as created only when the + before snapshot is complete and as deleted only when the after snapshot is + complete. Previously a file that existed before the run but fell outside + the truncated pre-run scan was reported as created by the run, and a file + outside the post-run scan was reported deleted. The scanner also walks + subdirectories in sorted order so the truncation boundary is reproducible + across the before and after scans, and the worker records + `verification_skipped=truncated_pre_run_snapshot` in the `run.delivery` + receipt when a truncated pre-run scan leaves no provable output changes. + ([#7172]) + +- **memory:** Exclude hidden human messages from Honcho memory writes: a + message carrying `hide_from_ui` in `additional_kwargs` is dropped from the + outgoing batch unless the `should_keep_hidden_message` host hook accepts + it, so framework-injected context no longer reaches Honcho's server-side + derivation while legitimate user clarification answers still do. + Standalone construction without a callable hook drops every hidden human + message, and a hook that raises skips only that message and logs a generic + warning that carries no message content or exception text. ([#7159]) + +- **scheduler:** Keep a recurring task's `next_run_at` cursor when a manual + trial launches: `should_advance_schedule_cursor()` treats a manual trigger + on a recurring task as an extra run that must not consume the next + scheduled occurrence, so the stored cadence survives launches, occurrence + finalization, and crash-repair bookkeeping in the runs repository. + Previously every launch recomputed `next_run_at` from the launch time, so + one manual trial pushed the next automatic occurrence a full interval + later. One-time tasks keep their existing boundary semantics. ([#7209]) + +- **api:** Preserve `Request` inputs in the frontend's authenticated fetch + wrapper: `fetcher.ts` extracted only the URL and forwarded `{...init}`, so + calling with a `Request` object silently lost its method, headers and + abort signal. The wrapper now reads `method`, `headers` and `signal` from + the `Request` whenever `init` does not override them, forwards them to the + static-demo resolver, and passes the original input to `globalThis.fetch` + instead of a bare URL string. ([#7254]) + +- **citations:** Scope inline-code masking to a single paragraph when + extracting citation sources: `maskInlineCode` paired backticks across the + whole document, so two stray backticks in different paragraphs formed a + span CommonMark never renders, the citation between them vanished from the + Sources list, and the stolen opener un-masked a real code span further + down. Inline spans now end at paragraph boundaries (CRLF-aware) and at the + other block boundaries CommonMark uses, and fenced-code masking recognizes + fences nested in list items and blockquotes — marker-aware openers and + closers, container-relative indentation, tab-expanded columns — which + previously only closed by accident and let sample citations surface as + sources. The container prefix is scanned position by position, so messages + carrying thousands of quote markers no longer hit exponential regex + backtracking that stalled rendering. ([#5876]) + +- **uploads,integrations:** Keep atomic-write cleanup from replacing the + caller's outcome: the upload companion recorder and the Lark broker and + CLI staging paths now release their temporary file through + `cleanup_failed_atomic_write()`, which preserves the original exception + and makes descriptor close and unlink best-effort steps that only log on + failure. Previously `register_companion` unlinked its staging file in a + `finally` block, so an `OSError` there overwrote the error the caller was + about to see, and the Lark archive download could lose its network error + to a cleanup failure. ([#7200]) + +- **scripts:** Make the `start-daemon.sh` compatibility wrapper start + production mode: it invoked `serve.sh --dev --daemon` while the Makefile's + `start-daemon` target uses `--prod --daemon`, so the wrapper brought up a + development stack where a background production start was expected. The + wrapper now matches the target and still passes extra arguments through. + ([#7261]) + +- **workspace:** Publish the snapshot text cache byte-exactly on every host: + the scanner writes it with `newline="\n"` and the snapshot text reader + opens it with `newline=""`, so decoded CRLF content survives the cache + round-trip unchanged. Text-mode defaults translated newlines in both + directions: on Windows, publication rewrote each `\n` into `os.linesep` + (turning CRLF content into `\r\r\n` bytes), and universal-newline reads + silently normalized CRLF to LF, corrupting the file text served from the + cache. The Windows workflow now runs the text-cache guard modules on pull + requests touching `workspace_changes` instead of only on its weekly + schedule. ([#7110]) + +- **helm:** Honor `redis-password` from a user-managed Secret: the bundled + Redis StatefulSet derived its AUTH mode only from `redis.auth.password`, + so a `redis.existingSecret` carrying both `redis-url` and `redis-password` + started the server without `--requirepass` while the gateway authenticated + against the URL's password, and both probes pinged without auth — the pod + reported healthy while the gateway could not connect. The container now + reads the optional `redis-password` key from the selected Secret at + runtime and enables AUTH only when it is nonempty, with the readiness and + liveness probes mirroring that choice; a Secret carrying only `redis-url` + keeps no-auth mode, and a stale `redis.auth.password` no longer wins over + the selected Secret. ([#7161]) + +- **frontend:** Keep the collapsed sidebar's navigation reachable on mobile: + the stock sidebar rendered only a modal drawer there, so with the drawer + closed the workspace lost its chat list, plugin and channel navigation and + had no visible trigger to reopen it. A `WorkspaceSidebarFrame` now keeps + an icon-width collapsed rail mounted while the mobile drawer is closed, + closes the drawer on same-origin navigation to release its focus trap, and + restores focus to the opener or to the rail's trigger; the header trigger + is always visible on mobile. Only one content tree mounts at a time, + `ProjectsSection` and `RecentChatList` follow the drawer's expanded state + instead of the saved desktop preference, and the Agents page header + actions wrap within the mobile viewport. ([#7198]) + +- **sandbox:** Escape mapped paths when substituting them into PowerShell + commands: the local sandbox resolved `/mnt/user-data` container paths to + host paths and spliced them in raw, so a host path containing a space or a + PowerShell metacharacter (`$`, `&`, quotes) broke or reparsed the command. + `escape_powershell_path()` now reads the caller's quote context — including + here-strings, comments and subexpressions — and doubles quotes inside + single-quoted strings or backtick-escapes metacharacters in double-quoted + and unquoted positions, while preserving wildcards and a caller's closing + quote. Both the sandbox command resolver and + `replace_virtual_paths_in_command()` apply it. ([#7245]) + +- **client:** Publish converted Markdown companions without replacing an + existing file of the same name: `upload_files` wrote a companion through + the existing name whenever the basename was already present at call start, + so uploading `report.xlsx` after a `report.md` — user-authored or left by + an earlier upload call — overwrote that Markdown's content. Companions now + always publish no-replace and register the name the bytes actually landed + under, so the existing Markdown is preserved and the new companion takes + the next free `_N` name, while re-uploading a document still replaces the + original upload and receives a fresh companion. ([#7054]) + +- **runtime:** Retain assistant output from interrupted runs: streamed text + never reached persisted history because `llm.ai.response` events were + produced only by `on_llm_end`, which a run interrupted mid-generation + never fires, so the persisted message history lost text the client had + already seen. The worker now accumulates streamed root assistant text + while the run streams and, once the run resolves to `interrupted`, + `RunJournal.record_interrupted_ai_responses()` emits the pending responses + as feed events tagged `interrupted` and `partial` in metadata; they do not + enter checkpoint state, hidden messages are skipped, and a response the + callbacks already persisted wins by message identity. ([#7153]) + +- **gateway:** Include active task messages in history search results: + archived full-text matches carried a hard `LIMIT 8` and active messages + were appended only afterwards, so once eight archived rows matched, live + messages that also matched were truncated out of the results. The archive + query now takes its limit as a parameter, active matches are collected + first and fill the result window, and archived rows deduplicate against + them, preferring the active copy's text when a source exists in both. + ([#7169]) + +- **skills:** Decode and header-check MiniMax music audio before touching the + output path in `music-generation/scripts/generate.py`: the hex payload is + decoded up front, empty bytes raise "MiniMax audio decoded to empty bytes", + and responses without an ID3 tag header or MPEG frame signature + (`_has_mp3_header`) raise "MiniMax audio does not have an MP3 header" — + before the output directory is created or the file is opened. Previously a + malformed response hit `bytes.fromhex` only at write time, after + `open(output_file, "wb")` had truncated any existing output; a valid + generation flow is unchanged, and the signature check is a sanity check, + not audio decoding. ([#6688]) + +- **skills:** Align `skill-creator`'s `quick_validate.py` with the runtime + skill schema so it stops rejecting skills the loader accepts and starts + rejecting ones it drops: `SKILL.md` is read with `utf-8-sig` (also in + `utils.parse_skill_md`, so BOM files that pass validation do not break + `run_eval`/`run_loop`), frontmatter delimiters may carry trailing + whitespace, and the DeerFlow fields `argument-hint`, `required-secrets`, + `secrets-autonomous`, `version`, and `author` are allowed. + `allowed-tools` now gets the runtime parser's shape rules (non-string list + items, empty tool names, unmatched closing parentheses, unclosed quotes, + unclosed parenthesized patterns) and `required-secrets`/`secrets-autonomous` + shapes are checked like the runtime validator, while empty + `name`/`description` are rejected and the `compatibility` type/length + check is dropped because the runtime never inspects that field. Non-string + YAML keys are stringified before the unexpected-key check instead of + raising `TypeError` in `sorted()`. ([#7180]) + +- **sandbox:** Surface permission errors from local `list_dir` and glob walks + instead of reporting an empty successful result: `list_dir` no longer + swallows `PermissionError` per directory, and `find_glob_matches` passes + `os.walk` an `onerror` handler that re-raises anything outside the + `_GREP_RACE_ERRNOS` walk-race allowlist, so only transient filesystem + races still degrade silently. `LocalSandbox.list_dir`/`glob` reverse-resolve + `PermissionError.filename`/`filename2` back to container paths so + `ls_tool`/`glob_tool` report the denied path with `Error: Permission + denied:` rather than always naming the requested root, keeping custom-mount + host paths out of the message. ([#7234]) + +- **reflection:** Reject malformed variable paths in `resolve_variable` with + the path-format ImportError before any import or diagnosis is attempted. + Previously `"json:"` reached `getattr(module, "")` and reported "Module + json does not define a attribute/class", `"json:loads:extra"` left the + extra colon in the module half and was misdiagnosed as a missing + dependency with an unusable `uv add json:loads` hint, a leading-dot path + crashed with `TypeError` from `import_module` (callers only catch + `ImportError`), and stray whitespace or a trailing dot advertised + distributions like `json.` or `json ` in the rendered hint. All these + shapes now get the same format guidance; well-formed paths are unchanged. + ([#7016]) + +- **scripts:** Locate Git Bash from the Git installation root in + `scripts/run-with-git-bash.cmd` instead of assuming `git.exe`'s parent + holds `bin\bash.exe`: the wrapper now also probes one directory above, so + PortableGit and GitHub Desktop layouts that keep `git.exe` in + `<root>/mingw64/bin` find `<root>/bin/bash.exe` instead of reporting + "Could not locate Git for Windows Bash" and breaking every Makefile recipe + that runs a repository shell script. `backend/tests/support/shell.py` + mirrors the same two-root probe (`_git_roots`) so the shell-script suites + stop skipping on those hosts. ([#7214]) + +- **scheduler:** Treat only `ConflictError` and `BusyThreadConflict` as a + retryable overlap conflict in `ScheduledTaskService._is_overlap_conflict`. + `start_run` also raises plain HTTP 409s for permanent failures such as + idempotency-key and thread/assistant mismatches, and the scheduler used to + requeue the occurrence for any 409, retrying a launch that can never + succeed until `scheduler.queue_timeout_seconds` elapsed; a plain 409 now + terminalizes the occurrence as failed with the 409 reason persisted as the + failure detail. `BusyThreadConflict` is imported inside the classifier so + the scheduler module keeps no load-time dependency on + `app.gateway.services`. ([#7196]) + +- **image-search:** Report DuckDuckGo image-search failures as search errors + instead of empty results: `_search_images` propagates exceptions to the + async boundary, where `image_search_tool` returns + `{"error": "DuckDuckGo image search failed: <type>: <message>", "query": ...}`, + and only a returned empty collection yields `No images found`. Previously + every failure (missing `ddgs` install, timeout, rate limit) was logged and + swallowed as `[]`, so the model read a failed search as proof the query + has no matching images; the SDK's "No results found." sentinel stays an + error because DDGS 9.14.1 can also raise it when a backend misses the + dispatcher deadline. ([#6680]) + +- **mcp:** Keep durable control claims alive for the whole poll or cancel + call: each control operation verifies and extends its claim via the new + `renew_control_claim` before calling the driver and renews on a heartbeat + every third of `mcp_tasks.lease_seconds`. Previously a status or + cancellation request slower than the claim lease had its snapshot apply + rejected as expired — discarding terminal results — and another worker + could reclaim the row mid-flight. A failed, timed-out, or rejected renewal + now stops the local call, an ownership-loss fence (`_require_control_lease`) + keeps a cancellation-swallowing driver from saving later snapshots or retry + failures, and `_apply_cancel_snapshot` discards results written against a + lost lease. Cancel-claim transactions take SQLite's writer with + `BEGIN IMMEDIATE` so two deferred scans cannot claim the same due row, and + draining calls keep their capacity slots and are excluded from later + claims via `exclude_task_ids` until their children settle. No schema + migration or new configuration; `mcp_tasks.lease_seconds` remains a + crash-recovery window, not a remote-call timeout. ([#6780]) + +- **channels:** Recover from invalid UTF-8 in the channel thread-binding + store the same way as malformed JSON: `_read_legacy_file` in + `backend/app/channels/store.py` now catches `UnicodeDecodeError` and + applies the caller's `on_corrupt` policy, so the SQL migration warns and + skips the file (leaving it unchanged so channels can start) and the + memory JSON store warns and starts empty, replacing the file on its next + write. Previously an undecodable `channels/store.json` escaped store + loading as an unhandled `UnicodeDecodeError` instead of degrading. + ([#7233]) + +- **harness:** Complete Lark device authorization through a single-flight + login run owned by the flow generation: the first `/lark/auth/complete` + poll spawns one background `lark-cli auth login` per (user_id, generation) + and later polls attach to it, so `wait_timeout_seconds` only bounds the + caller's wait (the 504 contract is unchanged) and never kills the + subprocess. Previously each poll ran the CLI under that timeout, and a + kill after the single-use device code was redeemed but before credentials + persisted stranded the flow — every later poll failed with "device_code + is invalid". A finished run's outcome stays attachable for 120 seconds, + advancing the generation or re-issuing a device code abandons the old + process, and durable marker/result files (owner pid plus start time with + a 10-second owner-alive publication grace) let another worker attach to a + completed attempt instead of respawning a redeemed code. Credential-tree + walks tolerate entries that vanish mid-hardening, and result publication + is serialized under the credential lock. ([#7182]) + +- **browser:** Preserve Shift and Alt modifiers when forwarding keys from the + Browser Live view: `browser-view-panel.tsx` now passes `shiftKey` and + `altKey` alongside ctrl/meta, and `decideBrowserKeyInput` composes the + full chord via `keyChord` for both single-character chords and forwarded + named keys. Previously a chord was collapsed to `Meta+X`/`Control+X` + alone and a named key was forwarded bare, so shortcuts like Shift+Enter + or Alt+Arrow lost their modifiers before reaching the page. ([#7213]) + +- **scripts:** Stop only sandbox-named containers in + `scripts/cleanup-containers.sh`. Docker's `name=` filter matches + substrings, so `--filter "name=deer-flow-sandbox"` could stop an + unrelated container whose name merely contained the text; the script now + queries `deer-flow-sandbox-` and keeps only names matching the anchored + `"$PREFIX"-*` pattern before stopping, and the Apple Container path + matches `configuration.id` with `startswith` instead of a substring test, + passing the prefix through the environment rather than string + interpolation. Containers with the prefix elsewhere in their names are + left alone. ([#7189]) + +- **workflows:** Let sequential Agent handoffs use the same 16,000 + serialized JSON bytes for task inputs that file-reference tasks already + got (`AGENT_TASK_BYTES` in + `examples/deerflow-extension-workflows/.../service.py`); an ordinary + Agent task was capped at 8,000 bytes, too small to pass a saved + 12,000-byte step output to the next Agent with a short instruction. Step + outputs stay capped at 12,000 bytes (`AGENT_OUTPUT_BYTES`), JSON escaping + counts toward both limits, and oversized tasks are still rejected before + admission. ([#7231]) + +- **github-research:** Raise `GitHubAPIError` from `get_readme` and + `get_file_content` when a fetch fails instead of returning marker strings + such as `[README not found: HTTP 404]`. Previously every failure reached the + caller as a string that `main()` printed on stdout with exit 0, so a + rate-limited run, a missing repository, and a real README body were + indistinguishable to anyone reading stdout. Failures now reach the CLI's + existing exception handler, which names the operation, the `owner/repo` + target, the file path, and the HTTP status on stderr and exits 1, keeping + the success channel empty; successful fetches are unchanged and a 403 rate + limit no longer reads as a missing repository. Both the `requests` transport + and the `urllib` fallback raise the same way. ([#7221]) + - **mcp:** MCP servers using the OAuth `refresh_token` grant keep working across Gateway restarts, tool-cache resets, repeated personal-connection runs and multiple Gateway instances when the provider rotates refresh tokens @@ -1634,6 +3577,37 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. an enabled `refresh_token` OAuth server now refuses to start without `DEER_FLOW_CREDENTIALS_KEY`. ([#7207]) +- **channels:** Pin `encoding="utf-8"` and `newline=""` on every IM channel + JSON state write — `JsonChannelStore._save`, + `ChannelRuntimeConfigStore._save`, `BuzzSeenEventStore._write_snapshot`, + Discord thread mappings, and the WeChat cursor and auth-state writers — and + read Discord's thread store back with an explicit UTF-8 encoding. + Previously these calls inherited the platform locale and newline + translation, so state persisted on Windows carried CRLF line endings and + locale-encoded text, and non-ASCII channel, thread, and user IDs could fail + to encode or decode on a host with a different locale. A new AST-based test + pins the keywords at each writer and reader call site inside its owning + method, so an unpinned call cannot hide behind unrelated module settings. + ([#7228]) + +- **config:** Reconfigure stdout with `errors="replace"` at the start of + `doctor.main` and `setup_wizard.main`, preserving the stream's encoding so + legacy-locale terminals and pipes (Windows cp936, ASCII) can finish the + health report and setup instead of aborting with `UnicodeEncodeError` on + status symbols such as `✓`; the `reconfigure` call is guarded for embedded + streams that lack it. `configure.py` also drops the `✓` from its success + line, and new subprocess tests drive all three scripts under ascii, cp1252, + cp936, and utf-8 stdout. ([#7102]) + +- **channels:** Release standalone temporary sandboxes after DingTalk upload + syncs by passing `release_on_last=True` to the shared `sandbox_files` sync, + matching Feishu: the execution holder is released after the last sandbox + operation and requests provider release instead of parking the sandbox. + Previously DingTalk retained its non-releasing holder behavior, so each + upload to a non-mounted sandbox left a standalone temporary sandbox behind. + Mounted sandboxes and the shared-client protection for parallel runs are + unchanged. ([#7203]) + - **community:** Reject booleans in numeric LightRAG and RAGFlow `knowledge_search` settings instead of silently treating `true` as a one-second timeout, one result or one character of output. Invalid settings @@ -2300,7 +4274,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `channel_conversations` / `channel_thread_bindings`: the instance that loses adopts the winner's thread and deletes its own empty one, and `/new` still rebinds. (issue #7103, [#7104]) - + - **skills:** A skill the agent creates, edits, patches or deletes with the `skill_manage` tool now reaches every Gateway process sharing the home volume, as a change made through the skills API already did. The tool @@ -2312,14 +4286,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. this or a peer process serving the stale list. When the marker cannot be written, the tool reports that the change was applied but other processes were not notified, instead of reading as a failed write. ([#7099]) - + - **provisioner:** When two provisioner replicas start together and `K8S_NAMESPACE` does not exist yet, the one that loses the create race no longer crashes at startup: its `409 AlreadyExists` now counts as the namespace existing, as the Pod and Service creates already did. Helm deployments, where the provisioner runs inside the namespace it manages, never took this path. ([#7099]) - + - **projects:** A shelf upload or save-from-thread into a project the caller cannot write no longer leaves a directory behind or answers `500`. Both routes staged the body under `users/{user_id}/projects/{project_id}/` before @@ -2329,7 +4303,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. now check for an owned, active project first and answer the same `404` without writing; the shelf insert still re-checks under the project row lock. ([#7040]) - + - **skills:** Toggling a skill when no `extensions_config.json` exists yet now creates the file in the project root (`DEER_FLOW_PROJECT_ROOT`, else the working directory), where the MCP editor already creates it and where config @@ -2350,7 +4324,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. cancellation, faults and worker exits every 20 ms) without exchanging frames, so idle waits no longer consume turns and `timeout_seconds` is the only wall-clock bound; scripts that spin the job queue still hit the turn cap. (issue [#7022], [#7024]) - + - **browser:** `browser_screenshot` no longer replaces a file that already exists in the thread's outputs directory. A screenshot saved as `report.png` used to overwrite the user's own `report.png`, an earlier @@ -3694,6 +5668,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. deferred messages hold a slot of the bounded inbound queue (`channels.inbound_queue_maxsize`), so a database outage cannot grow the backlog, and one that cannot be admitted is dropped with a warning. ([#6558]) + - **runtime:** The JSONL event store no longer loses events after a torn final line. A write interrupted mid-record left the file without a trailing newline, so the next append was glued onto the partial record and both became one @@ -3703,12 +5678,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. newline, reads decode each physical line on its own and skip only the broken one, and a failed batch append still truncates back to the original size. ([#6520]) + - **channels:** `/goal <objective>` and `/goal clear` from an IM channel work again when Gateway auth is enabled (the default). Both sent their write with the internal auth token alone, and the Gateway's CSRF check, which does not exempt internal auth, answered 403, so the channel replied "Failed to set goal." or "Failed to clear goal." They now send the same CSRF cookie and header pair as the channel's SDK client. `/goal` status was unaffected. ([#6537]) + - **auth:** In the Docker stack, five wrong passwords from one client no longer lock every user out of login for five minutes. Failed logins are counted per client IP, and the Gateway honors `X-Real-IP` only from a peer listed in @@ -3723,6 +5700,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. exports it for Compose interpolation like the proxy variables. Deployments behind another reverse proxy also need nginx's `real_ip` module for that proxy; see `.env.example`. ([#6519]) + - **scheduler:** "Run once now" on a one-time task before its run time no longer cancels the scheduled run. The trial launched as the task's own run: the task was marked `running`, and the trial's outcome then finished it (`completed`, @@ -3734,6 +5712,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the task's run. A trial whose launch bookkeeping lands after the poller has claimed the now-due task no longer clears that claim's lease, which made the claim's admission fail and left the task `running` with nothing scheduled. ([#6512]) + - **auth:** Login lockouts are now counted once per client IP across every Gateway replica. `POST /api/v1/auth/login/local` kept its failed-login counter in a per-process dict, so with N replicas behind one load balancer an @@ -3757,6 +5736,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. keeps `memory` logs a startup warning. Status codes and messages of the login endpoint are unchanged; `max_login_attempts` and `lockout_seconds` stay live-read. `config_version` is now 57. ([#6501]) + - **memory:** DeerMem's derived SQLite FTS5 retrieval index can now live outside the memory root, and a Gateway instance now notices facts another instance wrote. The index for every user was one SQLite database in WAL mode @@ -3777,6 +5757,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. a Pod-local `emptyDir` at `/var/lib/deerflow/memory-index`, points the key at it, and drops the legacy `memory.storage_path: memory.json` line that the Gateway discarded with a warning at every start. `config_version` is now 56. ([#6494]) + - **skills:** A `/skill-name` activation now survives a retried model call. The activation was marked as done before the model was called, so when the call failed (rate limit, overload, timeout) or came back empty and was retried, the @@ -3784,6 +5765,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. still applied. The retry now carries the same reminder as the first attempt, without re-reading the skill or recording a second activation, and the retried response keeps the skill-usage record. ([#6506]) + - **skills:** Skill changes made through one Gateway replica now reach the skill list in every other replica's system prompt. `SkillStorage` rescans disk on every call, but the prompt layer caches the enabled-skills list and @@ -3802,6 +5784,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. operators no longer need to call it on every Pod or worker separately. The MCP cache reset's marker now shares the same `deerflow.config.shared_reset_marker` helper. ([#6495]) + - **persistence:** `scripts/migrate_user_isolation.py` now moves each legacy thread to the user who owns it. It looked for thread owners in `{base_dir}/deer-flow.db`, a file DeerFlow never creates (the database is @@ -3813,6 +5796,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `default` instead, for installs that never recorded thread owners. Installs that already ran the old script can recover their threads with the steps in `docker/provisioner/README.md`. ([#6450]) + - **sandbox:** A failed command in the e2b sandbox now keeps its output and exit code. The e2b SDK raises `CommandExitException` on a nonzero exit instead of returning a result, so `E2BSandbox.execute_command` returned @@ -3825,6 +5809,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. sandbox as reaped. `list_dir` on e2b handles the same exception, so a missing directory raises `FileNotFoundError` and a listing truncated at 500 entries (SIGPIPE 141) is returned instead of failing with `OSError`. ([#6441]) + - **uploads:** The Gateway's startup sweep of orphaned `.upload-*.part` staging files now skips files younger than 24 hours. The sweep removed every staging file it found, which was right for one Gateway but not for several replicas @@ -3838,6 +5823,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. staged-name removal) is still reclaimed on the next startup at any age, so that destination does not fail the multi-link safety check on its next replacement. ([#6445]) + - **gateway:** `GET /health/ready` now reports unready while the Redis stream bridge is unreachable. The bridge's Redis client connects lazily and nothing pinged it, so a gateway whose Redis was down started, answered `200 ready` to @@ -3852,6 +5838,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. provisioner. A probe that overruns the endpoint deadline is now the only one marked unreachable, and the public probe gate is one lock per probe kind. ([#6447]) + - **frontend:** Retrying a message after its attachment upload fails now keeps the context that was attached to it. The composer dropped its quotes, conversation references, staged project files and stored draft as soon as a @@ -3861,6 +5848,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. upload. A send that finishes uploading after the user has switched conversations or left the page clears only the stored draft and staged files it carried, keeping a draft saved or a document attached since. ([#6412]) + - **client:** `DeerFlowClient.list_threads(limit)` now limits threads rather than checkpoints. It passed `limit` to a checkpoint scan across every thread, and one turn writes several checkpoints, so a single long conversation filled @@ -3871,6 +5859,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. follows checkpoint write order, so a goal-only write counts as activity. A new `sort_by="updated_at"` option lets `--continue` keep resuming the most recently active thread; the default order stays newest created first. ([#6426]) + - **client:** Goals now work in the TUI and embedded `DeerFlowClient` on the SQLite and Postgres checkpointers. Their synchronous savers define the async checkpoint methods but raise `NotImplementedError` from them, and the goal @@ -3878,6 +5867,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. goal." and `get_goal`/`set_goal`/`clear_goal` raised. The goal helpers now fall back to the synchronous methods for those savers. The web UI was not affected. ([#6448]) + - **community:** The Browserless `web_fetch` provider now authenticates. It sent the configured token inside the `/content` JSON body, but Browserless reads the token only from the `token` query parameter or the `Authorization` @@ -3887,6 +5877,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `TOKEN`, whose body schema rejects the unknown key) while `web_capture` worked with the same token. Both tools now send the token as a query parameter. ([#6484]) + - **mcp:** An MCP server with `task_toolsets` that is unreachable or times out during tool discovery no longer removes every MCP tool. Discovery skipped the failed server with an empty tool list, the task-toolset check then reported @@ -3896,6 +5887,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. stdio servers and re-requesting OAuth tokens. A server whose discovery fails is now skipped like any other failed server; a server that answers without its configured tools still fails as a configuration error. ([#6481]) + - **frontend:** A failed side-chat send no longer clears the composer. The side chat's submit handler showed the error toast and then resolved, which the composer treats as success, so the typed text and attachments were lost when @@ -3904,6 +5896,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. The handler now rejects after the toast, and the queued first send settles the submit with its own outcome, so the draft stays for a retry and clears only once the message is sent. ([#6407]) + - **sandbox:** AIO sandboxes no longer trust unverified state for credential placement or container reuse. A failed provisioner capability probe no longer reads as "broker off" (which bind-mounted plaintext lark credential dirs into @@ -3914,6 +5907,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. by a persistent quarantine (`{DEER_FLOW_HOME}/sandbox-quarantine`) across warm reuse, rediscovery and restarts, recycled under the existing ownership and teardown fences, with records retired only after confirmed absence. ([#6436]) + - **frontend:** A failed reconnect after a page refresh is now retried in the same tab. The SDK reconnects once from the tab's `lg:stream` pointer and keeps that pointer on error, and active-run recovery skipped any run with a matching @@ -3922,6 +5916,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and rejoins the run if the server still reports it active, with its existing bounded retries (immediately, then after 1s and 2s). Failed submitted runs are unchanged. ([#6400]) + - **channels:** Opening an IM-channel conversation on the web while its run is still going no longer shows the user's message twice. Channel run input carried no message id, so the Gateway stored it id-less in the run record while the @@ -3929,12 +5924,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. hydration, which matches the two copies by id, kept both until the run finished. Channel human messages now carry their own id, so both copies share it. ([#6401]) + - **deploy:** `make up`, `make down` and `make prod-logs` no longer stop on a fresh checkout with `env file .../.env not found`. `.env` and `frontend/.env` are gitignored and `make up` does not create them, but the production compose file required both. Its `env_file` entries are now optional, as in the development compose file: Compose loads them when present and skips them otherwise. This needs Docker Compose 2.24 or newer, the floor the README already documents. ([#6370]) + - **memory:** DeerMem relevance ranking no longer counts punctuation as query terms when jieba is installed (`memory-zh` extra). `tokenize()` dropped only whitespace from `jieba.cut`, which emits `,`, `。`, `,` and `!` as standalone @@ -3943,6 +5940,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. 128-token budget before later query terms. Tokens without a letter or digit are now dropped, as the no-jieba fallback and the FTS5 query filter already did. ([#6388]) + - **gateway:** Run streams now arrive incrementally behind compressing proxies. Every SSE response (`POST /api/threads/{id}/runs/stream`, `GET .../join`, `GET`/`POST .../runs/{run_id}/stream` and `POST /api/runs/stream`) sends @@ -3951,6 +5949,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. proxy used by `pnpm start` without nginx, no longer buffer the stream and deliver it in bursts. `X-Accel-Buffering: no` and `Content-Location` are unchanged, and nginx deployments behave as before. ([#6393]) + - **config:** Every Gateway process that shares one `extensions_config.json` now sees the MCP and skill changes made by another one. The parsed file was cached once per process and only the process that handled the write @@ -3965,6 +5964,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. revision (for example midway through the non-atomic overwrite fallback on a bind-mounted file) keeps the previous configuration and is logged once; a broken file at startup still fails loudly. ([#6386]) + - **middleware:** Tool-output budgeting no longer hides a failed shell exit from subagent evidence. A bash result between `externalize_min_chars` (12,000) and the sandbox limit (20,000) was replaced by a preview ending in its `Access:` @@ -3977,6 +5977,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `fallback_max_chars` budget the way sandbox truncation does. Only `bash` and `bash_tool` results are affected; the persisted full output is unchanged. ([#6354]) + - **make:** `make clean` now says what it deletes and refuses to run under a live Docker Gateway. `make help` described it as cleaning up "temporary files", but it deletes `backend/.deer-flow`: the local database, users, threads, uploads, @@ -3986,6 +5987,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. deletion now name the directory, and `make clean` stops with a hint to run `make down` or `make docker-stop` while that container is running, before it stops anything. ([#6351]) + - **tui:** A new prompt no longer races an interrupted run in the same conversation. After `Ctrl+C` the run's worker keeps going until its current step returns, so a tool call already in progress (a long shell command, for @@ -3994,6 +5996,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the new turn could silently disappear from it. Until the interrupted worker returns, a prompt in that conversation now shows a notice instead of starting a second run; other conversations stay available through `/new` and `/resume`. ([#6350]) + - **helm:** A default Helm install can create sandboxes again. The chart enables the sandbox provisioner and points `config.sandbox.provisioner_url` at it, but nothing rendered `PROVISIONER_API_KEY`: the provisioner @@ -4013,6 +6016,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. empty string, so the default `config` still loads and the gateway boots as before even when a user-managed provider Secret holds only model keys. docker-compose was unaffected: it reads the key from `.env`. ([#6365]) + - **sandbox:** With host bash enabled, the local sandbox no longer keeps a thread on the skill view of the last restricted Agent that ran there. That view is only maintained while host bash is off, but `LocalSandboxProvider` @@ -4020,6 +6024,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the old allowlist and could not read other enabled or newly added skills under `/mnt/skills`. Those runs now use the shared skill views; the thread view is kept and enforced again once host bash is turned off. ([#6344]) + - **sandbox:** The local sandbox no longer rewrites line endings. `read_file` translated CRLF to LF, so `str_replace` on a CRLF file wrote every line back as LF, and the read-before-write gate could not see a change that only touched @@ -4033,6 +6038,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. tool output saved under `outputs/.tool-results/` was likewise written as CRLF, no longer matched its stamped blob reference, and was deleted on the next model call when no blob store was configured; it is now written byte-exact. ([#6343]) + - **skills:** Editing a custom skill no longer runs filesystem work on the event loop. `PUT /api/skills/custom/{name}` built the user-scoped skill storage, probed the custom, public, legacy and integration roots, validated the @@ -4042,6 +6048,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. rollback route. The custom-skill delete and archive-install routes and the agent's `skill_manage` tool, whose storage lookup stats `config.yaml` on every call, also build their storage off the loop. ([#6332]) + - **gateway:** The knowledge retrieval catalog no longer blocks the Gateway event loop while it loads a custom agent's config. Both `GET /api/knowledge/retrieval-catalog/datasets` and @@ -4051,6 +6058,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. every other request. The load now runs in `asyncio.to_thread`, like the other Gateway routes that read agent configs; responses and the 404 for an unknown agent are unchanged. ([#6313]) + - **gateway:** Deleting a thread with a large workspace no longer freezes every other Gateway request while its files are removed. `DELETE /api/threads/{id}` ran `shutil.rmtree` over the thread directory on the event loop, so other @@ -4059,6 +6067,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. volumes). The removal now runs on the file-IO pool, and a cancelled request keeps its thread reservation until the removal finishes, so no new run can start on a thread whose files are still being deleted. ([#6319]) + - **sandbox:** Medium-risk audit warnings no longer hide a failed shell exit from subagent evidence. `SandboxAuditMiddleware` appended its warning after the trailing `Exit Code: N` marker and rebuilt the `ToolMessage` from four fields, @@ -4068,6 +6077,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. before a trailing `Exit Code: N` or `Command exited with code N`, an output that is only `Command exited with code N` is left unchanged, and the result keeps `deerflow_tool_meta`, `artifact` and `id`. ([#6307]) + - **deploy:** The Helm chart and docker-compose no longer ship a multi-replica trap. The chart's default `config` enables `run_ownership.heartbeat_enabled` and `run_events.backend: db` (so a starting Pod only reclaims runs whose @@ -4094,6 +6104,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. Upgrading a release that predates `AUTH_JWT_SECRET` generates a new key and signs every browser session out once; the chart README shows how to seed the previous key into the Secret first to keep sessions. ([#6347]) + - **deploy:** The Helm chart no longer pins the sandbox provisioner to one Pod. `provisioner.replicas` (default 1, unchanged) sets the replica count, the provisioner Deployment gets the gateway's surge-then-drain rollout strategy @@ -4111,6 +6122,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. in the pre-existing gateway budget, silently turned it into `0`, a budget that protects nothing) and fails the render for `0`, `"0%"` or any other unsupported value with a message naming the values key. ([#6543]) + - **persistence:** A second Gateway instance no longer fails startup with `TimeoutError` while another instance runs a PostgreSQL schema migration. The bootstrap advisory lock was taken with a blocking `pg_advisory_lock` on the @@ -4120,6 +6132,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `pg_try_advisory_lock`: the wait lasts as long as the holder's migration, each attempt stays bounded by `command_timeout`, and the wait is logged once. ([#6306]) + - **projects:** Reading a shelf document for the first time no longer blocks every other database write on SQLite while the document converts. Lazy conversion ran pymupdf/markitdown inside the `BEGIN IMMEDIATE` transaction @@ -4131,6 +6144,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. into place, so a document trashed or purged meanwhile still publishes nothing. Concurrent first reads of one document share a single conversion rather than each holding a file-IO worker. ([#6305]) + - **gateway:** Per-run reads now return the rows of IM-channel owners. `start_run` stamps run rows and run events with the raw trusted owner id (for example `feishu:owner-777`), but several run-scoped routes filtered by the @@ -4142,6 +6156,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. reads now use the same data identity as the thread message routes (#5448), and every event store accepts `user_id` on `list_messages_by_run()`. Browser and API sessions keep their per-user filter. ([#6282]) + - **runtime:** A multi-worker run that finished successfully is no longer reclaimed as an orphan `error` while its worker is still finalizing. With an event store, the worker records the terminal status in memory first and @@ -4152,6 +6167,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. reconciler, claim the still-active row. The heartbeat now keeps renewing until that deferred write is attempted, and fences the run if a peer claims it. Affects only `run_ownership.heartbeat_enabled` deployments. ([#6263]) + - **channels:** Buzz now follows a thread without a fresh mention for authors bound with `/connect`. With `channel_connections.enabled`, the manager maps a bound author's threads only in the connection repository, but Buzz's @@ -4164,6 +6180,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. check had its own copy of the same read and is fixed with it: a bound user with no thread yet was checked against the agent of a legacy JSON thread for the same chat and could be told an enabled skill was not available. ([#6232]) + - **memory:** Reading DeerMem agent memory no longer fails while another write deletes a fact. `load()`, `reload()`, and the full `rebuild_index()` scan list the fact files without the storage locks, so a delete committed between the @@ -4174,6 +6191,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the fact as failed. A fact that vanishes after the listing is now treated as deleted; an entry that is still present but unreadable, such as a dangling symlink, is still reported as corruption. ([#6255]) + - **memory:** A DeerMem memory reload no longer pins an older document in the cache. `reload()` read the document before computing its cache signature, so a write committed in between (for example by the background memory updater) @@ -4181,6 +6199,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. returned the outdated memory until the next write. `reload()` now computes the signature first, as `load()` already did, so a racing write forces a re-read instead. ([#6238]) + - **channels:** Discord now runs its channel-connection database work on the Gateway event loop. discord.py delivers messages on a private loop in the client thread, and the Discord adapter awaited the connection repository there @@ -4210,6 +6229,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. metadata address still report as global. Operators who intentionally fetch tailnet or CGNAT hosts with these tools must now set `allow_private_addresses: true`. ([#6202]) + - **browser:** The agentic browser can no longer be steered to a private or cloud-metadata host by a DNS answer that changes after the SSRF check. The navigate screen and the per-request guard resolve a hostname to vet it, but @@ -4222,6 +6242,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. WebRTC UDP does not traverse the proxy and is not covered. CDP-attached Chrome is unchanged, and delegated fetch services (crawl4ai, Browserless, fastcrw) still resolve on their own side, which the Gateway cannot pin. ([#6201]) + - **channels:** The Discord typing indicator is now actually sent while the agent works on a reply. `_start_typing()` called `channel.trigger_typing()`, which discord.py removed in 2.0 (the project requires `>=2.7.0`), and its loop @@ -4230,6 +6251,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. indicator. A typing loop's first failed tick is logged at WARNING (a missing permission or sustained rate limiting is visible at the default level) and later ticks at DEBUG, instead of being dropped. ([#6138]) + - **community:** The SSRF URL guard no longer stalls the Gateway event loop while it resolves a hostname. `validate_public_http_url` resolves with blocking `socket.getaddrinfo`, and the crawl4ai and Browserless `web_fetch`, @@ -4244,6 +6266,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. strict blocking-IO gate gains a `socket.getaddrinfo` rule, because Blockbuster's defaults wrap socket methods but not the module-level resolver. ([#6140]) + - **agents:** Loop-detection integer thresholds now reject YAML booleans instead of coercing `true` to `1`. A configuration such as `warn_threshold: true` and `hard_limit: true` previously made the first tool-call set meet the hard @@ -4252,6 +6275,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. limits to one. All integer threshold fields now fail configuration loading with a field-specific error while valid integers and numeric strings retain their existing behavior.([#6017]) + - **agents:** App-config integer settings now reject YAML booleans instead of coercing `true` to `1`. A configuration such as `recursion_limit: true` previously made every Gateway run that does not supply its own limit hit the @@ -4260,6 +6284,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. backoff delays) collapsed retries and the concurrency cap to one. All seven integer fields now fail configuration loading with a field-specific error while valid integers and numeric strings retain their existing behavior.([#6171]) + - **uploads:** Converted Markdown ownership is now recorded when a document is converted. `list_uploaded_files` hides only verified conversion outputs, and document outlines use only the recorded companion; a user-uploaded Markdown @@ -4269,6 +6294,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. files appear separately in historical listings and their source documents have no inferred outline or preview. Re-uploading the source creates a new verified conversion when automatic conversion is enabled. ([#6101]) + - **agents:** A retried model call in plan mode no longer loses the todo completion reminder that `TodoMiddleware` had queued for it. The middleware drained the reminder inside `wrap_model_call`; because @@ -4279,6 +6305,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. carries it and the cap still allows the second reminder. Successful calls are unchanged, and a run whose reminder state was cleared meanwhile does not get it back. ([#6132]) + - **channels:** Stopping or restarting the Slack, Feishu, DingTalk or Discord channel no longer freezes the Gateway event loop. `SlackChannel.stop()` called `SocketModeClient.close()` inline, which joins the SDK's message-processor @@ -4292,6 +6319,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `POST /api/channels/{name}/restart`. The teardown now runs in a worker thread; Slack's close is shielded and tracked, so a cancelled shutdown leaves it running and a retried `stop()` awaits it instead of closing twice. ([#6134]) + - **middleware:** `_externalize_to_sandbox` now validates the full byte count of externalized tool outputs instead of only testing non-emptiness with `test -s`. When a remote sandbox write truncated the file part-way (e.g. disk @@ -4309,12 +6337,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `app.gateway.utils.constant_time_equals`, and the standalone provisioner encodes inline. No bypass was possible; the request was already failing, just with the wrong status. ([#6076]) + - **agents:** Context-compaction fraction triggers and fraction-based retention now use the active run model's context profile; a separate `summarization.model_name` remains generation-only. This prevents mismatched run and summary windows from compacting too late or too early. The middleware release identity now records `profile_model` separately from `summary_model`, intentionally refreshing the identity when either owner changes. ([#5566]) + - **events:** Run-scoped reads no longer return 500 on the JSONL backend for a run ID it cannot use as a filename. `GET /api/threads/{thread_id}/runs/{run_id}/events`, `.../messages`, and @@ -4323,6 +6353,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. while the memory and database stores return an empty result. JSONL reads and deletes now treat such an ID as an unknown run; writes still reject it. ([#6070]) + - **agents:** A run started with `"max_total_subagents": null` in its context now uses the configured `subagents.max_total_per_run` instead of failing with a `TypeError`. The key was present, so `dict.get(key, default)` returned @@ -4332,6 +6363,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. prompt now resolve the cap through one helper that treats `null` as unset and clamps to 1-50, so the extension-facing host policy and the release policy also report the enforced cap rather than an out-of-range request. ([#6088]) + - **scheduler:** Fixed-hour cron tasks no longer fire twice on the daylight-saving fall-back day. `croniter` returns both occurrences of an ambiguous wall-clock hour (the first with `fold=0`, the second with `fold=1`). For tasks where @@ -4339,6 +6371,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. to preserve once-per-day semantics (Vixie cron contract), while wildcard schedules (such as `0 * * * *`) still run in both occurrences of the repeated hour. (issue #6052, [#6066]) + - **persistence:** A SQLite `checkpointer.connection_string` written as a `file:` URI now fails at startup instead of silently writing somewhere else. LangGraph's SQLite checkpointer and Store open connection strings without @@ -4370,6 +6403,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `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. ([#6015]) + - **doctor:** `make doctor` now checks the config file the Gateway actually loads. It always inspected `<checkout>/config.yaml` and ignored `DEER_FLOW_CONFIG_PATH` and `DEER_FLOW_PROJECT_ROOT`, so a missing override @@ -4402,6 +6436,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the cadence worked, because that path computes a fresh datetime. The repository now coerces every serialized timestamp it accepts in `update()`, as `update_after_launch()` already did. ([#5964]) + - **gateway:** `GET /api/skills`, `GET /api/skills/custom` and `GET /api/skills/{name}` no longer walk the skill directories on the event loop. Each called `load_skills()` inline, which resolves the caller's @@ -4413,6 +6448,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. a large or slow skills tree stalled every other request on the worker for the duration of the scan. All three now offload the load with `asyncio.to_thread` through one shared helper. ([#5945]) + - **gateway:** `GET` and `PUT /api/user-profile` no longer run their filesystem work on the event loop. Both handlers resolved the per-user `USER.md` path (which builds absolute paths on every call), stat'ed, read, @@ -4422,6 +6458,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `BlockingError`; in production a slow disk stalled every other request on the worker for the duration. Both handlers now offload the whole resolve-stat-read / resolve-mkdir-write sequence. ([#5935]) + - **docker:** Project-document uploads larger than 1 MB no longer fail with a bare nginx `413` through the unified entry point. `POST /api/projects/{id}/documents` is a multipart upload that Gateway accepts up @@ -4433,6 +6470,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `/api/projects/{id}/documents` its own location with the thread-uploads settings (100M ceiling, streamed request body) and the read timeout the catch-all already granted; the catch-all itself keeps nginx's defaults. ([#5934]) + - **docker:** The production stack (`make up` / `scripts/deploy.sh`) now starts on hosts with IPv6 disabled. `docker/nginx/nginx.conf` also listens on `[::]:2026`; on a kernel booted with `ipv6.disable=1` that listen makes nginx @@ -4441,6 +6479,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `/proc/net/if_inet6` is absent since #2027, and the Helm chart mirrors it; the production compose file was the one launcher still without the guard. It now uses the same launcher, and starts nginx with `exec` so it is PID 1. ([#5900]) + - **deploy:** `make up` / `scripts/deploy.sh` now honors `BETTER_AUTH_SECRET` and `DEER_FLOW_INTERNAL_AUTH_TOKEN` written to the repo-root `.env`. The script only checked the shell before reloading a persisted secret or @@ -4456,6 +6495,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. count the way Compose counts them on every Compose v2 client. A value that resolves empty, like an exported-but-empty shell variable, still triggers generation because Compose would otherwise pass the empty value through. ([#5928]) + - **skills:** `skill_manage(action="remove_file")` and `write_file` now work on binary support files, and reject directories cleanly. A `.skill` archive may carry `assets/logo.png` (the installer only rejects *executable* binaries), @@ -4465,6 +6505,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. directory such as `assets` also slipped through path validation and raised `IsADirectoryError`. Non-text content is now recorded as no previous text, and a directory path is a validation error instead of a crash. ([#5893]) + - **models:** `when_thinking_enabled` and `when_thinking_disabled` no longer replace a profile's whole `extra_body` on the legacy (no `reasoning:` block) path. Both templates were applied with a shallow `dict.update`, so a profile @@ -4493,12 +6534,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. synthesized disable payloads and the contract path already behaved this way. Templates are also deep-copied as they are merged, so constructor kwargs never alias the cached profile. ([#5894]) + - **projects:** The conversation-files view no longer shows an empty heading for a member thread that has no title yet. A thread's `display_name` is `null` on the wire until title generation has run (or if it never does), but the file-group type declared it as a required string and rendered it as-is, so files uploaded before the first reply sat under a blank line. Such groups now read "Untitled", matching the project's thread list. ([#5879]) + - **skills:** Deleting a custom skill whose package directory is an operator-managed symlink now succeeds. The storage contract accepts a one-level link under `custom/`, but `delete_custom_skill` removed the @@ -4507,6 +6550,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. skill view was cleared on the way out, and `DELETE /api/skills/custom/{name}` answered 500. The link itself is now removed; the external tree it points at is never touched. ([#5881]) + - **frontend:** Stop mutating subtask render state during `MessageList` render. Subtask synchronization moved from render into an effect, so cards immediately receive pure message-derived snapshots even before the task context publishes @@ -4515,12 +6559,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. prompt. Message arguments supply title, prompt and subagent type; live state preserves lifecycle, model, usage and step history, with terminal snapshots keeping status/result precedence. ([#3157]) + - **skills:** Surface failed skill enable/disable requests in Settings instead of treating them as success. The frontend skills helpers parsed the response JSON without checking `response.ok`, so a 4xx/5xx from the skills list or toggle endpoints resolved as success and the UI flipped the switch even though the backend rejected the change. Non-OK responses now stay on the error path and show the backend error message. ([#3823]) + - **agent:** Align unattended-run prompting with the tool policy. Non-interactive runs removed `ask_clarification` from the toolset while the system prompt still demanded it for ambiguity, leaving GitHub, webhook, and scheduled runs unable @@ -4529,6 +6575,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. blocking information when context is insufficient; `scheduled`/`autonomous` make minimal reversible assumptions and report high-risk ambiguity as blocked. ([#4919]) + - **mcp:** Make the durable MCP task claim lifecycle cancellation-safe. Caller cancellation after a claim was acquired could leave rows leased until expiry, abandon a started release, or let cleanup failure replace the original @@ -4537,6 +6584,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. service-owned past the foreground deadline, and per-claim lease tokens reject stale-generation writes atomically (closing a SQLite race). Migration `0026` adds nullable lease-token columns; no public MCP API changes. ([#4966]) + - **frontend:** Keep streamed answer text out of the Thinking panel. A streaming AI message that already carried reasoning plus answer text was grouped into the processing disclosure until the turn settled, so the answer could sit in the @@ -4545,6 +6593,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. model-visible contract also describes cross-platform local environment discovery — start with `uname -s`, use `sw_vers` on Darwin, recover from blocked absolute paths with command-only probes. ([#5001]) + - **runtime:** Harden model response recovery at provider boundaries. An empty completed response could end a run with no usable answer, and length-capped responses could carry truncated tool calls into the loop. A true empty stop is @@ -4553,6 +6602,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. turn; a detected length cap drops all tool calls in that response, even with parsed arguments. DeepSeek thinking-mode history keeps `reasoning_content` on assistant tool turns. ([#5080]) + - **sandbox:** Reject overflowing sandbox ownership lease TTLs at config validation. `renewal_interval_seconds` and `ttl_multiplier` were each checked finite, but their product could overflow to `inf`, failing only at the first @@ -4560,6 +6610,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and sub-millisecond Redis TTLs are now rejected, Redis TTLs are capped at half the signed 64-bit millisecond range, and fractional milliseconds round up so the stored lease is never shorter than validated. ([#5104]) + - **subagents:** Close the subagent graph stream before releasing runtime resources. Cooperative cancellation could return a terminal result without explicitly closing the active `agent.astream()` iterator, so the executor @@ -4568,12 +6619,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. closed before terminalization and release, the original stream error or host cancellation wins if close also fails, and teardown still running after 10s logs a warning. ([#5221]) + - **composer:** Reserve `context` as a slash command alias. `/context compact` is a built-in composer command, but `context` was not reserved in the shared slash-skill contract, so a skill could claim the alias and shadow it. The shared contract now reserves `context`, frontend and backend slash parsing stay aligned through it, and IM channel command classification is unchanged. ([#5279]) + - **scheduler:** Preserve a one-time task's execution instant across title and prompt edits. The schedule control converted the stored timestamp to minute-precision local time and back on mount, so in New York @@ -4581,12 +6634,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. seconds and milliseconds — a metadata-only edit silently moved the run. The edit form now snapshots the original timestamp, local input, and timezone and re-emits the original `run_at` until the time or timezone changes. ([#5330]) + - **scheduler:** Reject wall times skipped by daylight saving in the one-time schedule form. New York `2027-03-14T02:30` converted to UTC and back as `01:30`, so create and edit could save an instant different from the preview. The form now validates local-to-UTC-to-local equality, shows localized inline feedback with the input marked invalid, emits an empty schedule spec until corrected, and keeps Create (and edit-save) disabled. ([#5348]) + - **sandbox:** Drain the previous sandbox release during async rebind. Async rebind paths released the previous sandbox client through a bare `await asyncio.to_thread(provider.release, ...)`; cancelling the caller cancelled only @@ -4595,6 +6650,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. three async previous-release paths now route through the cancellation-safe `run_sync_lifecycle_operation()` helper, which holds the serializer until the old release completes and then propagates the original `CancelledError`. ([#5498]) + - **subagents:** Close delegation-ledger entries a stopped run left in progress. On Stop, the `task` tool cancels the subagent and re-raises cancellation, so no `ToolMessage` is written and the ledger keeps the delegation `in_progress` — every @@ -4603,6 +6659,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `DurableContextMiddleware` now marks such entries `cancelled` ("cancelled attempt; may retry with a changed plan"). Resumed runs, goal continuations sharing the run id, and entries with a result are unchanged. ([#5507]) + - **frontend:** Keep clarification text outside the execution-steps panel. Answer text accompanying an `ask_clarification` call rendered inside the bordered execution panel, and submitting a hidden reply moved previously completed answer bubbles back into it @@ -4610,6 +6667,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. boundaries so earlier answers stay in the conversation across continuation and reconnect, and the accompanying text renders once outside the panel with the original messages, tool-result associations, and usage accounting preserved. ([#5508]) + - **projects:** Drain trash reconciliation file workers across Gateway shutdown cancellation. Cancelling the startup trash-retention sweep cancelled only the asyncio awaiter while its executor worker kept walking/statting/unlinking files, so @@ -4619,6 +6677,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. already-started worker reaches terminal state first, then cancellation propagates and later stages never start. The hook timeout stays the graceful-completion budget, not a hard cap after cancellation. ([#5511]) + - **mcp:** Insert bare-filename rewrites literally instead of as a regex replacement template. `_rewrite_unique_bare_filenames` passed the correlated `/mnt/user-data/...` path to `Pattern.subn` as a template, so a literal backslash in a filename (a model @@ -4628,6 +6687,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. — for escapes `re` accepts — silently substituted the escape byte (`\r` became a CR) into the returned path, riding it into the transcript and checkpoint. The replacement now goes through a callable, so the path is inserted verbatim. ([#5522]) + - **nginx:** Allow model-bound `/api/` and `/api/skills` requests to outlive 60 seconds. The catch-all `location /api/` (e.g. `POST /api/runs/wait`, which also cancelled the run when nginx dropped the upstream connection, and @@ -4636,6 +6696,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `proxy_read_timeout` default and failed with `504`. Both locations now set `proxy_read_timeout 600s`, matching the model-bound locations; applied to the Docker, `make dev`, and Helm nginx configs. More specific locations are untouched. ([#5524]) + - **subagents:** Drain the owned batch-service stop across caller cancellation. `SubagentRuntime.stop()` cleared its batch-submitter flag before awaiting `SubagentBatchService.stop()`; an interrupted await left the runtime reporting no @@ -4644,6 +6705,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. runtime's lifecycle ownership. The stop now runs in a strongly-owned task that is shielded and drained across repeated cancellation; the first caller `CancelledError` propagates only after the owned stop reaches a terminal state. ([#5525]) + - **memory:** Drain Gateway memory shutdown workers across lifespan cancellation. The flush and backend close ran as separate `asyncio.to_thread()` awaits, so cancelling the lifespan task cancelled only the awaiter: `manager.close()` could start while the @@ -4652,6 +6714,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. resolution, `shutdown_flush()`, and `close()` now form one owned shutdown operation drained through the cancellation-safe `await_drained()` helper; the first caller cancellation propagates only after the started work reaches a terminal state. ([#5531]) + - **sandbox:** Report an exactly-full sandbox search result as complete. `glob` and `grep` in the AIO, E2B, OpenSandbox, Tenki, and BoxLite providers treated "collected `max_results` matches" as `truncated=True`, so a tree holding exactly that many @@ -4659,6 +6722,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. search for nothing. The filtered-match cap now looks one match past the cap before deciding — a genuinely dropped match still reports truncated — leaving the matches themselves and the remote raw-output cap unchanged. ([#5534]) + - **threads:** Clean persisted records safely on thread deletion. `DELETE /api/threads/{id}` removed the thread directory, checkpoints, browser session, and meta row but left `run_events`, historical `runs` rows, and `feedback` behind — @@ -4669,6 +6733,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `operation_kind == "run"` rows so the in-flight delete reservation survives. Durable event stores take an explicit per-thread mutation fence, so a concurrently admitted writer can no longer commit a row for a deleted thread after deletion returns. ([#5535]) + - **frontend:** Rejoin active runs after reopening a chat. The SSE reconnect path only knew the run id stored in the current tab's `sessionStorage`, so reopening the browser or opening a new tab lost the pointer while the server-side run kept going. Opening a @@ -4676,6 +6741,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. persists the pointer before joining so stop/cancel still targets the right run, and rejoins its resumable stream, retrying a failed recovery twice with bounded 1s/2s delays; a matching pointer suppresses duplicate joins. ([#5536]) + - **channels:** Roll back a partially started channel service across startup cancellation. `start_channel_service()` published the global singleton before awaiting `ChannelService.start()`, so cancellation after partial resource acquisition exited @@ -4684,6 +6750,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. rollback across repeated cancellation with `await_drained()`; the singleton clears only after cleanup succeeds, and a failed cleanup retains it so later shutdown can retry. ([#5537]) + - **docker:** Add a `make prod-logs` entry point for the production stack and stop `make docker-logs` from exiting silently after a production start. `make up` runs compose project `deer-flow` while `make docker-logs` tailed the dev @@ -4693,6 +6760,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `deploy.sh` exports (without them compose cannot parse the production volume specs on checkouts without a `.env`); dev-only logs with no containers print a hint pointing at `make prod-logs`. ([#5538]) + - **frontend:** Preserve literal `<think>` tags in fenced, indented, and inline code during reasoning extraction, including unfinished code while streaming. The parser only protected an opener immediately following a backtick, so a @@ -4702,6 +6770,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. streaming reasoning after literal code is still extracted, and Markdown delimiters inside actual reasoning no longer prevent finding its closing tag, so an unfinished fence there cannot hide the answer. ([#5540]) + - **uploads:** Handle a UTF-8 BOM in uploaded Markdown when building document summaries. A leading BOM stopped the outline extractor from recognizing the first heading or opening code fence — in the fence case a code comment became @@ -4711,6 +6780,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. which consumes the optional BOM while preserving ordinary UTF-8, embedded U+FEFF characters, physical line numbers, and the original uploaded bytes. ([#5541]) + - **gateway:** Preserve confirmed clarification-card answers when preparing a thread regeneration. The regenerate endpoint only selected visible human messages, but clarification-card answers are stored as hidden `HumanMessage` @@ -4720,6 +6790,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `human_input_response` and preserves their hidden and request-correlation metadata; summaries, goal-control messages, malformed hidden responses, and unrelated internal messages remain rejected. ([#5544]) + - **sandbox:** Wrap the `remote_list_dir_command` and `remote_search_command` probe scripts in a subshell so their trailing `exit` cannot kill the AIO sandbox's implicit persistent shell. A bare `exit` terminated the session @@ -4729,6 +6800,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. runs that issued parallel `[ls, bash]` tool calls. Output markers and exit codes still propagate from the subshell; only the probe scripts change. ([#5546]) + - **uploads:** Delete the requested upload entry instead of a symlink's resolved target. `delete_file_safe` resolved the requested path first and unlinked the result, so a symlink planted at an upload name (the sandbox maps the uploads @@ -4738,6 +6810,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. unlinked: a symlink, like any non-regular entry, returns `404`; links pointing outside the uploads directory still return `400`; and the companion `.md` name comes from the requested file. Regular-file deletes are unchanged. ([#5547]) + - **extensions:** Drain extension-service shutdown across host cancellation. The Gateway registered extension-service cleanup with `AsyncExitStack` but awaited `stop_services()` directly, so a cancellation landing while an extension @@ -4747,6 +6820,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. cancellation-safe `await_drained()` helper, so later runtime teardown waits until extension services reach a terminal state within the per-service timeout budget. ([#5549]) + - **memory:** Report malformed `memory.backend_config` values by key name instead of unnamed tracebacks. The Honcho backend read `failure_policy`, `workspace_overrides`, and `user_peer_overrides` through a falsy-only @@ -4758,6 +6832,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and OpenViking backends get the same key-naming guard for numeric knobs (`top_k`, `timeout_seconds`, ...) instead of bare `int()`/`float()` casts that escaped unnamed `TypeError`s. ([#5555]) + - **mcp:** Scope MCP sessions and durable-task access by thread incarnation. Deleting and recreating the same thread ID reused the previous lifecycle's persistent MCP session scope, and the replacement thread could list, read, and @@ -4771,6 +6846,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. incarnations keep the pre-activation scope, worker polling and cancellation keep working through the creating session, and incarnation fields stay out of public task responses. ([#5556]) + - **subagents:** Recognize empty regular files in remote subagent acceptance probes. GNU `stat -c %F` reports a zero-byte file as `regular empty file`, but the remote size and readability probes accepted only `regular file`, so an @@ -4779,6 +6855,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. readable. Both labels are now accepted: empty readable files can satisfy `exists` and `file_written`, and `non-empty` yields a deterministic negative result. Symlink, FIFO, and containment checks are unchanged. ([#5559]) + - **frontend:** Derive the `web_fetch` tool-step title from the first non-blank line of the fetched markdown. `extractTitleFromMarkdown()` only accepted a document whose very first characters were `# `, so a blank line or up-to-three @@ -4787,6 +6864,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. its step title and silently fell back to the raw URL. An indented code block still does not count as a title, and an empty heading yields no title instead of a literal `#`. ([#5560]) + - **sandbox:** Stop E2B reconciliation from reviving warm-pool sandboxes. Every reconciliation pass issued `Sandbox.connect()` against sandboxes parked in the warm pool — which the SDK normalizes to a 300-second timeout update, pushing @@ -4799,6 +6877,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. releasing their ownership leases, mount results, and capacity tracking. Warm sandboxes now actually expire at the configured `sandbox.idle_timeout`. ([#5562]) + - **utils:** Return empty text for content-less messages instead of the literal string `"None"`. `message_content_to_text(None)` stringified its argument, so a content-less sub-agent final message bypassed the `No response generated` @@ -4806,6 +6885,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `error_detail`, and the task-continuity archive failed to skip the empty text. Only `None` content is treated as empty — the literal string `"None"`, `0`, `False`, and list-block conversions behave exactly as before. ([#5563]) + - **doctor:** Validate the contents of CLI-backed model credentials in `make doctor`. The check reported Codex and Claude auth as healthy whenever the credential path merely existed, so an empty, logged-out, malformed, or expired @@ -4817,6 +6897,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. provider-specific failures with the existing remediation hints. The check stays standard-library-only, offline, and redacted; environment and file-descriptor sources remain non-consuming presence checks. ([#5567]) + - **middleware:** End length-capped turns cleanly instead of letting `TodoMiddleware` re-engage the model. When a model hit its per-response output cap (`finish_reason=length`) while emitting a `write_file` call, the @@ -4830,6 +6911,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. description is annotated with the configured `max_tokens` output budget so the model does not assume the 80 KB streaming ceiling is the practical single-write limit. ([#5569]) + - **ragflow:** Validate RAGFlow document selections in batches of at most 100 IDs. Selecting more than 100 documents from one dataset is valid under the 1000-document scope limit, but validation sent every ID in a single request, @@ -4837,6 +6919,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. Selections now validate in provider-sized batches that preserve input order and the fail-closed behavior for missing or unsearchable documents, sharing one concurrency budget across datasets. ([#5572]) + - **persistence:** Drain the PostgreSQL bootstrap advisory unlock across cancellation. The unlock in `_postgres_lock()`'s `finally` was awaited directly, so startup/lifespan cancellation arriving while @@ -4844,6 +6927,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the connection to the pool while it still held the bootstrap mutex. The release now completes through the cancellation-safe `await_drained()` helper on the same connection that acquired the lock. ([#5573]) + - **persistence:** Drain engine disposal across cancellation during gateway teardown. `close_engine()` awaited `AsyncEngine.dispose()` directly, so cancellation while disposal was blocked let teardown return while the @@ -4851,6 +6935,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. disposed resource. Disposal now completes through the cancellation-safe `await_drained()` helper, the globals stay published until it finishes, and stale cleanup can no longer erase a replacement engine. ([#5576]) + - **channels:** Photos and files sent to the Telegram bot now download and reach the agent. Two bugs compounded: the download opened a fresh connection on the manager event loop and failed with a generic `NetworkError`, and the @@ -4858,12 +6943,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `Permission denied` reading it. Downloads now route through a loop-bound download bot, inbound files get sandbox-readable permissions, and failures log the underlying cause with the token-bearing Bot API URL masked. ([#5581]) + - **models:** Treat a Codex auth file whose top level is not a JSON object as "no credential from this source". An array, string, or number in `~/.codex/auth.json` (or `$CODEX_AUTH_PATH`) previously raised `AttributeError` out of `CodexChatModel` construction, preempting the documented "Codex CLI credential not found" error; the loader now logs and falls through. ([#5584]) + - **memory:** Keep an explicit `confidence: 0.0` from scoring as the 0.5 default. Two read sites rescued the field with `or 0.5`, which cannot tell "explicitly zero" from "unset", so a zero-confidence fact was inflated by @@ -4871,6 +6958,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. indexed it. Zero now ranks strictly below 0.5 through both the FTS5 index and the substring fallback; an absent or null confidence still defaults to 0.5. ([#5586]) + - **skills:** Load a `SKILL.md` saved as UTF-8 with a BOM. The front-matter anchor rejected the leading `U+FEFF` written by Notepad and PowerShell's `Set-Content -Encoding UTF8`, so Windows-authored skills silently vanished @@ -4878,30 +6966,35 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. compiled anchor and reaches neither skill metadata, rendered instructions, nor the skill-context description, and the loader, validator and middleware copies of the pattern can no longer drift. ([#5588]) + - **models:** Skip Claude Code credentials sources with a non-numeric `expiresAt`. A string, null, list, or object value raised `TypeError` out of the loader and stopped the lookup loop, so one malformed file at `$CLAUDE_CODE_CREDENTIALS_PATH` prevented every `ClaudeChatModel` from ever reaching `~/.claude/.credentials.json`; the source is now skipped with a debug log like its sibling guards. ([#5591]) + - **skills:** Render an explicit empty `allowed-tools` as no tools. The skill metadata renderer truthiness-tested the field, so a skill declaring `allowed-tools: []` advertised `Allowed tools: (all)` while the tool policy stripped every business tool — the model planned against tools that would be rejected. The empty declaration now renders `(none)`; an omitted or non-empty declaration renders as before. ([#5593]) + - **gateway:** Classify the Windows `image/svg` MIME alias as active content. On Windows the MIME database maps `.svg` to `image/svg` rather than the standard `image/svg+xml`, and the shared active-content classifier only recognized the standard type, so SVG artifacts, `.skill` archive members, and project documents could be served inline instead of forced to download. The alias is now treated as active content on every host. ([#5594]) + - **models:** Tolerate a null Codex `account_id` before it reaches the request header. `or`-chaining let a JSON `null` survive the loader as `None`, so `CodexChatModel` construction crashed on `self._account_id[:8]`; a numeric `account_id` died later with `httpx` rejecting a non-string `ChatGPT-Account-ID` header. Null now falls back to the unknown-account value `""` and any other non-string is stringified. ([#5601]) + - **llm:** Fence circuit-breaker probe settlement by generation and ownership. `LLMErrorHandlingMiddleware` settled successes and failures unconditionally, so a request admitted under an older closed generation could finish after a @@ -4910,6 +7003,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. probe. Every admission now captures the circuit generation, and success, failure, or release only mutates the breaker when both the generation and — in half-open state — the exact probe token match. ([#5602]) + - **memory:** Reject a Honcho `base_url` that can never resolve. The field was read with no validation, so the natural scheme-less spelling for an internal host (`base_url: honcho:8000`) was accepted at startup — httpx parses the host @@ -4919,6 +7013,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. matched only a literal `http://` prefix. Malformed values now fail fast at Gateway startup, matching the documented behavior, and every URL that resolves today keeps working. ([#5607]) + - **subagents:** Report an explicit zero batch limit by name instead of defaulting it. `SubagentBatchService.submit()` resolved `max_live_items` / `max_running_items` with `or`, so a caller-supplied `0` read as "not supplied" @@ -4928,6 +7023,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. resolutions now compare against `None`, so an explicit `0` reaches the range guard and is rejected by name; an absent key still means "use the configured default". ([#5609]) + - **sandbox:** Drop ignored directories from remote `list_dir` output. A local sandbox skips `node_modules`, `.git` and the other ignore patterns, but the same call in a remote sandbox listed every path under them up to the @@ -4936,6 +7032,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. ignored directory name, matched relative to the listing root like the local walk: explicitly listing an ignored directory still works, and a fully ignored directory returns empty instead of `FileNotFoundError`. ([#5612]) + - **middleware:** Discard stale todo reminders during context compaction. After a todo reminder had been injected, compaction could summarize its old task statuses or keep the stale reminder in the retained message tail, which also @@ -4944,12 +7041,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. before selecting the summary and retained partitions, and the current todo state survives: the middleware regenerates a reminder before the next model call when no `write_todos` call remains visible. ([#5614]) + - **sandbox:** Honor `start_line`/`end_line` in Tenki's `read_file`. The Tenki provider accepted only `path`, so a ranged read — the normal way an agent continues a truncated read — raised `TypeError: ... unexpected keyword argument 'start_line'`, rendered as a generic read error. It now slices like the sibling providers, with negative bounds clamping the way the local sandbox does. ([#5616]) + - **persistence:** Drain the PostgreSQL schema bootstrap connection close across cancellation. `ensure_postgres_schema_async()` closed its dedicated psycopg connection with a bare `await`, so cancellation arriving during @@ -4957,6 +7056,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. still live. The close now goes through the cancellation-safe `await_drained()` helper, which defers the caller's cancellation until the owned connection finishes closing. ([#5617]) + - **runtime:** Drain runtime provider teardown across cancellation. The checkpoint-cache and stream-bridge provider context managers own their backends and awaited `aclose()`/`close()` directly on context exit, so host @@ -4964,6 +7064,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. memory or Redis backend was still live. Both closes now go through the cancellation-safe `await_drained()` helper for the memory and Redis backends; cancellation propagates only after the owned close finishes. ([#5622]) + - **helm:** Align the provisioner's state root with the Gateway's when the shared home PVC is enabled. The chart set the Gateway's `DEER_FLOW_HOST_BASE_DIR` but left the provisioner on its runtime default @@ -4972,6 +7073,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `USERDATA_PVC_NAME` mode. The provisioner now receives the same logical root so projections map to `deer-flow/<suffix>` on the PVC; it stays unset when `persistence.home.enabled=false`. ([#5625]) + - **client:** Honor an agent's MCP plugin selections in the embedded client. An agent saved with `mcp_plugins: [installation-A]` still received tools from every enabled MCP server through `DeerFlowClient`, an empty selection was @@ -4980,6 +7082,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. to delegated tasks on every run, including cached-graph runs; the selection joins the graph cache key as an unordered set, and saved configuration changes still take effect after `reset_agent()`. ([#5630]) + - **persistence:** Drain Alembic migration workers across cancellation. `bootstrap_schema()` held the SQLite in-process mutex or the PostgreSQL advisory lock while `stamp`/`upgrade` ran in `asyncio.to_thread()`, but those @@ -4988,6 +7091,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. schema state, letting a second bootstrap overlap a live migration. Every Alembic worker await now goes through the cancellation-safe `await_drained()` helper; cancellation propagates only after the worker finishes. ([#5631]) + - **sandbox:** Enforce command timeouts in the AIO sandbox. `AioSandbox` ignored the caller-provided timeout, so `sandbox.bash_command_timeout` never applied — a long-running command ran to completion, and a stalled persistent-shell @@ -4996,6 +7100,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `hard_timeout` on supported AIO images (the frozen legacy `:latest` gets a bounded host wait only), ambiguous outcomes are never replayed, and `sandbox.bash_command_timeout` is now the provider default. ([#5634]) + - **channels:** Log Slack Socket Mode connect failures instead of dropping them. `SlackChannel.start()` ran `SocketModeClient.connect()` through `run_in_executor()` and discarded the returned future, so a rejected @@ -5004,6 +7109,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. exception was never retrieved" at garbage-collection time. Connect failures are now logged with their traceback, matching what the Telegram and Discord channels already did. ([#5640]) + - **mcp:** Honor a stdio MCP server's configured `cwd`. `McpServerConfig` accepted the setting as an extra field, but `build_server_params()` dropped it before either launch path saw it, so a server configured with a working @@ -5012,6 +7118,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. calls. The directory is now forwarded to stdio connections when non-empty (including environment-variable references) and omitted for HTTP/SSE; omitted, `null`, or empty values keep the default behavior. ([#5643]) + - **skills:** Read Python secret assignments from the AST in SkillScan. The `secret-env-assignment` rule swept every text file with a `name[:=]value` regex, which misreads Python: a typed-optional parameter default @@ -5020,6 +7127,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. failing the review gate on unchanged bundled skills. Python sources are now analyzed from their AST, reporting only real literal values; non-Python coverage is unchanged, and a test pins the bundled skills stay clean. ([#5648]) + - **persistence:** Drain the database auto-create maintenance dispose across cancellation. `_auto_create_postgres_db()` released its throwaway engine with a bare `await maint_engine.dispose()`, so cancellation during a slow first @@ -5028,6 +7136,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. since the next boot takes the non-missing-database path and never retries the disposal. The dispose now goes through the cancellation-safe `await_drained()` helper; a successful first boot behaves as before. ([#5649]) + - **skills:** Keep per-user skill-install directory setup off the event loop. `UserScopedSkillStorage.ainstall_skill_from_archive` created the per-user custom directory with a blocking `os.mkdir` on the loop before any real work @@ -5036,6 +7145,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. host-scoped base class. The `mkdir` now runs on the worker thread like the extraction and validation around it, and the anchor covers the user-scoped path. Install semantics and on-disk layout are unchanged. ([#5650]) + - **sandbox:** Clamp negative `read_file` range bounds in the e2b, OpenSandbox, and BoxLite providers the way `LocalSandbox` already does. Negative values previously fell through to Python negative-index slicing, so `start_line=-1` @@ -5043,6 +7153,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. range instead of reading from the first line and yielding an empty range. The tool layer already rejected negative bounds; direct `Sandbox` consumers hit the gap. ([#5655]) + - **skills:** Align the bundled Vercel deploy skill's package directory with its declared name and point its deployment commands at the real mounted path `/mnt/skills/public/vercel-deploy/scripts/deploy.sh`. The skill lived in @@ -5050,12 +7161,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. instructions referenced a script path DeerFlow never mounts, so a deployment request could select the correct skill and still receive a command pointing at a file that does not exist. ([#5656]) + - **frontend:** Show explicit empty skill export requirements instead of a blank value. The export dialog formatted `allowed-tools` and `required-secrets` with `join(", ")`, so an explicitly empty declaration rendered as an empty string and was indistinguishable from an omitted one, even though the two have different policy semantics; absent declarations now render as "Not declared" and explicitly empty ones as a localized "None". ([#5659]) + - **sandbox:** Bound AIO `list_dir` with a dedicated directory deadline. The listing ran under the sandbox-wide lock with only a 600-second no-change timeout and inherited the SDK transport budget, so a stalled relay or wedged @@ -5064,6 +7177,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `hard_timeout` inside a 65-second no-retry host envelope: `hard_timeout` raises `TimeoutError` and keeps the shell generation reusable, ambiguous outcomes raise `OSError` and fence it. No new configuration. ([#5662]) + - **runtime:** Share one change position across an atomic thread operation in the memory run store. `create_thread_operation_atomic` advanced the change clock per row, so an interrupt-and-replace landed its interrupted row and @@ -5071,6 +7185,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `(change_seq, run_id)` could observe half of the atomic set; every claimed row and the new row now carry one position, matching the SQL store. The default (no durable database) deployment selects this store. ([#5663]) + - **runtime:** Drain synchronous checkpoint mutations across cancellation. Goal writes and rollback offload sync checkpointers with `asyncio.to_thread()`, and cancelling the caller previously stopped waiting while the `put`, `put_writes`, @@ -5078,12 +7193,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and the mutation could still commit afterwards. These fallbacks now drain through the cancellation-safe `await_drained()` helper; the read fallback `get_tuple` stays directly cancellable. ([#5664]) + - **agent:** Distinguish undeclared and explicitly empty skill `allowed-tools` in the agent assembly fingerprint. Both declaration states hashed to an empty list, so switching a skill from the legacy allow-all to a no-business-tools policy changed runtime tool availability without changing the fingerprint; undeclared `allowed_tools` now hashes as `None` while explicit allowlists stay sorted lists. Frontmatter parsing and runtime tool policy are unchanged. ([#5669]) + - **uploads:** Stop deleting a converted Markdown companion when its document is deleted. `delete_file_safe` recomputed `stem.md`, so two documents sharing a stem (`a.docx`, `a.pdf`) made deleting one document remove the other's @@ -5091,18 +7208,21 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `report.pdf`. `DELETE /api/threads/{id}/uploads/{filename}` now removes only the requested file; orphaned companions stay in the listing and can be deleted individually. No API shape or config change. ([#5673]) + - **sandbox:** Prune ignored entries before applying the remote `ls` listing limit. The 500-entry cap was applied before ignored paths were dropped, so a dependency tree could fill the entire window and hide a normal file beside it, sometimes leaving only the root in the returned listing; ignored descendants are now pruned inside the shared `find` command before `head`, preserving depth and output limits. ([#5676]) + - **persistence:** Apply `database.postgres_schema` to the synchronous SQLAlchemy store used by DB-backed custom agents and managed subagents. The async ORM and LangGraph stores already honored the configured schema, but synchronous agent-store queries fell back to `public` and failed to find the application tables; the connection URL now preserves existing libpq options while adding `search_path`. ([#5678]) + - **logging:** Collapse every non-absolute Redirecting slot to `/<redacted>`. The #5225 URL-redaction pass kept a slot verbatim unless it started with `/`, so slash-less relative `Location` references (`download?sign=…`), query-only, @@ -5110,12 +7230,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. verbatim in DEBUG redirect logs; only a target with a scheme at position 0 is now handed to the generic absolute-URL pass, and non-URL `-> /path` sandbox mount arrows still pass through. ([#5680]) + - **channels:** Log DingTalk inbound-file skips instead of dropping the attachment silently. A download that returned no content vanished with zero log lines, indistinguishable from a message that never had one; the skip now emits a neutral guard line naming the file, matching the WeChat channel's shape, with the accurate reason already logged inside the download helper. No behavior change. ([#5683]) + - **gateway:** Drain readiness-probe connection close across cancellation. `/health/ready` closed its SQLite and PostgreSQL probe connections with a bare `await connection.close()`, so a second cancellation during teardown could @@ -5123,6 +7245,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. detaching database cleanup from the request lifecycle; close now drains through the cancellation-safe `await_drained()` helper. Response shape is unchanged. ([#5684]) + - **logging:** Collapse space-carrying Redirecting slots to `/<redacted>`. Round 15 kept scheme-bearing slots verbatim for the generic absolute-URL pass, but that pass's pattern stops at whitespace, so a slot like @@ -5130,6 +7253,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. space and leaked the rest into DEBUG logs; a slot is now kept only when a whitespace-free absolute URL fills it, and ordinary absolute redirect pairs still keep their host. ([#5687]) + - **skills:** SkillScan's `secret-env-assignment` rule again reports secrets bound outside plain statement assignments. The AST walk from #5648 only saw bare-constant assignments, so keyword arguments (`connect(api_key="...")`), @@ -5138,6 +7262,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. fold is iterative: a recursive one could raise `RecursionError`, which the scanner records as a per-file error that drops every finding for the file. The #5648 precision (annotations, `os.getenv` calls) is preserved. ([#5691]) + - **community:** Browserless `web_fetch` now reads its documented wait settings the way `web_capture` does. `wait_for_timeout_ms` goes through the tolerant `_as_int` helper, so a value like `2s` falls back to the default instead of @@ -5145,6 +7270,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `wait_for_selector_timeout_ms` is read from tool config instead of a hardcoded `5000` (default unchanged, so existing configs behave the same). Both keys are now documented on the `web_fetch` entry in `config.example.yaml`. ([#5702]) + - **community:** SearXNG `max_results` above one page is now honored. The SearXNG API has no `limit` parameter, so the client sent an ignored `limit`, requested only page 1, and sliced an already-truncated list — `max_results: @@ -5153,6 +7279,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. a page adds nothing new (deduped by URL), capped at 5 pages; the dead `limit` parameter is no longer sent. Default `max_results` still costs one request. ([#5705]) + - **sandbox:** Ambiguous AIO session creation can no longer be replayed or executed. Shell and bash creation planes track each attempted id through a pending/owned/absent/tombstoned state machine: timeouts, transport errors, 5xx @@ -5161,6 +7288,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. tombstone — and new creates on that plane are blocked. A dirty sandbox is released through the existing teardown path instead of returning to the warm pool; shell ambiguity does not affect bash creation. ([#5711]) + - **models:** Official DeepSeek models added through Settings now use the native `PatchedChatDeepSeek` adapter. Managed profiles and their connection probes always built `ChatOpenAI`, so adding `deepseek-flash` failed the connection @@ -5171,6 +7299,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. untouched — and matching is limited to `api.deepseek.com`, so third-party compatible endpoints keep their behavior. Existing saved profiles gain the adapter without a catalog migration. ([#5718]) + - **community:** Browserless `web_fetch` now reads `reject_resource_types` and `reject_request_pattern` from its tool config. Both parameters were declared and passed to the client but only ever assigned `None`, so a configured value @@ -5179,6 +7308,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. could not be turned on. Values accept a YAML list or a comma-separated string; unusable values omit the parameter instead of sending it. Defaults are unchanged, and both keys are documented in `config.example.yaml`. ([#5719]) + - **skills:** The custom-skill rollback route no longer reads the skill's edit history on the event loop. `POST /api/skills/custom/{skill_name}/rollback` constructed the storage, probed existence, and parsed @@ -5187,6 +7317,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and stream on the worker for the parse. The work now runs on a worker thread, matching the adjacent history handler; status codes and response shapes are unchanged, and blocking-IO anchors guard the route. ([#5729]) + - **mcp:** Durable MCP task calls now bound connection setup and MCP initialization under the same `session_init_timeout` deadline as session entry. The timeout previously started only after entering the session @@ -5194,6 +7325,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. event kept the call waiting indefinitely. After initialization succeeds the deadline is disabled and the existing independent tool-call timeout applies; `None` still disables the initialization bound. ([#5733]) + - **skills:** `required-secrets[].optional` must now be a YAML boolean. A quoted value like `optional: "false"` passed frontmatter validation and was later parsed with Python truthiness, so `bool("false")` recorded a required secret @@ -5202,6 +7334,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. parser fails closed to `optional=False` with a warning (value type and secret name only) for mounted or historical skills. The skill-review analyzer flags the shape as `structure.invalid-required-secrets-optional`. ([#5738]) + - **skills:** Skill review no longer keeps sentence punctuation on extracted resource references. A bare path at the end of a sentence ("See references/setup.md.") was extracted with its trailing period, producing a @@ -5209,6 +7342,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. Extracted references now strip trailing `.`, `?`, and `!`, while real dotted filenames such as `references/config.yaml` and link targets like `references/v1.0.md` stay intact. ([#5739]) + - **uploads:** Remove a staged upload's `.part` only after its document-conversion descriptor is released. With `uploads.auto_convert_documents` enabled, commit unlinked the staged file while conversion still held an open descriptor, so on @@ -5216,18 +7350,21 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. (500) where POSIX silently allowed it. The staged name is now removed in the same cleanup that releases the descriptor, and abort-path cleanup is best-effort so a secondary removal failure can no longer mask the original error. ([#5740]) + - **composer:** Keep IME-composition Enter out of the slash-skill catalog. The main prompt textarea's suggestion handler lacked the `isIMEComposing` guard its sibling call sites had, so with an IME active and the `/` catalog open, the Enter that committed a CJK composition instead applied the highlighted skill and discarded the in-progress text. IME-consumed keydowns now fall through to the normal submit path; plain Enter still selects the highlighted skill. ([#5741]) + - **sandbox:** Cancel BoxLite loop work whose synchronous bridge wait timed out. `run_coroutine_threadsafe(...).result(timeout)` raised to the caller but left the submitted coroutine running on the private loop, so the sandbox could keep mutating or retain acquire ownership after the operation was reported as timed out; the pending future is now cancelled before the timeout re-raises, while already-completed futures are left alone. ([#5748]) + - **mcp:** Invalidate the MCP tool cache only when the effective MCP configuration changes. Invalidation previously keyed on the whole `extensions_config.json` signature, so toggling an unrelated skill rebuilt cached tools and retired every @@ -5236,6 +7373,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. anything; when no servers are enabled, a non-initializing refresh retires stale state without adding discovery overhead, and malformed-config error logging no longer emits resolved credentials. ([#5750]) + - **gateway:** Drain artifact updates to completion across request cancellation. Cancelling `PUT /api/threads/{thread_id}/artifacts/{path}` unwound the thread-operation reservation and sandbox lease while the non-mounted remote sync @@ -5244,6 +7382,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. remote-sync/local-replace transaction is now drained before ownership is released, with rollback semantics preserved and the primary commit failure logged before rollback. ([#5755]) + - **uploads:** Preserve the original link error when staged-upload cleanup fails. On a failed link commit whose conversion descriptor was still open, Windows rejected the error handler's unlink with a sharing violation and that secondary @@ -5251,6 +7390,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. respects `unlink_staged=False` and all staged cleanup is best-effort. A cleanup failure after a successful link also no longer returns 500 for an already-published destination. ([#5756]) + - **runtime:** Drain database provider context exits across caller cancellation. The async SQLite/PostgreSQL checkpointer and Store providers used raw `async with`, so a cancellation delivered while the backend's `__aexit__()` was @@ -5258,17 +7398,20 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. database/pool cleanup — deterministically across all six provider paths. A `drained_async_context()` helper now preserves exception and suppression semantics while draining the exit to completion. ([#5757]) + - **channels:** Resolve the Buzz seen-event store path off the event loop. `ChannelService._start_channel` built the default `seen_event_store_path` inline, dereferencing `Paths.base_dir` (a `realpath`) on the loop in a path reachable from `POST /api/channels/{name}/restart`; the resolution now runs in a worker thread, with a blocking-IO anchor pinning it to the strict gate. ([#5759]) + - **sandbox:** Resolve the AIO sandbox acquire lock path off the event loop. `_discover_or_create_with_lock_async` offloaded every acquire step except the inline lock-file path construction, which dereferences `Paths.base_dir` (a `realpath`) on the loop on the hot path of every sandboxed tool call; the resolution now joins the surrounding steps in a worker thread, and the sibling ownership-publish anchor exercises the production path layer again. ([#5760]) + - **threads:** Do not leave orphan threads behind when a LangGraph run is rejected. Standalone run admission pre-creates a missing thread before LangGraph re-checks assistant ownership, so a run against another user's non-system assistant @@ -5277,10 +7420,12 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `Threads.put` (with the ambient auth context temporarily cleared), while ordinary users can still implicitly create threads for registered system assistants. ([#5762]) + - **setup:** Run the pre-commit hook through `uv` so `make install` no longer fails when uv's tool bin directory is not on PATH, and make the Node.js prerequisite check actionable: nvm users get an install command and everyone else a link to the official download page. ([#5767]) + - **scheduler:** Read the dispatch lease owner under SQLite's writer lock. `release_dispatch_lease` was the only mutating path that skipped `_lock_task`, and because `FOR UPDATE` is ignored on SQLite its guard evaluated a stale @@ -5288,11 +7433,13 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. leaving the task `enabled` and firing again even though the API had confirmed the pause. The read now takes the writer lock first; PostgreSQL behavior is unchanged. ([#5777]) + - **utils:** Initialize `Article.url` so `to_message()` no longer raises `AttributeError` for pages containing images. `ReadabilityExtractor` never passed the page URL into `Article`, so resolving a relative image URL via `urljoin(self.url, …)` crashed; the attribute now defaults to `""` and the extractor passes the real URL. ([#5785]) + - **tools:** Coerce DDG web-search result limits from environment variables. `max_results: $DDG_MAX_RESULTS` kept the substituted string, and DDGS 9.14.1 raised a `TypeError` sizing its workers that the search wrapper @@ -5300,12 +7447,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. accept numeric strings; invalid or non-positive values log a warning and fall back to the default of 5, while integer configs, the default, and config-over-call precedence are unchanged. ([#5792]) + - **gateway:** Treat a blank `GATEWAY_HOST` or `GATEWAY_PORT` as unset. A compose file with `GATEWAY_PORT=` (or `GATEWAY_PORT=${PORT}` with `PORT` unset) handed an empty string to `int()`, killing the Gateway at import time before uvicorn could bind. Blank values now fall back to the declared defaults (`0.0.0.0` / `8001`); `GATEWAY_PORT=0` still asks the OS for a free port, and every non-empty value parses exactly as before. ([#5801]) + - **channels:** Persist channel runtime configuration as UTF-8. The store read its JSON as UTF-8 but wrote its temporary file with the host's default encoding and `ensure_ascii=False`, so on non-UTF-8 hosts (CP1252, GBK) @@ -5313,6 +7462,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. or be written in bytes the store could not read back. The temporary write now specifies UTF-8; atomic replace, locking, and chmod behavior are unchanged, and legacy-encoded files are not migrated. ([#5803]) + - **setup:** Accept successful Apple Container image pulls in `make setup-sandbox`. On macOS with Apple Container installed and Docker absent, the script fell through to the Docker availability check and exited @@ -5320,6 +7470,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. had already been pulled. An Apple-only pull now counts as success and continues to the default-image note; Docker behavior, including fallback after an Apple Container failure, is unchanged. ([#5804]) + - **runtime:** Keep agent stream teardown alive across repeated cancellation. `close_agent_stream()` awaited `stream.aclose()` in the caller task, so a second `Task.cancel()` during teardown cancelled the @@ -5329,6 +7480,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. shielded task, the first caller cancellation is deferred until it completes, and stream-originated close cancellation is reported as a close failure so the run terminalizes as failed. ([#5806]) + - **tools:** Apply the #5792 numeric-string coercion to DDG image search. `image_search_tool` passed the environment-substituted `max_results` string straight to DDGS, which raised a `TypeError` sizing its workers, so @@ -5336,6 +7488,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. found`; `0` removed the limit and negative values truncated results. Numeric strings now work, and invalid, zero, or negative values warn and fall back to 5, with integer configs and the default unchanged. ([#5807]) + - **client:** Stop resumed embedded-client streams from replaying prior turns. On a resumed thread, `DeerFlowClient.stream()` re-emitted earlier turns' message deltas — including tool results — in `messages-tuple` @@ -5344,6 +7497,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. suppressed from the current turn's deltas and usage, with the per-run `run_id` on the current user message as the boundary; Gateway SSE is unchanged. ([#5811]) + - **persistence:** Drain the postgres schema-setup connection exit across cancellation. `_ensure_postgres_schema_with_pool` still acquired its connection with a raw `async with`, so caller cancellation landing while @@ -5352,6 +7506,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. closed with a checked-out connection. The connection now goes through `drained_async_context()` like the rest of the checkpointer provider; no happy-path behavior change. ([#5812]) + - **mcp:** Stop the bare-filename correlation pass from mangling backslash paths. After a stdio MCP call created `page.yml`, a result mentioning `C:\outside\page.yml` had its basename rewritten to point at the new @@ -5359,6 +7514,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. boundary. Backslashes now act as path separators on both sides of a correlated name, so genuine bare filenames still resolve while names inside unresolved Windows, UNC, or relative paths stay unchanged. ([#5815]) + - **mcp:** Resolve generated file paths containing spaces in MCP text results. A file named `artifact with space.txt` kept its absolute host path in the free-text tool result, and a follow-up `read_file` failed with @@ -5367,17 +7523,20 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. now correlate with files the call created, validated inside the current thread's user-data tree; outside paths and ambiguous suffixes are untouched. ([#5819]) + - **middleware:** Preserve human message metadata in `ThreadDataMiddleware`. Adding run metadata reconstructed the last `HumanMessage` from parts, silently dropping fields it did not copy such as `response_metadata`; the middleware now copies the existing message and updates only its default name and `additional_kwargs`. ([#5823]) + - **uploads:** Keep document-conversion `stat()` and markdown write-back off the event loop. `convert_file_to_markdown` offloaded the conversion itself but still ran the size-check `stat()` and the multi-megabyte `write_text` of the converted markdown on the loop, blocking upload ingestion for large documents. Both now go through the shared `run_file_io` pool; the small-file inline conversion threshold is unchanged. ([#5830]) + - **mcp:** Run lazy MCP tool discovery once when initialization itself raises. The no-event-loop `except RuntimeError` fallback in `get_cached_mcp_tools()` also caught discovery's own errors: with no running loop a failing call @@ -5387,6 +7546,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. cannot be called from a running event loop". Initialization is now driven exactly once with the real failure logged; errors still return `[]` and leave the cache uninitialized for the next retry. ([#5836]) + - **config:** Honor `$VAR` substitution for `request_admission` integer fields. `requests_per_minute` and `max_queue_size` are declared `strict=True` so `true` and `60.0` stay rejected, but an environment substitution like @@ -5395,6 +7555,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. pace a model at all. A `BeforeValidator` now converts a plain decimal string to `int` before the strict check; bools, floats, `"60.0"`, empty and non-numeric values are still rejected and `gt=0` is unchanged. ([#5838]) + - **tools:** Coerce Exa's configured result limits before calling the SDK. `$VAR` substitution resolves to strings, so `max_results: $EXA_MAX_RESULTS` made every `web_search` call fail — exa-py rejects a string `num_results` — @@ -5403,6 +7564,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. blank, non-numeric, fractional, boolean, zero or negative value logs a warning and falls back to the defaults (5 and 1000) instead of failing the search, matching the DDG and SearXNG providers. ([#5839]) + - **gateway:** Treat a blank `GATEWAY_WORKERS` and blank Lark broker port or timeout as unset, extending #5801's blank-env rule. `int("")` in the WeChat QR-login guard turned a blank `GATEWAY_WORKERS` (e.g. `-e GATEWAY_WORKERS=`) @@ -5411,6 +7573,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. aborted the Lark broker at startup. The guard now reads `GATEWAY_WORKERS or WEB_CONCURRENCY or "1"` and still fails closed on multi-worker or non-numeric values; the broker falls back to its defaults. ([#5840]) + - **skills:** Strip section anchors from code-span resource references in the skill review resource graph. Markdown-link targets already dropped `#fragment` before resolving, but a code-span reference like @@ -5419,6 +7582,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. referenced. Code-span tokens now split the fragment the same way and strip trailing sentence punctuation; dotted filenames such as `references/v1.0.md#notes` keep their extension dots. ([#5841]) + - **skills:** Keep the skill review markdown-link scan linear on adversarial input. The extractor re-scanned the same target run from every candidate opener, so a long run of unmatched `[` or a `](`-dense invalid target drove @@ -5429,6 +7593,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. differential fuzzing against the base regex), and blanks matched spans by position instead of `str.replace`, which could blank an unrelated later copy of the same text. ([#5884]) + - **config:** Sign the bytes the config cache parsed, not a second read. The app-config loader opened `config.yaml` twice — parse once, then hash a fresh read to record the `(mtime, size, sha256)` signature — so a write landing @@ -5437,12 +7602,14 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. some later edit. The loader now reads once through a shared `file_signature.read_config_with_signature()` helper that signs exactly the returned bytes; a racing edit can only cost one extra reload. ([#5848]) + - **dev:** `make stop` / `make dev` can now reclaim dev ports held by a sibling worktree whose path contains spaces. `serve.sh` built its worktree-root list with `awk '{print $2}'` over `git worktree list --porcelain`, whose paths are unquoted, so `.../deer flow two` was recorded as `.../deer`; a Gateway or frontend started from that worktree was never recognised as deer-flow's and the start aborted with "port already in use". The whole path is kept now. ([#5856]) + - **uploads:** A malformed `files[*].size` in a run's message metadata no longer fails the whole run. `UploadsMiddleware` validated every other field of a client-supplied file entry fail-soft but passed `size` straight to @@ -5451,6 +7618,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. message, since the entry is carried over verbatim. The size only feeds the human-readable line in `<current_uploads>`; unusable values now fall back to `0`, the same as a missing size, while numeric strings keep working. ([#5855]) + - **release:** Bumping the version no longer leaves `backend/uv.lock` behind. `scripts/bump_version.sh` rewrote `backend/pyproject.toml`, `frontend/package.json` and the Helm chart, but the lockfile records the root package's own version too @@ -5461,6 +7629,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `uv-lock-check` hook. The script now refreshes the lock with `uv lock` and exits 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:** Stop `glob` and `grep` from returning nothing when the search root — or one of its ancestors — matches an ignore pattern such as `build`, `dist`, `logs`, `node_modules`, `coverage` or `target`. The remote sandboxes @@ -6964,6 +9133,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. value that yields no valid ID denies every guild and logs an error. Unset, `null`, `[]`, or a blank string still allows all guilds; `allowed_channels` gains the same scalar handling. ([#6338]) + - **release:** The `v*` release gate now rejects a stale `backend/uv.lock`. `scripts/verify_versions.sh` compared only `Chart.yaml`, `pyproject.toml` and `package.json`, so bumping those three by hand passed the gate even though the @@ -7007,6 +9177,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. checks (`AuthProvider.refreshUser`, `useRequireAuth`) kept the path but dropped the query string, and the shared fetcher dropped the fragment; all of them now keep the full address. ([#6681]) + - **sandbox:** The AIO, E2B and OpenSandbox providers and ACP agents no longer resolve `sandbox.environment` / `acp_agents.<name>.env` values a second time. `config.yaml` loading already replaces each `$VAR`, so a resolved secret that @@ -7015,6 +9186,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. agent as an empty string, or as an unrelated host variable's value when that name existed. Values are now passed through as loaded, as BoxLite and the local provider already did. ([#6673]) + - **frontend:** Loading more chats in the sidebar and on the Chats page no longer re-requests chats it already has. Side chats are hidden from these lists, so a page of 50 visible chats can span more server rows; the page kept that offset @@ -7031,6 +9203,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the trailing `-07` as a UTC offset and displayed UTC midnight instead: 08:00 in `Asia/Shanghai`, and the previous evening west of UTC, in the task list, detail, chat card and the edit form's prefilled time. ([#6692]) + - **subagents:** A durable batch worker that stops supervising a subagent still in flight now cancels it and removes its registry entry once it stops. Before, stopping the batch service in a long-lived process (an SDK @@ -7039,6 +9212,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. polling failed or requeued the item but left its subagent running unsupervised, so a retry could run the same item twice. An entry that never stops is removed once the item's lease has passed. ([#6785]) + - **gateway:** The subagent batch JSONL export (`GET /api/threads/{thread_id}/subagent-batches/{batch_id}/results.jsonl`) now reads stored results 10 rows at a time instead of 500. Each row can hold @@ -7047,6 +9221,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. default limit (roughly 1.7 GB at the 1,000,000 ceiling) and stalled the event loop for over half a second per page. The exported lines and their order are unchanged. ([#6706]) + - **gateway:** The subagent batch item list (`GET /api/threads/{thread_id}/subagent-batches/{batch_id}/items`), which the workspace polls every 2 seconds while a batch runs, no longer loads each @@ -7054,6 +9229,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. With the default `max_result_chars`, one 100-item page held about 34 MB and blocked the event loop for about 70 ms (170 MB and 300 ms at `limit=500`); it now holds under 1 MB. The response is unchanged. ([#6781]) + - **channels:** Saving or disconnecting a bot from the channel setup page no longer overwrites bots saved through another Gateway worker. Each worker kept the `runtime-config.json` contents it loaded at startup and rewrote the whole @@ -7061,6 +9237,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. was dropped when a Slack bot was saved on the other, and a disconnected bot came back. Every save now rereads the file under a lock shared across processes and changes only its own provider. ([#6844]) + - **persistence:** Two first ratings of one run by the same user at the same moment (two tabs, two devices, a client retry) no longer fail one of them with a 500. Both used to miss the existing-row lookup and insert, and the @@ -7068,6 +9245,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. 80 such requests failed. The same race in IM conversation binding, such as a first message arriving together with `/new`, no longer fails the message. The losing write now updates the row the other one created. ([#6852]) + - **mcp:** Cancelling a background MCP task on SQLite now reaches the remote server on the next poll cycle instead of up to `mcp_tasks.lease_seconds` (120 s by default) later. A cancel that committed while the poller was @@ -7076,6 +9254,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. that lease to expire, and the task was polled once more for a result that was thrown away. Each claim now re-checks that the task is still claimable when it writes the lease. Postgres was not affected. ([#6859]) + - **persistence:** Startup schema upgrades no longer log a false `safe_add_column ... drifts from the model definition` warning for columns that already match the model. The check compared the model's generic type @@ -7085,6 +9264,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. whose head stamp was lost logged 29 (SQLite) or 36 (Postgres), burying the real type mismatch the warning exists to catch. Both sides are now compared as the connected database spells them. ([#6865]) + - **sandbox:** With host bash enabled, the local path guard now rejects a shell variable used as a path prefix, such as `cat $HOME/.ssh/id_rsa`. `~/`, `${HOME}/` and `"$HOME"/` were already rejected, but the @@ -7099,6 +9279,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. names the same file as the equivalent relative path, unless the command rebinds `PWD` itself. Variables inside URLs are unaffected. The guard remains best-effort and is not an isolation boundary. ([#6930]) + - **sandbox:** With host bash enabled, the local path guard accepts awk field arithmetic again, such as `awk '{print $NF/2}' data.txt`, `awk '{print $sum/$n}' data.txt` and `($2+$3)/2`. Since [#6930] every @@ -7119,6 +9300,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `$'...'`, an unmatched `)`), nor to a program whose `"` may sit in an awk regex literal; `/` and `#` inside an awk string literal are plain text. (issue #7021, [#7023]) + - **sandbox:** On the local sandbox, `ls` now accepts the `/mnt/user-data` root itself and lists `workspace`, `uploads` and `outputs` as it does on AIO. The provider maps that root on purpose, but the tool layer allowed @@ -7127,9 +9309,53 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. covers only those three directories, so anything else stored directly under the root stays hidden. `glob`, `grep`, `read_file` and writes still reject the root, because a search would read every file below it. ([#6991]) - ### Security +- **artifacts:** Gate `_get_viewed_image_blob_response` with + `@require_permission("threads", "read", owner_check=True, + require_existing=True)` so shared viewed-image checkpoint blobs are served + only when the caller owns a surviving `threads_meta` row; previously the + route-level check alone treated a missing row as an allowed untracked legacy + thread, which was enough to reach the blob path. The helper also returns + early when blob storage is disabled, offloads `mimetypes.guess_type` to a + worker thread because a cold mimetypes database stats system files on its + first lookup, and logs resolution failures as warnings carrying the + exception type. ([#7604]) + +- **browser:** Bound and redact the final URL reported in unbrowse `web_fetch` + target errors. When a target page answered HTTP 400–599, the failure + message echoed the scraping service's `finalUrl` verbatim — unbounded and + with any embedded username and password intact. The message now runs the + URL through `_strip_url_userinfo`, which keeps only http(s) URLs with a + hostname (a URL whose authority fails to parse yields no final URL at all, + so raw credentials are never echoed), drops everything before the last `@` + in the netloc, and clips the result to 500 characters. ([#7399]) + +- **skillscan:** Walk compound-statement parts that cannot follow a sibling + (`if`/`elif`/`else` branches, the one `match` case that runs, `try` and loop + bodies) with the enclosing client handle live and let each part apply its + own rebinding in source order; parts that can follow a sibling (`else` + after a loop or `try`, handlers, `finally`, later `except*`) still start + with those names dropped. Previously every name a compound statement could + bind was dropped before any part, so an outbound network call that really + happened first disappeared when a later statement in the same body rebound + the variable. Dispatch-bound names (a loop target, an `except ... as` name, + a `match` capture) are dropped only after the header is walked, so a sink in + the header itself (`for s in s.post(...)`) is reported too. ([#7128]) + +- **skills:** Classify `skill_manage` `write_file` content with the shared + package-file rule `is_code_file` instead of a `"scripts/" in path` + substring test. Support files such as `references/scripts/overview.md` were + previously sent to executable-content moderation — an exact-"allow" scan + that hard-blocks when the moderation model is unavailable — while runnable + files outside the top-level `scripts/` directory (e.g. + `references/scripts/helper.py`) and extensionless shebang files escaped to + static moderation and could be written to disk on a warn verdict or with + `security_fail_closed=false`, with no later scan restoring the gate on this + write path. Executable content is now a top-level `scripts/` member, a + code-suffix file, or a shebang file, matching the installer and SkillScan. + ([#7088]) + - **uploads:** Thread uploads (`POST /api/threads/{thread_id}/uploads`), upload deletes (`DELETE /api/threads/{thread_id}/uploads/{filename}`) and shelf attach-to-thread no longer put the raw `OSError` text in their `500` detail. A @@ -7137,7 +9363,41 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the absolute host path of the thread's uploads directory, including the user and thread IDs. They now return `Failed to upload <name>.` / `Failed to delete <name>.`, and the full error stays in the Gateway log. ([#7217]) - + +- **sandbox:** Treat the `.` builtin as a `source` execution context in the + sandbox audit guard: only the word `source` matched, so `. $(curl ...)`, + `. <(curl ...)` and the backtick form passed as value position while the + `source` spelling of the same command blocked. The dot rule is anchored in + command position behind the existing assignment and wrapper prefixes, so a + standalone dot path argument (`du -sh . $(curl u)`) still passes, and a + position-blind sibling restores blocking for `eval . $(curl u)` including + wrapped spellings. A token-level check also blocks feeding a process + substitution to a command-position interpreter on stdin, but only when + nothing else supplies the program: a script operand, shell noexec flags, + or the dot builtin's filename requirement keep the fetched bytes as data. + ([#7247]) + +- **community:** Screen `web_search` result URLs through + `safe_public_result_url` in every provider — tavily, exa, youcom, + tencent_wsa, serply (including scholar `pdf_url`), groundroute, sofya, + ddg_search, fastcrw, firecrawl, searxng, webz, and brave and serper's + organic links — so a loopback, link-local, or metadata-service address in + a provider payload never reaches the model as a citation. A non-public + URL is blanked while the row's title and snippet are kept; previously only + image-search links were screened and serper's comment explicitly returned + organic links verbatim. ([#7037]) + +- **skills:** Sniff ELF/PE/Mach-O magics past leading filler bytes in + `executable_binary_magic`, which `is_executable_binary_prefix` now backs: + up to 63 bytes of ASCII whitespace/NUL padding plus a UTF-8 BOM are + skipped before matching, and scan windows widen from 8 to + `EXEC_SNIFF_BYTES` in archive preflight, the extraction guard, the + directory static scan, and nested-zip checks. Previously a single filler + byte before the magic (a `b'\n'`-prefixed ELF still loads through + `ld.so`) evaded every layer at once, passing preflight and extraction and + reporting nothing; a 64-plus-byte filler run stays outside the sniff so + indented prose does not trip the guard. ([#7081]) + - **agents:** The custom-agent and user-profile routes no longer put the raw exception text in their `500` detail. An unparsable agent `config.yaml` or a filesystem error (`[Errno 13]` and the like) made them answer with the @@ -7313,6 +9573,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `GET/PUT /api/user-profile` also read and wrote one global `USER.md`, letting any authenticated user overwrite or read everyone's prompt context — the file now lives at `{base_dir}/users/{user_id}/USER.md`. ([#4989]) + - **gateway:** Preserve owner isolation on threads whose metadata row is missing or NULL-owner. #5448's internal-caller run scoping returned an empty filter for every trusted internal caller, and `ThreadMetaStore.check_access(..., require_existing=False)` @@ -7323,6 +9584,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. established-owner threads keep the unfiltered read, while missing/NULL-owner threads fall back to the acting owner's raw stamp, keeping cross-user runs hidden. Browser and API sessions are untouched. ([#5484]) + - **frontend:** Gate tool-step links through the shared `isSafeHref` scheme allowlist. The `web_fetch`, `web_search`, and `image_search` tool steps put URLs straight into `<a href target="_blank">`, so a `ms-msdt:`, `search-ms:`, or `vscode:` link drawn @@ -7332,6 +9594,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. application. Unsafe URLs now render as the same inert "Unsafe link omitted" span markdown links use; `web_fetch` also stops throwing on a non-string `url` arg, and missing tool-call `args` normalize to `{}` across every tool branch. ([#5526]) + - **uploads:** Stop the embedded client's uploads from writing through symlinks. The uploads directory is writable from inside the sandbox, so a process there (for example one steered by prompt injection) could plant @@ -7340,6 +9603,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the client's privileges. Uploads now publish through a no-symlink copy helper; unsafe destinations are skipped and reported via `skipped_files` with `success: false`, matching the Gateway's contract. ([#5578]) + - **uploads:** Convert a private copy of the staged bytes instead of the name they landed under. The uploads directory is writable from inside the sandbox, so a process there could swap an upload's committed name for a symlink to any @@ -7348,6 +9612,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the converted `.md` companion. The Gateway now converts a descriptor-backed copy in a private temporary directory, and the client converts the caller's own source path — a swapped name no longer redirects the read. ([#5611]) + - **authz:** Enforce `threads:write` for Live Browser WebSocket connections. The WebSocket bypassed the HTTP authentication middleware, and `browser_stream()` checked authentication, Origin, thread ownership and @@ -7356,6 +9621,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. Browser, receive frames, and dispatch input events. Denied connections are now rejected with close code `4403` before session acquisition; behavior is unchanged when fine-grained authorization is disabled. ([#5621]) + - **gateway:** Reject external `system` and `developer` messages with HTTP 400 before checkpoint writes, run admission, or agent execution. Such messages were previously accepted and could be persisted, merging into the agent's @@ -7363,6 +9629,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. and validated; ordinary user, assistant, tool, and attachment messages and trusted internal system messages are unaffected, and existing checkpoints are not rewritten. ([#5651]) + - **skills:** A user-scoped `.skill` install now resolves the static content scan from the same app config as the inherited archive preflight. The user-scoped storage override passed no `app_config`, so the content scan fell @@ -7372,6 +9639,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. (such as embedded private-key material) installed anyway. The per-file LLM scan now forwards the same config instead of reading the global for model selection. ([#5703]) + - **sandbox:** Restored sandbox references are now scoped to the authenticated user and thread. An authenticated caller could submit `input.sandbox.sandbox_id` on a thread they owned, and the AIO restore path @@ -7383,6 +9651,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. scoped match and otherwise falls back to a canonical `acquire(user_id, thread_id)` persisted with `Overwrite`, and only the server-created fork path may borrow a parent's sandbox. ([#5736]) + - **logging:** Redact space-carrying request targets in urllib3 retry log lines. Three redaction patterns (`Retry: …`, `Retrying (…) …`, and the quoted request line) stopped matching at the first space, so an origin-form path containing an @@ -7391,6 +9660,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. production logs without DEBUG. The patterns now match the target up to its terminating quote or end of line, consistent with the already-hardened `Redirecting` and increment-retry slots. ([#5745]) + - **auth:** Claim the first admin atomically in `POST /api/v1/auth/initialize`. The handler counted admins in one session and created the user in another with no lock or constraint between them, so two concurrent first-boot requests with @@ -7400,6 +9670,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. advisory lock on PostgreSQL, unknown dialects fail at boot), and a losing request gets the documented `409 system_already_initialized`; sequential behavior and response shapes are unchanged. ([#5776]) + - **sandbox:** Require `sandbox:execute` before `view_image` reads. A role allowed to call `view_image` while denied `sandbox:execute` could still read an image from the current thread's allowed directories, and the image @@ -7407,6 +9678,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. gap, not a cross-user read. Synchronous and asynchronous reads now check execution authorization, and the middleware rechecks before reading an image recorded in thread state, including after a role change. ([#5799]) + - **authz:** Enforce `model:use` on the suggestions endpoint. With model authorization enabled, a user denied a model could still invoke it through `POST /api/threads/{thread_id}/suggestions` — by naming it, or by omitting @@ -7415,6 +9687,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. model-details check now runs before model creation, returns 403 on denial, and sits outside the best-effort handler so a permission failure cannot become `200` with empty results. ([#5816]) + - **sandbox:** Scope viewed-image host reads to the current thread. External run input or thread-state updates could supply `viewed_images` metadata with a chosen `actual_path`, and after matching `view_image` history the @@ -7555,6 +9828,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. always available, so skill tool policy could not remove it. Local targets are now confined to the configured skills root and the caller's own user directory; `skill://` and `inline://` targets are unchanged. ([#6580]) + - **sandbox:** `glob` and `grep` on the BoxLite, OpenSandbox and Tenki providers frame their records with `str.splitlines()`, which also ends a line at a bare carriage return, form feed, vertical tab, file/group/record separator, next-line, and @@ -7564,6 +9838,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. truncated at that character. These providers now split on `" "` only, as the shared parser already documents and as LocalSandbox, the AIO backend and E2B already do. ([#6595]) + - **security:** Add `DEER_FLOW_CREDENTIALS_KEY`, an env-only at-rest encryption key for stored credentials. Per-connection IM channel credentials had an encryption path that no production code wired up, so they could be @@ -7580,9 +9855,23 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. files pass it to the Gateway. `.jwt_secret` (and the managed-model key) are now created exclusively and read back, so replicas cold-starting on a shared volume no longer keep different session-signing secrets. ([#6611]) - ### Documentation +- **config:** Document skills and sandbox paths across platforms in a new + "Skills and sandbox paths" section of `backend/docs/CONFIGURATION.md`. It + separates `skills.path` (the Gateway-read source root) from + `skills.container_path` (the agent-facing `/mnt/skills` root, kept + POSIX-style even on Windows), pins the resolution order (`skills.path`, + then `DEER_FLOW_SKILLS_PATH`, then the project-root `skills/` directory, + with relative paths resolved against `DEER_FLOW_PROJECT_ROOT` or the + process cwd rather than the directory holding `config.yaml`), and gives + source paths for Linux, macOS, native Windows, WSL, and a Docker Gateway. + YAML backslash quoting, the per-user custom-skill directory under + `DEER_FLOW_HOME`, and the no-second-skills-mount rule are covered; + `config.example.yaml` comments and the `README.md` sandbox pointer are + updated to match. + ([#7304]) + - **extensions:** Add an example-authoring and packaging guide plus `scripts/check_extension.py PACKAGE`, which builds a temporary wheel and checks entry points, Python syntax, API dependency compatibility and browser @@ -7595,6 +9884,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. only the prose is translated. The language switcher in `README.md` and the other translations, and the translations list in `AGENTS.md`, now link to it. ([#6909]) + - **docs:** Configuration checks and migration commands now run in the backend environment. The documented examples ran with system Python, which cannot import the installed backend workspace packages; selecting @@ -7616,16 +9906,19 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `local_backend.py`, documents the existing Docker selection for restricted networking, and uses the two existing runtime-selection pytest cases (which mock runtime commands) as verification. ([#5605]) + - **config:** Move the `# Sandbox Configuration` banner in `config.example.yaml` to sit directly above the `sandbox:` key it introduces instead of the `uploads:` block it had drifted twenty lines away from, and give `uploads:` a banner of its own. Comment-only change; `config.yaml` files generated from the example are unaffected. ([#5652]) + - **docs:** Clarify the upgrade workflow for existing local and Docker deployments in the README and the operations-and-troubleshooting guide: `make config` and `make docker-init` are first-time setup steps, not routine source-upgrade steps, and `make config-upgrade` is only needed when a release adds configuration fields. ([#5654]) + - **docs:** Document how to connect DeerFlow to a local llama.cpp `llama-server` (closes #2931). `config.example.yaml` and the English/Chinese configuration pages gain an OpenAI-compatible Qwen example (`use: @@ -7633,6 +9926,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. the `--alias` model name, a Docker-reachable `base_url`, the API key when `llama-server --api-key` is enabled, and the chat-template/tool-calling requirement. ([#5688]) + - **extensions:** Cover full-stack plugins and request-scoped run evidence in the extensions manual (deerflow-extension-api 0.2.3, en and zh). A new Full-Stack Plugins chapter documents `registry.plugin()`, `BrowserModule`/`BrowserAssets`, @@ -7640,6 +9934,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `resolve_run_evidence_reader`/`require_run_evidence_reader` with a per-user route example; and plugin sections are added to Reference, Troubleshooting, and Operating Extensions. ([#5775]) + - **docs:** Correct the documented skill and MCP enable/disable endpoints. The docs site and `backend/docs/API.md` pointed at `POST /api/extensions/skills/{name}/enable` and similar routes that do not exist in the gateway (404/405); the skill toggle @@ -7647,6 +9942,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `PATCH /api/mcp/config` with `{"server_name", "enabled"}` (both admin only), now documented in the en/zh skills and MCP pages and in API.md with working cURL examples. ([#5778]) + - **docs:** Fix backend doc examples that referenced APIs that do not exist. The `PATH_EXAMPLES.md` upload example imported a `THREAD_DATA_BASE_DIR` symbol no module exports, and pointed at the legacy upload bucket; it now @@ -7654,6 +9950,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `deerflow.uploads.manager`, which reads the bucket the Gateway actually writes to. `CONFIGURATION.md` also drops a `sandbox.auto_start` key that `SandboxConfig` never declared and no code reads. ([#5798]) + - **docs:** Make the custom-memory-storage guide followable. The harness `memory.mdx` / `customization.mdx` snippets imported a module that does not exist, showed a storage class the factory cannot construct and the memory @@ -7661,6 +9958,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. `storage_class` never resolves, and promised a graceful fallback where the code deliberately raises. The guide (English and Chinese) now names the real module, matches the real call shapes, and states the actual error behavior. ([#5844]) + - **docs:** Add a ten-chapter checkpoint storage manual (`harness/checkpoints`) in English and Chinese — quick start, channel modes, snapshot cadence, history cache, resume and rollback, operations, observability, troubleshooting and @@ -7736,9 +10034,158 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. three missing `changes` fields. The extension example no longer claims to cover every contribution kind: it shows five of the eight, and its README points to the observers guide and the bookmarks plugin for the rest. ([#6582]) - ### Internal +- **gateway:** Delete unused code with no behavior change: the frontend + `ProxyPolicy` interface and `LANGGRAPH_COMPAT_POLICY` constant + (`frontend/src/core/auth/proxy-policy.ts`), the unused `_CURRENT_VERSION` + password-hash constant in `app/gateway/auth/password.py`, the unused + `_PYTHON_CLIENT_SINK_METHODS` set in the skillscan orchestrator (its test now + derives sink methods from `_PYTHON_CLIENT_SPECS`), and the unused Lark CLI + config/data path constants in the AIO sandbox remote backend, whose + `_PROVISIONER_EXTRA_MOUNT_PATHS` now reuses the shared + `_LARK_CLI_RUNTIME_CONTAINER_PATH` literal. ([#7554]) + +- **provisioner:** Remove the ignored `include_legacy_skills` parameter from + `_build_volumes`, `_build_volume_mounts`, and `_build_pod` in + `docker/provisioner/app.py`. The request field stays on + `CreateSandboxRequest`, accepted and logged so older Gateways keep working, + but it no longer gates any mount — the public/custom/legacy skills + projections are built unconditionally, which the slimmed PVC tests now assert + without passing the flag. ([#7555]) + +- **gateway:** Remove unused frontend components and an unused helper: the + `components/ai-elements/` canvas-flow set (`canvas`, `checkpoint`, + `connection`, `context`, `controls`, `edge`, `image`, `node`, + `open-in-chat`, `panel`, `plan`, `sources`, `toolbar`, `web-preview`) that + nothing imports, which also drops the `@xyflow/react` and `tokenlens` + dependencies, and the unused `_group_searchable_datasets` function in the + RAGFlow community tools. No user-facing behavior changes. ([#7543]) + +- **deps:** Bump the transitive `opentelemetry-api` lock entry from 1.41.1 to + 1.45.1 in `backend/uv.lock`. The new version no longer requires + `importlib-metadata` (dropped from the lock along with `zipp`), and the + refreshed resolution adds `opentelemetry-exporter-http-transport` and + `opentelemetry-exporter-otlp-common` lock entries. ([#7510]) + +- **sandbox:** Make the `test_bench_sandbox_prewarm` coarse-clock regression + deterministic by replacing the benchmark module's clock binding + (`bench.time`) with a fixed-tick fake instead of quantizing the real + `time.monotonic` and spinning until a fresh bucket starts. `Trace` is now + initialized from the controlled clock, asyncio keeps its real clock so + timeouts still fire, and the tick advances exactly at the acquire/tool event + gate; the old same-tick test becomes a parametrized case over a 0.0 and + 0.015 tick offset, and `backend/tests/AGENTS.md` records the + pattern. ([#7513]) + +- **deps:** Bump `zod` from 4.3.6 to 4.6.7 in the frontend lockfile. The + dependency is indirect, so only `frontend/pnpm-lock.yaml` changes. + ([#7512]) + +- **deps:** Bump `langgraph-checkpoint-postgres` from 3.1.2 to 3.1.3 in the + backend lockfile. + ([#7511]) + +- **gateway:** Keep strong references to DingTalk stream message handler + tasks and log their failures. The stream loop previously passed each frame + to `client.background_task(...)` via a bare `asyncio.create_task(...)` + whose result was dropped; asyncio only holds a weak reference until the + task is awaited, so a handler could be garbage-collected mid-execution and + silently drop a message, and any exception it raised was never observed. + Tasks now land in `DingTalkChannel._background_tasks` with a done callback + discarding them (the set stays bounded) and `_log_task_error` retrieving + and logging exceptions, matching the tracking used by the subagent + executor and MCP session pool. + ([#7429]) + +- **scripts:** Pin the `scripts/test-config-version.sh` wrapper's uv index + isolation with a new test suite. The wrapper exists so a host whose `uv` + points at a mirror that blocks dependencies can still run + `tests/test_config_version.py`, but nothing pinned the contract: dropping + one of the cleared `UV_INDEX`/`UV_EXTRA_INDEX_URL`/`UV_INDEX_URL` names or + the `UV_DEFAULT_INDEX` export would silently restore the failure the + wrapper was added for. The new `test_test_config_version_script.py` runs + the real script against a stand-in `uv` on `PATH` and asserts the + environment the child actually receives, plus that arguments such as + `--deselect` are forwarded to the + `pytest -q tests/test_config_version.py` invocation. + ([#7453]) + +- **mcp:** Add test coverage showing that waiters on other event loops retry the + token refresh after the refresh owner fails, whether the owner dies with a + cancellation or a `RuntimeError`. The new `OAuthTokenManager` cases hold + the first `post` to the token endpoint across a `ThreadPoolExecutor` of + independent `asyncio.run` callers, assert that a pending waiter elects a + replacement owner and that all waiters recover with the refreshed token. A + dynamic-context blocking-IO test now patches `_read_failures_are_fatal` to + false and releases its blocked inject in a `finally` block so the scenario + cleans up on assertion failure. + ([#7458]) + +- **deps:** Bump `pillow` from 12.3.0 to 13.0.0 in + `tests/skills/requirements.txt`. + ([#7443]) + +- **deps-dev:** Bump `happy-dom` from 20.11.1 to 21.0.4 in the frontend and + isolate decorative `FlipDisplay` animations from DOM test suites. Happy + DOM 21's animation cancellation rejects `finished`, so the content/state + suites for `subtask-card` and `tool-call-details` now swap in a + `tests/unit/helpers/flip-display.tsx` stub that keeps children, classes, and + keyed remounts via `uniqueKey` without exercising Motion's browser animation + lifecycle; real animation behavior stays in Playwright, and rejection + reporting stays enabled. + ([#7391]) + +- **lark:** Pin the failure contract of `_publish_bytes_atomically` for Lark + CLI downloads with regression tests and documentation. A failed write, + fsync, or `os.replace` leaves the previous file intact, and removing the + staged temporary is best-effort: when the unlink fails, or even the + warning-log handler itself raises, the original publish error (including a + `KeyboardInterrupt`) is still the one raised. The tests also record that + `mkstemp` publishes mode `0o600` and that callers chmod the final path + afterward. The cleanup call itself landed earlier in #7200; this change + locks it against regressions. + ([#7329]) + +- **deps:** Bump `next` from 16.3.8 to 16.4.1 in `frontend/package.json` and the + lockfile. + ([#7384]) + +- **skill:** Run the root public skill script regression suite (`tests/skills/`) + in Skill Review CI, independently of package review. A new `skill-tests` job + syncs the backend dev group and runs `pytest` against `tests/skills`, fails + instead of silently skipping data-analysis tests when `duckdb`, `openpyxl`, + or `requests` are missing, and sets `PYTHONDONTWRITEBYTECODE=1` so test + imports do not leave `.pyc` files inside the reviewed skill packages. + Changes to `tests/skills/**` now trigger the workflow, and a new + `test_skill_script_ci.py` pins that the job keeps collecting every root skill + test module and stays gated by the same draft filter as package review. + ([#7350]) + +- **config:** Add a `make test-config-version` target that runs the config + upgrade suite (`tests/test_config_version.py`) against public PyPI. On hosts + whose `uv` points at a mirror that blocks dependencies, the suite previously + failed with no workaround that left the operator's indexes alone; + `scripts/test-config-version.sh` unsets `UV_INDEX`, `UV_EXTRA_INDEX_URL`, and + the deprecated `UV_INDEX_URL`, exports + `UV_DEFAULT_INDEX=https://pypi.org/simple`, and runs + `uv run --locked pytest -q tests/test_config_version.py` with any extra + arguments forwarded. Normal `uv` and `config-upgrade` invocations keep the + host's configured indexes, and `CONTRIBUTING.md` documents the opt-in target. + ([#7251]) + +- **deps:** Bump `oauthlib` from 3.3.1 to 4.0.0 in the backend lockfile. The + dependency is indirect, so only `backend/uv.lock` changes. + ([#7240]) + +- **deps:** Bump `certifi` from 2026.4.22 to 2026.10.19 in the backend lockfile, + refreshing the bundled CA certificate list. + ([#7239]) + +- **deps:** Bump `pillow` from 12.3.0 to 13.0.0 in the backend lockfile. The + dependency is indirect, so only `backend/uv.lock` changes. + ([#7238]) + - **frontend:** Remove the deprecated, unused `hast` and `@types/gsap` packages. Existing `@types/hast` and GSAP's bundled types continue to supply the required declarations. ([#7108]) @@ -7859,17 +10306,21 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. writes, and verify healthy positions survive repeated bootstrap, downgrade, and re-upgrade. Documentation only — the repair itself landed in #5517; no runtime behavior change. ([#5518]) + - **capabilities:** Internal cleanup of the capability gallery and MCP connection validation: named status/action helpers with early returns, a separate `validate_mcp_connection()` with unchanged 422 responses, a shared `installationQuery()`, and removal of an unused display-name argument from `catalogForServer()`. Rendered UI and behavior are unchanged. ([#5580]) + - **deps:** Bump `anyio` from 4.13.0 to 4.14.2 in `/backend`. ([#5583]) + - **sandbox:** Share offline search contracts across sandbox providers. A new 112-case parameterized suite exercises the same `ls` / `glob` / `grep` scenarios through all six adapters against a shared fixture filesystem, covering ignored descendants, special paths, grep scope, overflow, transport failures, and unavailable binaries. Production code is unchanged. ([#5677]) + - **persistence:** `langgraph-checkpoint` is bumped to `>=4.2.0,<5.0` and the postgres extra's `langgraph-checkpoint-postgres` to `>=3.1.2,<3.2`, letting `checkpoint_patches.py` drop its `InMemorySaver` patch in favor of the @@ -7878,6 +10329,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. them a resolver can quietly install the versions whose bug is no longer patched. Behavior is unchanged — the local patch had been masking the bug. ([#5734]) + - **tests:** Cover the delta-history cache's full-to-delta migration and pin two open upstream `DeltaChannel` defects. The migration contract now runs with the history cache disabled, cold, and warm, asserting materialized @@ -7975,6 +10427,7 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. coverage stays excluded. A new test pins the concurrent startup and the wait-for-every-shard failure reporting with offline worker doubles. ([#6324]) + - **integrations:** Lark/Feishu CLI output is decoded as UTF-8 instead of with the host locale. `lark-cli` (a native binary shipped through the `@larksuite/cli` npm package) and npm both write UTF-8 to a pipe, but every @@ -12580,6 +15033,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#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 +[#5876]: https://github.com/bytedance/deer-flow/pull/5876 [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5880]: https://github.com/bytedance/deer-flow/pull/5880 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 @@ -12818,6 +15272,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6324]: https://github.com/bytedance/deer-flow/pull/6324 [#6326]: https://github.com/bytedance/deer-flow/pull/6326 [#6328]: https://github.com/bytedance/deer-flow/pull/6328 +[#6329]: https://github.com/bytedance/deer-flow/pull/6329 [#6332]: https://github.com/bytedance/deer-flow/pull/6332 [#6333]: https://github.com/bytedance/deer-flow/pull/6333 [#6334]: https://github.com/bytedance/deer-flow/pull/6334 @@ -12919,6 +15374,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6501]: https://github.com/bytedance/deer-flow/pull/6501 [#6502]: https://github.com/bytedance/deer-flow/pull/6502 [#6503]: https://github.com/bytedance/deer-flow/pull/6503 +[#6504]: https://github.com/bytedance/deer-flow/pull/6504 [#6505]: https://github.com/bytedance/deer-flow/pull/6505 [#6506]: https://github.com/bytedance/deer-flow/pull/6506 [#6508]: https://github.com/bytedance/deer-flow/pull/6508 @@ -12932,6 +15388,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6519]: https://github.com/bytedance/deer-flow/pull/6519 [#6520]: https://github.com/bytedance/deer-flow/pull/6520 [#6521]: https://github.com/bytedance/deer-flow/pull/6521 +[#6522]: https://github.com/bytedance/deer-flow/pull/6522 [#6523]: https://github.com/bytedance/deer-flow/pull/6523 [#6524]: https://github.com/bytedance/deer-flow/pull/6524 [#6527]: https://github.com/bytedance/deer-flow/pull/6527 @@ -13007,9 +15464,11 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6673]: https://github.com/bytedance/deer-flow/pull/6673 [#6674]: https://github.com/bytedance/deer-flow/pull/6674 [#6676]: https://github.com/bytedance/deer-flow/pull/6676 +[#6680]: https://github.com/bytedance/deer-flow/pull/6680 [#6681]: https://github.com/bytedance/deer-flow/pull/6681 [#6683]: https://github.com/bytedance/deer-flow/pull/6683 [#6685]: https://github.com/bytedance/deer-flow/pull/6685 +[#6688]: https://github.com/bytedance/deer-flow/pull/6688 [#6689]: https://github.com/bytedance/deer-flow/pull/6689 [#6690]: https://github.com/bytedance/deer-flow/pull/6690 [#6691]: https://github.com/bytedance/deer-flow/pull/6691 @@ -13022,6 +15481,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6706]: https://github.com/bytedance/deer-flow/pull/6706 [#6709]: https://github.com/bytedance/deer-flow/pull/6709 [#6714]: https://github.com/bytedance/deer-flow/pull/6714 +[#6715]: https://github.com/bytedance/deer-flow/pull/6715 [#6716]: https://github.com/bytedance/deer-flow/pull/6716 [#6717]: https://github.com/bytedance/deer-flow/pull/6717 [#6719]: https://github.com/bytedance/deer-flow/pull/6719 @@ -13047,10 +15507,12 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6762]: https://github.com/bytedance/deer-flow/pull/6762 [#6764]: https://github.com/bytedance/deer-flow/pull/6764 [#6768]: https://github.com/bytedance/deer-flow/pull/6768 +[#6770]: https://github.com/bytedance/deer-flow/pull/6770 [#6772]: https://github.com/bytedance/deer-flow/pull/6772 [#6774]: https://github.com/bytedance/deer-flow/pull/6774 [#6776]: https://github.com/bytedance/deer-flow/pull/6776 [#6779]: https://github.com/bytedance/deer-flow/pull/6779 +[#6780]: https://github.com/bytedance/deer-flow/pull/6780 [#6781]: https://github.com/bytedance/deer-flow/pull/6781 [#6782]: https://github.com/bytedance/deer-flow/pull/6782 [#6783]: https://github.com/bytedance/deer-flow/pull/6783 @@ -13110,6 +15572,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6882]: https://github.com/bytedance/deer-flow/pull/6882 [#6885]: https://github.com/bytedance/deer-flow/pull/6885 [#6886]: https://github.com/bytedance/deer-flow/pull/6886 +[#6888]: https://github.com/bytedance/deer-flow/pull/6888 [#6891]: https://github.com/bytedance/deer-flow/pull/6891 [#6893]: https://github.com/bytedance/deer-flow/pull/6893 [#6894]: https://github.com/bytedance/deer-flow/pull/6894 @@ -13149,6 +15612,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6957]: https://github.com/bytedance/deer-flow/pull/6957 [#6959]: https://github.com/bytedance/deer-flow/pull/6959 [#6961]: https://github.com/bytedance/deer-flow/pull/6961 +[#6965]: https://github.com/bytedance/deer-flow/pull/6965 [#6967]: https://github.com/bytedance/deer-flow/pull/6967 [#6969]: https://github.com/bytedance/deer-flow/pull/6969 [#6971]: https://github.com/bytedance/deer-flow/pull/6971 @@ -13167,7 +15631,9 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7001]: https://github.com/bytedance/deer-flow/pull/7001 [#7002]: https://github.com/bytedance/deer-flow/pull/7002 [#7004]: https://github.com/bytedance/deer-flow/pull/7004 +[#7006]: https://github.com/bytedance/deer-flow/pull/7006 [#7014]: https://github.com/bytedance/deer-flow/pull/7014 +[#7016]: https://github.com/bytedance/deer-flow/pull/7016 [#7017]: https://github.com/bytedance/deer-flow/pull/7017 [#7018]: https://github.com/bytedance/deer-flow/pull/7018 [#7020]: https://github.com/bytedance/deer-flow/pull/7020 @@ -13176,33 +15642,50 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7031]: https://github.com/bytedance/deer-flow/pull/7031 [#7033]: https://github.com/bytedance/deer-flow/pull/7033 [#7035]: https://github.com/bytedance/deer-flow/pull/7035 +[#7037]: https://github.com/bytedance/deer-flow/pull/7037 [#7039]: https://github.com/bytedance/deer-flow/pull/7039 [#7040]: https://github.com/bytedance/deer-flow/pull/7040 -[#7048]: https://github.com/bytedance/deer-flow/pull/7048 [#7042]: https://github.com/bytedance/deer-flow/pull/7042 +[#7043]: https://github.com/bytedance/deer-flow/pull/7043 +[#7045]: https://github.com/bytedance/deer-flow/pull/7045 +[#7048]: https://github.com/bytedance/deer-flow/pull/7048 +[#7049]: https://github.com/bytedance/deer-flow/pull/7049 [#7051]: https://github.com/bytedance/deer-flow/pull/7051 [#7053]: https://github.com/bytedance/deer-flow/pull/7053 +[#7054]: https://github.com/bytedance/deer-flow/pull/7054 [#7057]: https://github.com/bytedance/deer-flow/pull/7057 [#7058]: https://github.com/bytedance/deer-flow/pull/7058 [#7059]: https://github.com/bytedance/deer-flow/pull/7059 [#7061]: https://github.com/bytedance/deer-flow/pull/7061 [#7065]: https://github.com/bytedance/deer-flow/pull/7065 +[#7071]: https://github.com/bytedance/deer-flow/pull/7071 [#7074]: https://github.com/bytedance/deer-flow/pull/7074 [#7075]: https://github.com/bytedance/deer-flow/pull/7075 [#7076]: https://github.com/bytedance/deer-flow/pull/7076 [#7078]: https://github.com/bytedance/deer-flow/pull/7078 +[#7081]: https://github.com/bytedance/deer-flow/pull/7081 [#7084]: https://github.com/bytedance/deer-flow/pull/7084 +[#7088]: https://github.com/bytedance/deer-flow/pull/7088 +[#7092]: https://github.com/bytedance/deer-flow/pull/7092 [#7093]: https://github.com/bytedance/deer-flow/pull/7093 [#7099]: https://github.com/bytedance/deer-flow/pull/7099 +[#7102]: https://github.com/bytedance/deer-flow/pull/7102 [#7104]: https://github.com/bytedance/deer-flow/pull/7104 [#7106]: https://github.com/bytedance/deer-flow/pull/7106 [#7108]: https://github.com/bytedance/deer-flow/pull/7108 +[#7110]: https://github.com/bytedance/deer-flow/pull/7110 [#7112]: https://github.com/bytedance/deer-flow/pull/7112 +[#7115]: https://github.com/bytedance/deer-flow/pull/7115 [#7116]: https://github.com/bytedance/deer-flow/pull/7116 [#7117]: https://github.com/bytedance/deer-flow/pull/7117 [#7122]: https://github.com/bytedance/deer-flow/pull/7122 [#7123]: https://github.com/bytedance/deer-flow/pull/7123 [#7127]: https://github.com/bytedance/deer-flow/pull/7127 +[#7128]: https://github.com/bytedance/deer-flow/pull/7128 +[#7132]: https://github.com/bytedance/deer-flow/pull/7132 +[#7134]: https://github.com/bytedance/deer-flow/pull/7134 +[#7136]: https://github.com/bytedance/deer-flow/pull/7136 +[#7138]: https://github.com/bytedance/deer-flow/pull/7138 [#7140]: https://github.com/bytedance/deer-flow/pull/7140 [#7141]: https://github.com/bytedance/deer-flow/pull/7141 [#7144]: https://github.com/bytedance/deer-flow/pull/7144 @@ -13210,39 +15693,219 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7150]: https://github.com/bytedance/deer-flow/pull/7150 [#7151]: https://github.com/bytedance/deer-flow/pull/7151 [#7152]: https://github.com/bytedance/deer-flow/pull/7152 +[#7153]: https://github.com/bytedance/deer-flow/pull/7153 [#7154]: https://github.com/bytedance/deer-flow/pull/7154 [#7155]: https://github.com/bytedance/deer-flow/pull/7155 +[#7159]: https://github.com/bytedance/deer-flow/pull/7159 +[#7161]: https://github.com/bytedance/deer-flow/pull/7161 [#7163]: https://github.com/bytedance/deer-flow/pull/7163 +[#7164]: https://github.com/bytedance/deer-flow/pull/7164 +[#7165]: https://github.com/bytedance/deer-flow/pull/7165 [#7167]: https://github.com/bytedance/deer-flow/pull/7167 +[#7169]: https://github.com/bytedance/deer-flow/pull/7169 [#7170]: https://github.com/bytedance/deer-flow/pull/7170 +[#7172]: https://github.com/bytedance/deer-flow/pull/7172 +[#7175]: https://github.com/bytedance/deer-flow/pull/7175 +[#7180]: https://github.com/bytedance/deer-flow/pull/7180 +[#7182]: https://github.com/bytedance/deer-flow/pull/7182 +[#7183]: https://github.com/bytedance/deer-flow/pull/7183 +[#7186]: https://github.com/bytedance/deer-flow/pull/7186 +[#7189]: https://github.com/bytedance/deer-flow/pull/7189 +[#7193]: https://github.com/bytedance/deer-flow/pull/7193 +[#7196]: https://github.com/bytedance/deer-flow/pull/7196 +[#7198]: https://github.com/bytedance/deer-flow/pull/7198 +[#7200]: https://github.com/bytedance/deer-flow/pull/7200 +[#7202]: https://github.com/bytedance/deer-flow/pull/7202 +[#7203]: https://github.com/bytedance/deer-flow/pull/7203 [#7205]: https://github.com/bytedance/deer-flow/pull/7205 [#7207]: https://github.com/bytedance/deer-flow/pull/7207 +[#7209]: https://github.com/bytedance/deer-flow/pull/7209 +[#7213]: https://github.com/bytedance/deer-flow/pull/7213 +[#7214]: https://github.com/bytedance/deer-flow/pull/7214 [#7217]: https://github.com/bytedance/deer-flow/pull/7217 +[#7221]: https://github.com/bytedance/deer-flow/pull/7221 +[#7228]: https://github.com/bytedance/deer-flow/pull/7228 +[#7229]: https://github.com/bytedance/deer-flow/pull/7229 +[#7231]: https://github.com/bytedance/deer-flow/pull/7231 +[#7233]: https://github.com/bytedance/deer-flow/pull/7233 +[#7234]: https://github.com/bytedance/deer-flow/pull/7234 +[#7236]: https://github.com/bytedance/deer-flow/pull/7236 +[#7238]: https://github.com/bytedance/deer-flow/pull/7238 +[#7239]: https://github.com/bytedance/deer-flow/pull/7239 +[#7240]: https://github.com/bytedance/deer-flow/pull/7240 +[#7245]: https://github.com/bytedance/deer-flow/pull/7245 +[#7247]: https://github.com/bytedance/deer-flow/pull/7247 +[#7249]: https://github.com/bytedance/deer-flow/pull/7249 +[#7251]: https://github.com/bytedance/deer-flow/pull/7251 +[#7253]: https://github.com/bytedance/deer-flow/pull/7253 +[#7254]: https://github.com/bytedance/deer-flow/pull/7254 [#7255]: https://github.com/bytedance/deer-flow/pull/7255 +[#7256]: https://github.com/bytedance/deer-flow/pull/7256 +[#7257]: https://github.com/bytedance/deer-flow/pull/7257 [#7258]: https://github.com/bytedance/deer-flow/pull/7258 +[#7259]: https://github.com/bytedance/deer-flow/pull/7259 +[#7261]: https://github.com/bytedance/deer-flow/pull/7261 +[#7264]: https://github.com/bytedance/deer-flow/pull/7264 +[#7266]: https://github.com/bytedance/deer-flow/pull/7266 +[#7269]: https://github.com/bytedance/deer-flow/pull/7269 +[#7271]: https://github.com/bytedance/deer-flow/pull/7271 +[#7273]: https://github.com/bytedance/deer-flow/pull/7273 +[#7274]: https://github.com/bytedance/deer-flow/pull/7274 [#7277]: https://github.com/bytedance/deer-flow/pull/7277 +[#7279]: https://github.com/bytedance/deer-flow/pull/7279 +[#7281]: https://github.com/bytedance/deer-flow/pull/7281 +[#7284]: https://github.com/bytedance/deer-flow/pull/7284 +[#7286]: https://github.com/bytedance/deer-flow/pull/7286 +[#7289]: https://github.com/bytedance/deer-flow/pull/7289 +[#7290]: https://github.com/bytedance/deer-flow/pull/7290 +[#7291]: https://github.com/bytedance/deer-flow/pull/7291 +[#7293]: https://github.com/bytedance/deer-flow/pull/7293 +[#7294]: https://github.com/bytedance/deer-flow/pull/7294 +[#7295]: https://github.com/bytedance/deer-flow/pull/7295 +[#7297]: https://github.com/bytedance/deer-flow/pull/7297 +[#7299]: https://github.com/bytedance/deer-flow/pull/7299 +[#7301]: https://github.com/bytedance/deer-flow/pull/7301 +[#7304]: https://github.com/bytedance/deer-flow/pull/7304 +[#7305]: https://github.com/bytedance/deer-flow/pull/7305 +[#7306]: https://github.com/bytedance/deer-flow/pull/7306 +[#7307]: https://github.com/bytedance/deer-flow/pull/7307 +[#7308]: https://github.com/bytedance/deer-flow/pull/7308 +[#7311]: https://github.com/bytedance/deer-flow/pull/7311 +[#7312]: https://github.com/bytedance/deer-flow/pull/7312 +[#7313]: https://github.com/bytedance/deer-flow/pull/7313 [#7325]: https://github.com/bytedance/deer-flow/pull/7325 [#7326]: https://github.com/bytedance/deer-flow/pull/7326 +[#7328]: https://github.com/bytedance/deer-flow/pull/7328 +[#7329]: https://github.com/bytedance/deer-flow/pull/7329 +[#7333]: https://github.com/bytedance/deer-flow/pull/7333 +[#7338]: https://github.com/bytedance/deer-flow/pull/7338 +[#7340]: https://github.com/bytedance/deer-flow/pull/7340 +[#7342]: https://github.com/bytedance/deer-flow/pull/7342 +[#7345]: https://github.com/bytedance/deer-flow/pull/7345 +[#7347]: https://github.com/bytedance/deer-flow/pull/7347 +[#7348]: https://github.com/bytedance/deer-flow/pull/7348 +[#7349]: https://github.com/bytedance/deer-flow/pull/7349 +[#7350]: https://github.com/bytedance/deer-flow/pull/7350 +[#7351]: https://github.com/bytedance/deer-flow/pull/7351 +[#7352]: https://github.com/bytedance/deer-flow/pull/7352 +[#7353]: https://github.com/bytedance/deer-flow/pull/7353 +[#7354]: https://github.com/bytedance/deer-flow/pull/7354 +[#7355]: https://github.com/bytedance/deer-flow/pull/7355 +[#7356]: https://github.com/bytedance/deer-flow/pull/7356 +[#7357]: https://github.com/bytedance/deer-flow/pull/7357 +[#7358]: https://github.com/bytedance/deer-flow/pull/7358 +[#7360]: https://github.com/bytedance/deer-flow/pull/7360 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 +[#7363]: https://github.com/bytedance/deer-flow/pull/7363 [#7364]: https://github.com/bytedance/deer-flow/pull/7364 +[#7369]: https://github.com/bytedance/deer-flow/pull/7369 +[#7370]: https://github.com/bytedance/deer-flow/pull/7370 +[#7371]: https://github.com/bytedance/deer-flow/pull/7371 +[#7372]: https://github.com/bytedance/deer-flow/pull/7372 +[#7374]: https://github.com/bytedance/deer-flow/pull/7374 +[#7377]: https://github.com/bytedance/deer-flow/pull/7377 +[#7378]: https://github.com/bytedance/deer-flow/pull/7378 +[#7383]: https://github.com/bytedance/deer-flow/pull/7383 +[#7384]: https://github.com/bytedance/deer-flow/pull/7384 +[#7388]: https://github.com/bytedance/deer-flow/pull/7388 +[#7390]: https://github.com/bytedance/deer-flow/pull/7390 +[#7391]: https://github.com/bytedance/deer-flow/pull/7391 +[#7393]: https://github.com/bytedance/deer-flow/pull/7393 +[#7394]: https://github.com/bytedance/deer-flow/pull/7394 +[#7395]: https://github.com/bytedance/deer-flow/pull/7395 +[#7397]: https://github.com/bytedance/deer-flow/pull/7397 +[#7399]: https://github.com/bytedance/deer-flow/pull/7399 [#7400]: https://github.com/bytedance/deer-flow/pull/7400 +[#7404]: https://github.com/bytedance/deer-flow/pull/7404 +[#7407]: https://github.com/bytedance/deer-flow/pull/7407 +[#7409]: https://github.com/bytedance/deer-flow/pull/7409 [#7410]: https://github.com/bytedance/deer-flow/pull/7410 +[#7411]: https://github.com/bytedance/deer-flow/pull/7411 +[#7412]: https://github.com/bytedance/deer-flow/pull/7412 +[#7417]: https://github.com/bytedance/deer-flow/pull/7417 +[#7418]: https://github.com/bytedance/deer-flow/pull/7418 +[#7423]: https://github.com/bytedance/deer-flow/pull/7423 +[#7428]: https://github.com/bytedance/deer-flow/pull/7428 +[#7429]: https://github.com/bytedance/deer-flow/pull/7429 +[#7430]: https://github.com/bytedance/deer-flow/pull/7430 +[#7432]: https://github.com/bytedance/deer-flow/pull/7432 +[#7441]: https://github.com/bytedance/deer-flow/pull/7441 +[#7442]: https://github.com/bytedance/deer-flow/pull/7442 +[#7443]: https://github.com/bytedance/deer-flow/pull/7443 [#7445]: https://github.com/bytedance/deer-flow/pull/7445 +[#7446]: https://github.com/bytedance/deer-flow/pull/7446 [#7449]: https://github.com/bytedance/deer-flow/pull/7449 +[#7450]: https://github.com/bytedance/deer-flow/pull/7450 +[#7452]: https://github.com/bytedance/deer-flow/pull/7452 +[#7453]: https://github.com/bytedance/deer-flow/pull/7453 +[#7455]: https://github.com/bytedance/deer-flow/pull/7455 +[#7456]: https://github.com/bytedance/deer-flow/pull/7456 +[#7458]: https://github.com/bytedance/deer-flow/pull/7458 +[#7462]: https://github.com/bytedance/deer-flow/pull/7462 +[#7465]: https://github.com/bytedance/deer-flow/pull/7465 [#7467]: https://github.com/bytedance/deer-flow/pull/7467 +[#7473]: https://github.com/bytedance/deer-flow/pull/7473 +[#7474]: https://github.com/bytedance/deer-flow/pull/7474 +[#7483]: https://github.com/bytedance/deer-flow/pull/7483 +[#7484]: https://github.com/bytedance/deer-flow/pull/7484 +[#7485]: https://github.com/bytedance/deer-flow/pull/7485 +[#7489]: https://github.com/bytedance/deer-flow/pull/7489 +[#7492]: https://github.com/bytedance/deer-flow/pull/7492 +[#7496]: https://github.com/bytedance/deer-flow/pull/7496 +[#7498]: https://github.com/bytedance/deer-flow/pull/7498 +[#7499]: https://github.com/bytedance/deer-flow/pull/7499 +[#7500]: https://github.com/bytedance/deer-flow/pull/7500 +[#7501]: https://github.com/bytedance/deer-flow/pull/7501 +[#7503]: https://github.com/bytedance/deer-flow/pull/7503 +[#7504]: https://github.com/bytedance/deer-flow/pull/7504 +[#7505]: https://github.com/bytedance/deer-flow/pull/7505 [#7507]: https://github.com/bytedance/deer-flow/pull/7507 +[#7510]: https://github.com/bytedance/deer-flow/pull/7510 +[#7511]: https://github.com/bytedance/deer-flow/pull/7511 +[#7512]: https://github.com/bytedance/deer-flow/pull/7512 +[#7513]: https://github.com/bytedance/deer-flow/pull/7513 +[#7516]: https://github.com/bytedance/deer-flow/pull/7516 +[#7517]: https://github.com/bytedance/deer-flow/pull/7517 [#7521]: https://github.com/bytedance/deer-flow/pull/7521 [#7530]: https://github.com/bytedance/deer-flow/pull/7530 +[#7532]: https://github.com/bytedance/deer-flow/pull/7532 +[#7542]: https://github.com/bytedance/deer-flow/pull/7542 +[#7543]: https://github.com/bytedance/deer-flow/pull/7543 +[#7546]: https://github.com/bytedance/deer-flow/pull/7546 +[#7548]: https://github.com/bytedance/deer-flow/pull/7548 +[#7553]: https://github.com/bytedance/deer-flow/pull/7553 +[#7554]: https://github.com/bytedance/deer-flow/pull/7554 +[#7555]: https://github.com/bytedance/deer-flow/pull/7555 +[#7556]: https://github.com/bytedance/deer-flow/pull/7556 [#7557]: https://github.com/bytedance/deer-flow/pull/7557 [#7560]: https://github.com/bytedance/deer-flow/pull/7560 +[#7561]: https://github.com/bytedance/deer-flow/pull/7561 +[#7564]: https://github.com/bytedance/deer-flow/pull/7564 +[#7566]: https://github.com/bytedance/deer-flow/pull/7566 +[#7567]: https://github.com/bytedance/deer-flow/pull/7567 +[#7569]: https://github.com/bytedance/deer-flow/pull/7569 +[#7572]: https://github.com/bytedance/deer-flow/pull/7572 [#7576]: https://github.com/bytedance/deer-flow/pull/7576 +[#7579]: https://github.com/bytedance/deer-flow/pull/7579 [#7580]: https://github.com/bytedance/deer-flow/pull/7580 +[#7582]: https://github.com/bytedance/deer-flow/pull/7582 [#7585]: https://github.com/bytedance/deer-flow/pull/7585 +[#7586]: https://github.com/bytedance/deer-flow/pull/7586 [#7588]: https://github.com/bytedance/deer-flow/pull/7588 +[#7589]: https://github.com/bytedance/deer-flow/pull/7589 [#7591]: https://github.com/bytedance/deer-flow/pull/7591 [#7593]: https://github.com/bytedance/deer-flow/pull/7593 [#7594]: https://github.com/bytedance/deer-flow/pull/7594 [#7597]: https://github.com/bytedance/deer-flow/pull/7597 +[#7604]: https://github.com/bytedance/deer-flow/pull/7604 [#7606]: https://github.com/bytedance/deer-flow/pull/7606 +[#7609]: https://github.com/bytedance/deer-flow/pull/7609 +[#7611]: https://github.com/bytedance/deer-flow/pull/7611 +[#7612]: https://github.com/bytedance/deer-flow/pull/7612 +[#7617]: https://github.com/bytedance/deer-flow/pull/7617 +[#7622]: https://github.com/bytedance/deer-flow/pull/7622 +[#7628]: https://github.com/bytedance/deer-flow/pull/7628 [#7631]: https://github.com/bytedance/deer-flow/pull/7631 +[#7643]: https://github.com/bytedance/deer-flow/pull/7643 [#7651]: https://github.com/bytedance/deer-flow/pull/7651 +[#7659]: https://github.com/bytedance/deer-flow/pull/7659 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 26a57c66cd..7b0b9b8bce 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -10,10 +10,218 @@ ## [未发布] 本节累积面向 **2.2.0** 里程碑([2.2.0](https://github.com/bytedance/deer-flow/milestone/3))的工作。 -截至 2026-10-21,该里程碑已合并 **829 个拉取请求**。 +截至 2026-10-28,该里程碑已合并 **1068 个拉取请求**。 ### 新增 +- **插件:** 新增仅管理员可访问的 `GET /api/plugins/diagnostics` 端点, + 返回 Gateway 进程保留的扩展诊断的脱敏快照。每个条目带有 `source` + (限制为至多 256 字符的点分标识符,其他值一律返回 `[redacted]`)、 + 限定为 `debug`/`info`/`warning`/`error` 的 `level`,以及固定的 + `summary` 分类——扩展加载、注册、路由挂载或服务启动/停止——分类按 + 消息前缀选取,绝不使用原始异常文本,后者仍留在 Gateway 日志中。响应 + 以 `Cache-Control: private, no-store` 发送。([#7612]) + +- **扩展:** agent-teams 示例扩展获得持久的团队公开状态:三个新的模型 + 工具 `publish_team_state`、`read_team_state` 和 `update_team_state` 让 + 成员能够存储目标、计划、发现、问题和需求,这些内容在共享消息窗口之外 + 持续存在。发布按成员 `request_id` 幂等;更新使用 `expected_revision` + 乐观并发,过期的写入方收到当前条目而不是覆盖它;条目携带创建者/编辑者 + 与来源归属;状态有界于 20 个条目和 16,000 UTF-8 JSON 字节。活跃状态会 + 注入每个成员的共享上下文、排在近期消息之前;团队浏览器页面显示状态 + 快照,并附有指回来源对话的链接。([#7611]) + +- **模型:** `moonshot-cn` 受管预设现在识别 `kimi-k3.1-pro` 和 + `kimi-k3.1-pro-highspeed`:必需思考、保留推理历史、支持视觉,并使用 + 原生 `reasoning_effort` 取值 `low`/`high`/`max`(默认 `max`;不支持的 + 取值回退到默认)。在官方 `https://api.moonshot.cn/v1` 端点上, + `PatchedChatDeepSeek` 现在会从最终请求中省略 `thinking` 和固定的采样键 + `temperature`、`top_p`、`n`、`presence_penalty`、`frequency_penalty` + ——包括通过 `extra_body` 提供的副本,且不改动调用方拥有的映射——让 + Moonshot 应用它自己的固定值。端点检查匹配包含 `extra_body.model` 覆盖 + 在内的生效请求模型;Kimi K2.5、未知 ID 和其他端点保持不变。([#7566]) + +- **jina:** 每次 Jina 抓取结束输出一条有界的 DEBUG 完成摘要——一行 + `Jina crawl completed`,带 `outcome`(`success`、`error` 或 + `cancelled`)、分类后的 `reason`(`success`、`empty_response`、 + `response_limit`、`invalid_configuration`、`nonretryable_http`、 + `attempt_exhaustion`、`retry_after_unfit`、`budget_exhaustion`、 + `request_failure` 或 `cancellation`)、在调用开始时计数的 HTTPX + `post`/`stream` 尝试次数,以及度量实际等待时长的单调 + `elapsed_seconds`/`backoff_seconds`,包括被部分取消的睡眠。由仓库根 + `config.yaml` 的 `log_level: debug` 启用;INFO 级别不输出任何内容。 + 摘要不包含 URL、凭据、请求头、响应体或异常文本,日志处理器失败会被 + 吞掉,因此诊断输出绝不会掩盖结果或取消;返回值与错误日志不变。([#6504]) + +- **运行时:** 将 Web UI 的语言设置(设置 > 外观,`en-US` 或 `zh-CN`) + 以仅限单次请求的 `context.locale` 带入运行:网关按有限允许列表校验该值, + 并剥离遗留的 `configurable` 副本,使其永远不会成为检查点默认值; + `DynamicContextMiddleware` 将其投影为隐藏的 + `<response_language_preference>` 人类消息,输入净化会阻止不可信输入 + 伪造该消息;除非用户明确要求其他语言,主智能体都会用该语言撰写回复。 + 内置 DeerMem 会为经 PII 脱敏的排队消息副本打上 `deerflow_ui_locale` + 标记,并用首选语言写入新事实与摘要文本,但不改动已存储的事实、JSON + 键或记忆后端 API。没有受支持 locale 的运行仍从最新消息推断语言。([#7294]) + +- **批处理:** 新增对已持久化子智能体批量结果的浏览器查看:新的经属主 + 校验的 `GET .../subagent-batches/{batch_id}/items/{position}` 路由由 + `SubagentBatchService.read_browser_batch_item` 提供,返回报告、验收、 + 计时与证据字段的固定投影,既不暴露提示词或执行规格,也无需存活的 + 批处理 worker 或提供方拉取即可工作。其证据快照对 `knowledge_sources` + v1 RAGFlow 字段做白名单过滤,并在读取时对遗留行重新限定边界(100 个 + 来源、512 字符名称、256 字符 ID、16,000 字符文本、最多 100 个有效页), + 超出部分计入省略数而不是返回超大数据。工作区批处理面板新增逐条结果 + 对话框,以引用的证据渲染报告,并在重试或状态变更后刷新。([#7006]) + +- **知识:** 发布可选的 `deerflow-extension-knowledge-platform` 示例扩展 + (知识工作的第三阶段):由运维方配置的平台连接复用知识管理的 + `ManagementProvider` 工厂,并提供层级逻辑空间(最多 500 个空间、每个 + 200 名成员、1 MiB 策略 JSON),其 `blocked`/`viewer`/`editor`/`manager` + 成员从祖先继承资源、操作、工具与处理默认值,且只能收紧它们。位于 + `/api/extensions/knowledge-platform` 下的路由提供仅管理员可用的策略 + 快照、带版本号校验的原子策略替换(过期返回 `409`,写入在 + `BEGIN IMMEDIATE` 下进行),以及按空间路由的管理操作——它们叠加宿主 + 插件对两个命名空间的授权、管理员下限、空间授权以及既有的适配器与 + 上游授权。空间策略只管辖这些路由——不会改变 `knowledge_search` 的 + 路由,也不授予聊天检索;SQLite 失败返回净化后的 `503`。([#7417]) + +- **运行时:** 为已完成的运行发布权威的 `run.terminal` 追踪事件:属主 + worker 的收尾流程在最终提交 ACK 与 Journal 排空之后、证据回执捕获 + 之前,通过事件存储的原子 `put_if_absent`(`confirm_terminal_event`) + 将其确认为运行范围内的单例,携带最终状态(`success`、`error`、 + `interrupted` 或 `timeout`)以及 `protocol_version: 1` 元数据。确认要求 + `RunManager.terminal_event_status`——一条归因于本 worker 的最终 + ACK——因此读取到匹配行或回滚产生的中间 ACK 均不构成授权,冲突或未知 + 结果会让封存失败且不改变业务状态。终止确认现在还要求一次显式成功的 + `update_status` 持久化写入,运行事件流契约也补充记录了这一新增 + 事件。([#7134]) + +- **项目:** 新增 `search_project_documents` 项目架工具:先在整个项目架 + 上匹配文档名称与摘要,只有命中过少时才在解码字符预算内进行内容扫描 + (`projects.search_max_chars_per_document`,每篇文档默认 1,000,000; + `projects.search_max_total_chars`,整个项目架默认 8,000,000;两者每次 + 调用都会读取,无效组合会将其重置为默认值)。内容命中携带原文的字符 + 偏移和中和后的摘录,可交给 `read_project_document(offset=...)` 使用, + 且搜索绝不会触发即时文档转换。项目架通过一次快照查询物化,并由两个 + 层级共享,因此并发的上传或回收站操作不会造成命中重复或遗漏;结果会 + 报告 `stop_reason`、跳过计数以及封顶的已截断/未扫描 ID 列表,部分 + 覆盖永远不会被声称为完整。配置版本升到 62。([#7286]) + +- **渠道:** 为飞书待处理卡片注册表新增线程级生命周期辅助方法—— + `discard_thread` 退役一个线程注册的所有待处理卡片,`prune_expired` + 丢弃 TTL 过期的条目,`get_pending_entry` 读取条目但不消费它——并把 + 退役接入对话生命周期:`ChannelManager` 在 `/new` 时查找旧线程并调用 + 新的 `Channel.on_thread_replaced` 钩子,飞书以丢弃该线程的注册条目 + 来实现它,使过期的卡片按钮点击被拒绝而不是滞留。([#7357]) + +- **插件:** 新增 `deerflow-extension-report-sections` 示例扩展:有状态的 + `save_section` 模型工具把长报告逐节存入插件自己的 SQLite 文件(按用户 + 与线程划分范围;大纲存于索引 0,各章存于 1..N,每节上限 24,000 + 字符),最后一次 `section_index=999` 调用把所有已存小节组装成一份 + markdown 文档并作为工具工件交付,只有一行简短状态重新进入模型上下文, + 完整报告不占用上下文预算。组装会丢弃重复小节标题的开头标题行,并 + 拒绝超过 60,000 字节上限的组装文档,各小节保持已存储状态。([#7371]) + +- **知识:** 新增可选的 `deerflow-extension-knowledge-management` 扩展, + 将文档摄取到 WeKnora v0.8.3 知识库,并报告由提供方持有的处理快照, + 不触碰核心检索。`/api/extensions/knowledge-management` 下的路由覆盖 + 能力、摄取、状态、重新处理与取消,每个都要求非内部的管理员主体, + 外加 `config.yaml -> plugins:` 中针对确切用户、连接、知识库与操作的 + 运维方授权。快照规范化上游状态(无法识别的状态保持 `unknown`),不 + 虚构任务 ID 或分阶段成功;写入绝不自动重试;父子知识库上的 chunk + 覆盖以 `422` 失败;凭据缺失经 `ManagementCredentialUnavailable` 映射为 + `503`;日志只保留异常类型、HTTP 状态与固定类别。([#7293]) + +- **扩展:** 新增 `deerflow extensions list --json`,将既有的 + `list_configured()` 清单按配置顺序序列化为 JSON 数组,只含 `name`、 + `package`、`entry_point`、`enabled` 与 `required`;未配置的包保持 + `"-"`,空清单打印 `[]`,默认表格输出不变。整个清单在任何 stdout 写入 + 之前完成校验,因此配置错误仍以非零码退出、诊断输出到 stderr 且无 + 部分输出;列举从不解析 entry point,也不暴露私有的插件配置或源 + 元数据。([#7351]) + +- **工作流:** 为 workflows 扩展新增第 4 阶段的多网关协调:共享的 + PostgreSQL `storage_url` 退出 SQLite 单属主锁,因此在一个网关上启动的 + 工作流可以在另一个网关上查看、审批、恢复或取消,且租约到期后的恢复 + 仍需会话授权而非无人值守——恢复方用户提供新的能力凭据。宿主新增 + `extension_execution_leases` 表(迁移 + `0048_extension_execution_leases.py`);公共 API 新增 `ExecutionFence`, + `AgentRuns.start`/`cancel` 在准入或取消事务内以行锁检查用户、持有者、 + epoch 与到期时间,失败即关闭,且不提供运行心跳或跨进程流桥接。 + SQLite 模式保留第 3 阶段的单属主契约。([#7138]) + +- **智能体:** 新增只读内建工具 `inspect_run_status`,返回当前运行的 + 无内容 JSON 快照——令牌用量合计、消息与工具调用计数、待处理与进行中 + 的待办计数、是否存在压缩摘要,以及以上次模型报告的输入对有效模型所 + 配 `context_window` 计算的上下文占用(`used_input_tokens`、 + `limit_tokens`、`fraction`)。当运行前的消息 ID 记录在 + `CURRENT_RUN_PRE_EXISTING_MESSAGE_IDS_KEY` 下时,计数以当前运行为 + 范围,否则标记为 `visible_state`(若压缩已移除全部运行前 ID,则保留 + 不确定的标记而不声称运行范围);当 `task`/`batch_task` 派发消息已把 + 已完成子智能体的用量并入自身时,其输入计数会从占用估算中排除。该 + 工具绝不返回对话或待办文本。([#7297]) + +- **社区工具:** 将 You.com 的 `web_search` 工具迁移为基于 + `httpx.AsyncClient` 的协程;此前的同步 `httpx.Client` 版本在 `ainvoke` + 下运行于默认执行器,整个网络往返期间占用一个线程池线程,导致并行的 + 提供方查询彼此排队。API 密钥与配置的 `max_results` 现在通过 + `asyncio.to_thread` 在单次 `_resolve_tool_settings` 读取中解析, + `User-Agent` 的冷启动包元数据读取只在首次调用时离开事件循环执行, + 后续调用命中内存缓存。 + `backend/tests/blocking_io/test_youcom_search_async.py` 固定了循环线程 + 请求、循环外配置查找与所用客户端工厂——这些覆盖仅靠线程断言是 + 发现不了的。([#7186]) + +- **工具:** Exa 的 `web_search` 与 `web_fetch` 工具可通过按工具配置的 + `base_url` 条目(最高优先级)或 `EXA_BASE_URL` 环境变量指向 + Exa 兼容的提供方,两者都未设置时回退到 SDK 默认的 + `https://api.exa.ai`。覆盖值必须是带主机和有效端口、无查询串或片段的 + 绝对 `http://`/`https://` URL;空白与末尾斜杠会被去除,无效值会引发 + 配置错误,既不回显配置值,也绝不回退到其他主机。由于自定义后端在其 + 自己的网络中抓取页面,`web_fetch` 会用 `validate_delegated_backend_url` + 对配置的 base URL 做筛查,拒绝解析到环回、私有或不可验证地址的值, + 除非工具条目设置 `network_isolation_confirmed: true`。API 密钥通过 + `x-api-key` 请求头发送给配置的主机。([#7306]) + +- **社区工具:** 新增 `deerflow.community.jsonify.tools:dataset_rows_tool`, + 一个只读工具,返回受维护 Jsonify 数据集的行,让智能体读取当前表格而 + 不是抓取页面。运维方在工具条目(`datasets`、`max_rows`)中把数据集 + 名称映射到 serve URL;模型按名称选择,永远看不到 URL——该凭据必须是 + https 且不含 userinfo,在任何请求之前通过 `validate_public_http_url` + 校验,并从错误文本中脱敏,传输失败只报告异常类型。分页遵循 + `has_more`/`next_offset`,数据集版本在第一页之后固定,遇到空页或不再 + 前进的游标即停止,行数按 `max_rows` 封顶并带 `truncated` 标记,并剥离 + `_parent_row_id`/`_source_key` 系统列。([#7253]) + +- **模型:** Web 对话新增按作用域的模型控制:只读的 `POST /api/models/resolve` + 端点将调用方的直接输入与中英文选择短语匹配,返回作用于 `task`、`turn` 或 + `conversation` 的决定,并用 `model:use` 授权解析出的选择,因此被识别但被拒绝 + 的名称会变成 `unavailable`,而不是退化为任务文本。模型最多可声明 16 个 + `aliases`;聊天页面通过人工输入提示解决歧义结果,并按线程保存对话级选择。 + 显式选择只经运行期专用的上下文键 `model_selection_requested` 与 + `subagent_model_name` 传递,在组装时跳过优雅的默认替换兜底; + `task_tool` 接受子智能体各自的 `model_name` 并在分发时重新授权。([#7249]) + +- **社区工具:** GroundRoute 的 `web_search` 与 `web_fetch` 工具迁移为基于 + `httpx.AsyncClient` 的协程;两者此前都用同步的 `httpx.Client`,因此经 + `ainvoke` 调用的工具会在默认执行器的线程上占住整个网络往返,并行的提供方 + 查询只能相互排队。现在每次调用都通过一次 `_resolve_tool_settings` 读取解析 + API key 与配置的 `max_results`(`web_fetch` 也复用该读取,取代被移除的 + `_get_api_key` 包装),并经 `asyncio.to_thread` 执行,让配置文件读取不占用 + 事件循环。`backend/tests/blocking_io/test_groundroute_search_async.py` + 固定了三条约束:在事件循环线程上发起请求、在循环外读取配置,以及所用的 + 客户端工厂。([#7183]) + +- **部署:** 生产 Docker 命令现在可以用 `DEER_FLOW_COMPOSE_ENV_FILE` 选择根目录 + 下的 dotenv 文件,例如 `DEER_FLOW_COMPOSE_ENV_FILE=... make up`。启动器解析出 + 唯一路径,相对选择器以检出根目录为基准,并把同一个文件提供给 Compose + `--env-file` 插值、`docker/docker-compose.yaml` 中 Gateway 与 provisioner 的 + `env_file` 条目(现为 `${DEER_FLOW_COMPOSE_ENV_FILE:-../.env}`)、密钥解析 + 探测以及 `UV_EXTRAS` 构建额外依赖的发现,从不把它当作 shell 代码执行。显式 + 选择器缺失、是目录或不可读时会在调用 Docker 之前直接失败,而文件删除后 + `make down` 仍能停止整个栈;不设置或为空的选择器保持可选的仓库根 `.env` + 默认值。([#6522]) + - **MCP:** 按未变更的服务器缓存完成包装的工具,仅在绑定对账后重新发现缺失或配置 发生变化的服务器。将成功但未发现工具的结果与发现失败分开保存,并保留代际、共享 重置与 stdio 属主围栏。([#6795]) @@ -741,6 +949,7 @@ 内容:普通 HTTP 足够时直接抓取,需要 JavaScript 的页面则在 Unbrowse 托管的 云端浏览器中渲染(`render: auto|never|always`)。每次抓取仅一次 JSON-RPC POST,不引入新依赖。([#5981]) + - **模型:** 管理员现在可以从“设置 → 模型”管理共享模型, 无需编辑服务器配置。新的仅管理员端点 `GET/PUT /api/managed-models` 与 @@ -837,6 +1046,7 @@ 过期与重试状态,以及一个保留未过期绑定指令的“再次扫描”选项; 凭据保存在后端,且只有微信渠道会重启。 新的微信连接默认走扫码登录,手动输入 token 仍然可用。([#5582]) + - **scheduler:** 把定时任务的运行结果推送到任务所有者已绑定的 IM 身份: 完成钩子把结果写入持久化的 `notification_deliveries` outbox, 由 `NotificationDeliveryWorker` 负责投递。仅当 `channel_connections.enabled` @@ -1103,7 +1313,6 @@ 目标栏里。设置了目标时,最后一轮的编辑铅笔改为灰显并附提示,而不是直接隐藏;消息 工具栏在键盘聚焦时也会显示。因还有运行未结束而被拒绝的 `/goal` 会保留草稿并说明 原因。([#6556]) - ### 变更 - **工具输出:** 被后续操作取代的 `write_file` 载荷省略现在改为显式启用。该优化仍可 @@ -1113,13 +1322,61 @@ - **技能:** 将 `allowed-tools` 作为不强制执行的兼容性元数据。激活技能不再隐藏或 阻止智能体配置与宿主授权已经允许的工具,即使该声明为空也是如此。保留解析、校验、 导出、技能激活权限,以及现有的密钥和用户隔离边界。([#6935]) - ### 修复 +- **项目:** `_convert_and_publish` 中的暂存丢弃改为经 + `await_drained` 排空,读取器被反复取消时,unlink 工作线程在仍持有 + single-flight 属主期间不会再被遗弃;`_remove_staging` 改经 + `cleanup_failed_atomic_write`,尽力而为的清理警告不会再顶替转换的 + 结果——正常结果、原始错误或取消。遗留的暂存文件仍留给保留清扫处理, + 该清扫在既有的年龄护栏之后重试。([#7659]) + +- **事件:** `MemoryRunEventStore` 的记录与调用方相互隔离:写入时深拷贝 + `content` 与 `metadata`,`put`、`put_batch`、`put_if_absent`、 + `list_messages`、`list_events`、`list_messages_by_run` 返回脱离原对象的 + 快照。此前调用方修改写入输入或返回的记录(包括嵌套值)会改动已存储的 + 事件及之后的所有读取;`RunEventStore` 契约现在写明快照保证, + 每个后端都必须同样脱离记录。([#7423]) + +- **基准测试:** 在构造任何提供方之前,先拒绝负数与 NaN 的沙箱空闲超时值,并对 + BoxLite/AIO 健康检查的跳过窗口应用同样的校验。有效的零、正数与正无穷值保持既有 + 行为;OpenSandbox 继续拒绝非零的跳过窗口;AIO 基准输出现在会记录被接受的值, + 且不暗示其提供方会消费这些值。空闲回收场景会在预配沙箱之前拒绝无穷大,因为它 + 必须睡过所配置的超时时间。([#7651]) + +- **运行:** 当 `MemoryRunStore.put` 的替换快照携带不同的 `thread_id` 时, + 把该运行从前一线程的 `_runs_by_thread` 分桶中移除,桶变空时先行修剪, + 再把运行索引到新线程之下。此前该运行仍同时留在旧线程的索引里, + 按线程的查询和线程清理会在两边都看到它。移动只在该替换快照构建完成后发生, + 拷贝失败时前一行及其索引保持原样。([#7622]) + - **TUI:** `/threads` 和 `/model` 选择器现在按原样显示线程标题和模型名称。此前标签 会被当作 Textual 标记解析,标题如 `Fix [/bold] in parser` 会在打开选择器时以 `MarkupError` 使 TUI 退出,标题中的 `[a]` 也会消失。([#7631]) - + +- **Docker:** `docker/docker-compose.yaml` 和 `docker/docker-compose-dev.yaml` + 中的 nginx 镜像从浮动的 `nginx:alpine` 标签固定到 `nginx:1.31.2-alpine`。 + 从 1.31.3 起 alpine 构建附带 musl 1.2.6,其 `pwrite()` 会发出 + `pwritev2(RWF_NOAPPEND)`;较旧 Docker 版本(以及 CentOS 7 等已 EOL 的内核) + 的默认 seccomp 配置未将其列入允许列表,于是 master 以 + `pwrite() "/run/nginx.pid" failed (1: Operation not permitted)` 死亡, + 容器陷入重启循环,`make up` / `make docker-start` 始终无法变为健康。 + 1.31.2 是使用 musl 1.2.5 的最后一个 alpine 构建,其 `pwrite()` 仍用 + `pwrite64`;新增回归测试会拒绝任一 compose 文件中的浮动 nginx 标签。([#7643]) + +- **MCP:** 当 MCP 工具结果在 `_convert_call_tool_result` 中包含内联的 + `AudioContent` 块时,抛出指明 MIME 类型的 `NotImplementedError`。 + 此前此类块落入兜底的文本转换,音频的 Python repr——包括其 base64 + 载荷——会被字符串化进模型可见的工具文本;text、image、resource-link + 与 embedded-resource 块照旧转换。([#7628]) + +- **技能:** 触发评估器的子进程 stdout 现在读到 EOF 为止,而不是在进程退出时 + 停止。`run_single_query` + (`skills/public/skill-creator/scripts/run_eval.py`)逐行解析到达的输出, + 并在 EOF 时补一个换行,使末尾没有换行符的最后一个 JSON 事件仍能解析。 + 此前子进程退出时仍缓冲的输出只被追加进缓冲区而从未解析,已完成运行的 + 收尾 `result` 事件因此被丢弃,该查询被评为未触发。([#7043]) + - **渠道:** 钉钉 AI 卡片不再在 Gateway 事件循环上获取 access token。此前创建卡片和流式更新卡片 都会调用 dingtalk-stream 的同步 `get_access_token()`,它用 `requests` 发请求且没有超时,因此 首张卡片以及之后每次 SDK token 刷新(约每两小时一次)都会在整个请求期间阻塞所有渠道、请求和 @@ -1130,7 +1387,23 @@ 进程启动失败后停止重试。没有可用 Node/npm 工具链的安装环境会在后续抓取中直接使用 现有且保留链接的 Python 回退,不再重复执行失败的子进程探测;单篇文档导致的非零退出 仍会在后续页面重试 Node。([#5973]) - + +- **制品:** `ArtifactFileDetail` 与 `loadArtifactContentFromToolCall` 改用 + 导出的带防护辅助函数 `parseWriteFileArtifact` 解析 `write-file:` URL, + 而不是裸 `new URL` 加 `decodeURIComponent`。面板从 session storage + 恢复选择时只做字符串类型检查,因此过期或手改过的 URL(如 + `write-file:%E0%A4%A`)会在渲染时抛出 `URIError`(`write-file://[` + 则抛 `TypeError`);被拒绝的 URL 现在退化为原始字符串,内容加载返回 + `undefined`。([#7609]) + +- **网页抓取:** 基于 Node 的 Readability 提取只探测一次并记住结论: + `ReadabilityExtractor.extract_article` 在锁下查询 readabilipy 的 + `have_node`,缓存结果,并把 Node 可执行文件缺失(`FileNotFoundError`) + 视为环境级问题,设置 `_readability_js_launch_failed`,让后续提取直接走 + 纯 Python 回退。此前每次抓取都要先付出失败的子进程启动与警告才回落; + 文档特定的非零退出(`CalledProcessError`)仍保持 Node 启用,因为只有 + 当前文档失败。([#7617]) + - **脚本:** `make dev` 和 `make start` 不再把检出目录的 `.env` 当作 shell 脚本执行。此前 `serve.sh` 用 `source` 读取它,`$$` 会变成 shell 的 PID,反引号和 `$(...)` 会执行命令,未加引号 且含空格的值会把第二个词当作命令运行,启动脚本以退出码 127 中止。这些值还会覆盖 Gateway @@ -1141,37 +1414,176 @@ - **渠道:** 展示文件的 IM 回复不再在事件循环上解析这些文件。此前两条回复路径(`runs.wait` 和流式)都会对每个产物内联解析虚拟路径(`realpath`)、解析输出根目录并用 `stat` 检查文件, 在网络存储上会让其他渠道和请求停顿。现在这些工作在工作线程中进行。([#7594]) - + - **渠道:** 飞书流式回复不再在每个分块上重写相同的线程绑定。此前每次向运行中卡片发送消息都会 重新保存该回复的全部别名(每个分块约五次 `set_thread_id`:每次都是在存储锁下完整重写 `store.json`,或一次数据库 upsert),而实际只有 `updated_at` 变化。现在非最终分块对每个 别名每次回复只写一次,写入失败会在下一个分块重试,最终消息仍会写入全部别名。([#7593]) - + +- **技能:** skill-creator 的 `parse_skill_md`(`scripts/utils.py`)改用 + `yaml.safe_load` 解析 SKILL.md frontmatter,与 `quick_validate.py` 和 + 运行时加载器的语义一致,取代手写的逐行解析器——它此前会弄坏转义 + 引号、注释和块标量。无效 YAML、非映射的 frontmatter 或非字符串的 + `name`/`description` 会抛出 `ValueError`;`run_eval.py`、`run_loop.py` + 和 `improve_description.py` 将其转为 stderr 上的 `Error:` 并以退出 + 状态 1 结束。([#7049]) + +- **制品:** 产物 GET 找不到本地文件时,改为从共享 blob 存储提供已查看的 + 图片:收到 404 时,`_get_viewed_image_blob_response` 读取线程检查点的 + `viewed_images` 元数据,按虚拟路径、`MAX_VIEWED_IMAGE_BLOB_BYTES` + (20 MiB)、路径推断出的 MIME 类型,以及绑定到所记录 sha256、大小和 + content type 的 `viewed-image` `BlobRef` 校验该条目,然后对其要提供的 + 字节重新校验摘要。在负载均衡之后,读取方 Gateway 此前没有另一实例 + 查看过的图片的本地副本,请求因此 404;元数据或读取一旦失败, + 仍保持原有 404。([#6329]) + +- **持久化:** `FileAgentStore._stage_temp` 与 + `FileManagedSubagentStore._atomic_write` 现在在写入内容之前就把暂存的 + 临时文件登记进清理。此前路径要等暂存返回后才到达调用方的清理, + `write` 或 `fsync` 失败(例如磁盘写满)会越过暂存步骤抛出,泄漏 + `.tmp` 文件,既无清理尝试也无日志;同样的失败现在经既有的 + `cleanup_failed_atomic_write` 路径丢弃临时文件。([#7504]) + +- **智能体:** 输入净化仍保留用户 XML 的可还原性:`frame_untrusted_text` + 现在先转义原有的 & 符号,再转义被阻断的标签,并前置 + `_USER_INPUT_ENCODING_NOTE`,告诉模型在文件、代码、引文或工具调用 + 参数中复现用户原始文本时,HTML 实体只解码一次(issue #7526);此前 + 单向转义让模型写出的所有内容里都留着 `<system>` 式字节。 + 请求局部的 `_FramedUserText` 标记是文本由本模块加框的唯一凭据—— + 出现在用户文本内部的同名标记属于数据,会被重新编码,而不是跳过该 + 变换——主智能体提示词现在写明:解码只还原数据字节,不让解码出的标签 + 获得指令权威。([#7556]) + - **运行时:** 循环检测警告现在会让 agent 在作答前,先交付已经写入 `/mnt/user-data/outputs` 的文件,被警告的运行不会再因为做好的文件没交付而判为失败。 运行达到 `recursion_limit` 时,现在以 `stop_reason: recursion_capped` 结束,并给出 一条说明,列出已生成但未交付的文件,而不是 LangGraph 的原始报错。进行中的目标会直接 停止续跑(不调用评估模型)并保持活跃;`/wait` 把这个错误命名为 `GraphRecursionError`;IM 渠道也会回复这条说明,而不是笼统的错误提示。([#7588]) - + +- **安全:** 识别 Responses 原生的内容过滤终止并清除其工具调用:新增的 + `OpenAIResponsesContentFilterDetector` 加入 `default_detectors()`, + 把 `status: "incomplete"` 且 `incomplete_details.reason == "content_filter"` + (位于 `response_metadata` 或 `additional_kwargs`)映射为安全终止, + 与令牌耗尽相区分;`SafetyFinishReasonMiddleware` 抑制时现在会把 + `invalid_tool_calls` 与 `tool_calls` 一并计入,并从克隆的消息中清除 + 无效条目。此前在 `function_call` 中途被截断的 Responses 回复 + (例如参数不完整)会表现为中间件忽略的无效工具调用,重放时重新发送 + 该悬空调用,提供方于是拒绝该线程。([#6965]) + +- **tool-output:** `tool_output.elide_superseded_writes` 默认改为 `false`, + `write_file` 内容省略变为显式启用。省略只在同一路径的后续读取或写入 + 取代该写入后才会生效,因此会改写模型请求中较早的一段,使不变历史 + 建立起的 provider 提示词缓存前缀失效(#7319);除非运维显式启用, + 否则上下文节省不再以该代价换取。无论开关如何,已存储的历史、回执与 + 运行日志都保留原始参数。([#7455]) + +- **API:** 百分号解码失败时回退到原始的 CSRF cookie 值,而不是让 + `decodeURIComponent` 在 `fetch` 与 `getCsrfHeaders` 中抛出。cookie 在 + 同一主机上的所有应用之间共享,因此另一个本地应用或代理写入的杂散、 + 被截断的 `%` 转义,或过期残留值,会在网关应答 403 之前就把每个 + 改变状态的请求变成客户端 `URIError`。([#7589]) + - **渠道:** 连接 Slack、Discord、飞书、钉钉、企业微信、Buzz 或 QQ 时,现在会打开 一个对话框,持续显示 `/connect` 命令,并提供复制按钮、过期时间和绑定状态,不再只是 一闪而过的提示。机器人对 `/connect` 和 `/goal` 的回复会跟随用户在网页端选择的语言 (否则使用 `channel_connections.notification_locale`);IM 中的 `/goal` 也会像网页端 目标栏一样,说明目标是否在进行、为什么停止或已经达成。([#7591]) +- **技能:** 临时文件清理失败不再替换已裁定的技能发布结果。已校验的目录描述符 + 提交后,`replace_main` 会删除其 `.host-mutation-*` 临时文件;此前该清理只容忍 + `FileNotFoundError`,任何其他 `OSError` 都会向上传播,掩盖发布结果,或把已裁定 + 的结果变成清理失败。现在 unlink 失败会记录警告,遗留的临时文件之后仍会被 + `assert_temporary_absent` 捕获。([#7586]) + +- **沙箱:** 仅当虚拟根与宿主根位于路径开头时才进行转换,更长路径或 URL 内部的 + 匹配不再发生。此前转换只检查右侧段边界,嵌入 artifact URL 的 `/mnt/user-data` + 会被拼接进线程的宿主目录,挂在 `/data` 的挂载点会把模型可见输出中的 + `/var/data/app.log` 改写为 `/var/mnt/data/app.log`。现在,与扫描器一同从 + `_PATH_START_BLOCKERS` 派生的共享 `PATH_START_BOUNDARY` 后顾断言,会在 bash + 命令改写、`LocalSandbox` 命令/内容解析以及两条反向掩码路径中,拒绝前面紧邻 + 单词字符、`.`、`~` 或 `-` 的根;`/` 仍被允许,因此 + `file:///mnt/user-data` 的映射保持不变。([#7582]) + +- **检查点:** 在重放缓存的检查点历史的待处理写入之前,先校验这段历史。 + `valid_history_entry` 现在要求每条写入都是 `(str, str, ...)` 三元组,并且在 + `CachedHistorySaver` 读取时属于所请求的通道;此前的形状检查止步于列表类型的 + `writes` 字段,于是一条存成三个字符字符串的待处理写入会在物化 delta 时解包, + 无声地虚构出一个通道值。未通过校验的条目在读取时按未命中处理、写入时被跳过, + 权威 saver 因此会重建历史;缓存诊断改经带防护的 `_warn`,日志失败无法再改变 + 回退行为。([#7548]) + +- **Docker:** 在 sidecar 镜像中打包 Lark CLI broker 仅依赖标准库的辅助模块。 + 此前镜像只复制 `lark_broker.py`,但该模块从 `deerflow.utils.atomic_write` 导入 + `cleanup_failed_atomic_write`,未安装 harness 时独立 broker 会在导入时失败; + Dockerfile 现在还会把 `atomic_write.py` 和 `file_revision_metadata.py` 复制到 + /opt/broker/deerflow/utils/。([#7542]) + +- **流式传输:** 用单个 Lua 脚本让 Redis 流的 END 发布变为幂等。 + 属主与恢复重试现在会确认已存在的 END 标记而不是追加第二个;END 成为流尾部后 + 事件无法再追加;对已清理的流执行无属主的恢复 END,是成功的空操作而不是报错。 + 普通事件只检查尾部(带 `COUNT 1` 的 `XREVRANGE`)以保持 O(1);新增 + `stream_bridge.legacy_end_scan_enabled` + (默认 `true`,需重启):尾部不是 END 时会额外扫描整个保留窗口,覆盖旧写入方 + 写入的缓冲(其中 END 可能先于迟到的数据),应只在旧写入方停止且遗留缓冲排空 + 之后才设为 `false`。([#7572]) + - **社区工具:** 异步社区工具(Jina、Crawl4AI、Browserless、Tavily、SearXNG、Firecrawl、 InfoQuest、RAGFlow、LightRAG、Jsonify 和浏览器自动化工具)不再在事件循环上读取 `config.yaml` 和 `extensions_config.json`。每次调用最多会把这两个文件检查三次(各做一次 `stat` 和完整的 sha256),编辑后还会在循环上重新解析 `config.yaml`,期间其他运行、请求和 流式输出都会停顿。现在它们与图片搜索一样,每次调用只在工作线程中读取一次配置。([#7580]) +- **向导:** 安装向导的 Anthropic 默认模型现在是 `claude-sonnet-5-5`,取代已退役、 + Claude API 不再接受的 `claude-sonnet-4-20250514`。针对该模型,向导会省略它拒绝的 + 扩展思考(`budget_tokens`)模板与禁用思考模板。`config.example.yaml` 与配置文档中 + 的 Anthropic 示例也改用同一模型。([#7585]) + +- **国际化:** 防止畸形的 locale cookie 破坏 locale 引导与 `formatTimeAgo`。 + `getLocaleFromCookie` 此前不加防护地解码原始值,畸形的百分号编码会抛出 + `URIError`;并且按每个 `=` 拆分 cookie,截断含 `=` 的值。现在只在第一个 `=` 处 + 拆分,解码失败时返回原始值,再由 `normalizeLocale` 映射到默认 locale。 + `formatTimeAgo` 也改为对 cookie 中的 locale 做归一化,而不是把原始值强转成 + `Locale`,因此畸形但带已知前缀的 cookie 不再跳过归一化。([#7553]) + +- **setup-agent:** 临时文件清理失败不再替换 `setup_agent` SOUL.md 写入的 + 发布结果。`_publish_text` 在 `finally` 中用 `unlink(missing_ok=True)` 删除暂存 + 临时文件,该清理抛出的 `OSError` 会向上传播并掩盖发布结果;现在清理改经 + `cleanup_failed_atomic_write`,它保留原始错误并记录遗留的临时文件。([#7564]) + +- **子智能体:** 调用方取消时仍会排空子智能体的最终清理尾部。沙箱租约释放与扩展 + 任务停止通知此前只是普通 await,被取消的调用方会跳过剩余步骤,租约释放或任务 + 停止的属主关系可能清理到一半就解除;现在尾部经由 `await_drained` 执行,在清理 + 落定之前一直持有容量与执行完成的属主关系,之后才放行取消。同步超时路径也始终与 + 工作线程共享一个结果容器,并在请求取消之前先发布 FAILED 结果。([#7546]) + +- **技能:** 为公开的 `video-generation` 技能限定 Gemini 视频操作轮询的时长。 + `_generate_video_gemini` 此前在无界的 `while True` 循环中轮询长运行操作, + 永不完成的操作会永远轮询下去;新的 `_poll_gemini_operation` 把轮询上限设为 + 240 次、每次 3 秒(共 12 分钟),并把以下情况作为已诊断的失败呈现而不是挂起: + 操作报错、操作完成但没有生成样本(指明 `raiMediaFilteredReasons`)、或样本缺少 + 视频 URI。([#7579]) + - **鉴权:** 公开的认证路由(`/register`、`/setup-status`、`/providers`、 `/oauth/{provider}` 和 `/callback/{provider}`)不再在事件循环上读取 `config.yaml` 和 `extensions_config.json`。每个请求都会检查这两个文件(各做一次 `stat` 和完整的 sha256),编辑后还会在循环上重新解析 `config.yaml`,期间其他请求和流式输出都会 停顿。现在这次读取与 `/login/local` 一样在工作线程中执行。([#7576]) - + +- **技能:** 可选的重复声明在先时,密钥仍保持必填。`parse_required_secrets` + 此前会丢弃同名首个声明之后的所有声明,于是较早的 `optional: true` 条目会遮蔽 + 较后的必填声明,技能无声地在缺少密钥的情况下运行;现在重复声明保留首个位置, + 且必填声明会把条目升级为必填。导出清单应用相同的合并规则,并以失败关闭的方式 + 读取 `optional`(`is True`),与解析器把非布尔值视为必填的处理一致。([#7569]) + +- **测试:** 让后端测试与宿主环境配置及嵌套 Make 控制隔离。并行 make 下的覆盖率 + 测试会剥去外层运行器的 `MAKEFLAGS`、`MAKELEVEL`、`MAKEOVERRIDES`、 + `TEST_JOBS` 与分片变量,使复制出的项目使用自身默认值;mounted-grep 测试重置 + `_get_custom_mounts` 缓存以解析 fixture 挂载;OpenViking 管理器测试改用隔离的 + CLI 配置与回环服务器,并清空环境中的 `OPENVIKING_*` 变量;Postgres 接管围栏 + 测试在导入可选驱动之前即跳过。新增覆盖固定:记忆关闭时,压缩落盘钩子不会 + 捕获任何内容。([#7567]) + - **沙箱:** 停止 Gateway 时,AIO 和 E2B 沙箱不再在进行中的运行收尾之前被销毁。此前 如果第一次使用沙箱时在事件循环线程上构造了提供方,提供方自己的 SIGTERM/SIGINT 处理器会在信号到达时同步销毁所有沙箱:整个销毁期间事件循环冻结,正在执行的工具 @@ -1179,18 +1591,76 @@ 嵌入式客户端和脚本保留信号兜底,SIGHUP 仍会清理。AIO 的 acquire 在关闭开始后会 拒绝请求,与 E2B 一致。([#7560]) +- **记忆:** 把 Mem0 上下文记录的去重移入读取策略边界内。上下文注入此前在 + `list_memories` 返回之后才去重,`id` 不可哈希(列表或对象)的远端记录会在 + 策略之外抛出原始 `TypeError`;新的 `_deduplicate_context_records` 在 + `_read_or_fallback` 内部运行,把不可用的 ID 报告为值已脱敏的 `Mem0APIError`, + 交由 `failure_policy.read` 决定结果:`fail_open` 回退到空上下文, + `fail_closed` 抛出 `MemoryReadError`;即使无效 ID 出现在较后分页,也不会产生 + 部分上下文。首次出现的顺序与字符预算保持不变。([#7561]) + - **前端:** SSO 登录后现在会回到发起登录的页面(例如分享的对话链接),不再总是打开 默认工作区。此前登录相关页面会把所有已登录的请求都跳转到 `/workspace`,导致 `/auth/callback?next=...` 中的 `next` 路径丢失;已登录时访问 `/login?next=...` 也会同样丢失目标页面。前端和 Gateway 对 `next` 的校验现在也会拒绝控制字符:浏览器会 删除 URL 中的制表符、LF 和 CR,因此 `?next=%2F%0A%2F%2Fevil.example` 可能把登录后的跳转 导向其他网站。([#7467]) - + +- **持久化:** 在所有宿主上,技能与智能体文本写入都保留 LF 字节。 + 文本模式会把换行翻译成 `os.linesep`,因此在 Windows 上调用方提供的每个 LF 落盘 + 时都变成 CRLF,而读取方看到的是 LF;技能历史记录的 `prev_content` 描述了任何 + API 视图都无法返回的修订。现在,十处持久化或评判调用方文本的写入器都会传入 + `newline=""`:本地与用户范围的技能写入器、技能历史 JSONL 与技能状态 JSON、 + `FileAgentStore` SOUL.md/config.yaml 暂存、受管子智能体存储、setup_agent 的 + SOUL.md 发布、USER.md 路由,以及 frontmatter 校验器和静态扫描器评判所用的 + 临时副本。([#7501]) + - **鉴权:** 已认证的 Gateway 请求不再在事件循环上读取 `config.yaml` 和 `extensions_config.json`。解析路由权限时,每个请求都会检查这两个文件(各做一次 `stat` 和完整的 sha256),编辑后还会在循环上重新解析 `config.yaml`,期间其他请求和 流式输出都会停顿。现在这次读取与插件鉴权路径一样在工作线程中执行。([#7557]) +- **子智能体:** 向子智能体生命周期观察者报告进行中的取消。 + `asyncio.CancelledError` 此前会绕过异步执行的终态分支,任务停止钩子在任何状态 + 发布之前就运行,观察者看不到中止;现在执行器会在流清理之后发布 `CANCELLED`(对 + 任务生命周期钩子呈现为 `aborted`),并附带收集器的最终用量与工具回执,然后再 + 向上传播。同步超时仍报告 FAILED:`execute()` 与工作线程共享结果容器,并在 + 请求取消之前先发布失败,`execute_async()` 发布 `TIMED_OUT`,且首次终态写入 + 生效,因此迟到的取消无法覆盖超时结果。([#7483]) + +- **客户端:** 转换产物无法删除时,已裁定的上传结果保持不变。`upload_files` + 把可转换的源文件暂存在 `deerflow-upload-source-` 临时目录中,并逐个删除; + 在 Windows 上,此刻仍被打开的产物会让直接 `unlink` 抛出 `PermissionError`, + 把不安全目标的跳过或已成功的上传变成整个批次的失败。现在,逐文件的删除与 + `finally` 目录清理都改经 `cleanup_failed_atomic_write` 与 + `ignore_cleanup_errors=True`,后续文件仍会上传,报告的 `skipped_files` 与 + `files` 列表保持原样。([#7517]) + +- **utils:** 一次性聊天模型的构建移出事件循环。`run_oneshot_llm_detailed` + 此前内联调用 `create_chat_model`,provider 构造(可能读取本地凭据文件) + 因此发生在事件循环上;现在改为先在专用组装池上通过 + `run_assembly` 解析模型,再等待推理,并保留请求上下文。utils 指引记录了 + input-polish、follow-up-suggestion 与 project-file-summary + 调用方的卸载契约,独立的 skill-moderation 与 goal-evaluator 构造器 + 则在其外,作为单独站点;覆盖测试为 + `tests/blocking_io/test_oneshot_llm_offloop.py`。([#7484]) + +- **渠道:** 附件投递失败会在同一会话内告知用户。`Channel._on_outbound` + 此前只记录被跳过或失败的 `send_file` 尝试,不告知用户。现在失败的文件名 + 会收到一条按语言环境生成的回复(由 `attachment_delivery_text` 构建,含 + 成功/失败计数,不含主机路径或提供方错误),作为普通后续消息发送;仍在基础 + `send_file` 实现上的渠道现在会声明不支持附件,并指向 DeerFlow 对话, + 而不是静默丢弃。企业微信在原始 WebSocket 帧上回复,并把通知裁剪到 + `_WECOM_MAX_CONTENT_BYTES`,回复上下文已过期时跳过该反馈, + 而不是回退到主动 API。([#7532]) + +- **技能:** GitHub issue 研究现在会排除 pull request。`github-deep-research` + 技能的 `get_issues` 此前返回 GitHub issues 端点的原始响应,其中包含 + pull request,因此 PR 会出现在 issue 研究结果中;现在会过滤掉带有 + `pull_request` 键的条目,客户端会继续翻页直到填满请求的条数,单页请求 + 上限为 100,非正数的 limit 返回空结果。SKILL.md 现在声明 `issues` 只返回 + 仓库 issue,并将 pull request 研究指向 `prs`。([#7516]) + - **记忆:** 自动上下文压缩在把即将被总结的消息保存到记忆时,不再阻塞事件循环。 使用 mem0、Honcho 或 OpenViking 后端时,这次保存是同步 HTTP 请求,Gateway 在记忆 服务器响应之前无法处理其他请求和流式输出。现在它与每轮结束后的记忆写入一样在工作 @@ -1201,38 +1671,558 @@ `session.config` 中的 `recursion_limit` 仍然优先;早先的 README 示例把它设为 `100`, 如需跟随顶层设置,请从 `config.yaml` 删除这一行。GitHub 渠道的运行不变。([#7521]) +- **网关:** 生产者租约启动失败时仍接纳跨进程运行。`start_run` + 此前把 `start_producer_lease` 在跨进程桥接上返回 False 视为致命错误并抛出, + 只要后端(Redis)出现短暂抖动,一次本应健康的运行就会拿到 500,尽管桥接 + 会回滚不完整的租约注册、不留下任何被追踪的状态。现在准入会记录一条警告并继续: + 未被追踪的生产者回退到历史上保守的恢复路径——等待真实的 END 或保留流的 + TTL,而不是合成的 END;worker 则独立重试自己的租约。([#7136]) + +- **项目:** 清空回收站时删除只读文档。在 Windows 上只读属性会让 + `Path.unlink` 以 `PermissionError`(`WinError 5`)失败,而 POSIX + 只要求父目录可写,因此一次恢复的归档或一次保留权限模式的同步 + 会让已彻底删除的文档无法清除:彻底删除重新抛出异常,对应的行删除回滚, + 回收站中的文档在每次重试时都报同样的错误。彻底删除的 + `_unlink_document_file` 现在会清除该属性(以 `S_IWRITE` 调用 `chmod`, + 跳过符号链接)并重试一次 unlink,与 `config/paths.py::_clear_read_only_tree` + 一致;`FileNotFoundError` 仍视为已删除,其他任何错误照常传播, + 因此该行保持可重试。([#7499]) + +- **前端:** 表格单元格与记忆事实的预览按码点计数。`artifact-table-preview` + 以 120 个 UTF-16 码元(`value.slice(0, 120)`)截断单元格,并用 `value.length` + 决定是否显示展开控件;`memory-settings-page` 上的 `truncateFactPreview` 以 + 140 个码元测量并截断事实预览,因此跨两个码元的 emoji 可能在截断处被切开并 + 渲染为 U+FFFD,而一个已符合 120 码点预算的单元格仍会显示展开控件。两处预览现在 + 共用 `core/utils/text` 中的 `truncateToCodePoints`(与 + `core/scheduled-tasks/format.ts` 的 `briefReason` 一致),按码点测量和截断, + 展开判定改用 `Array.from(value)` 计数。([#7503]) + - **沙箱:** AIO 提供方的异步 acquire 不再在事件循环上检查线程的技能目录。此前每次 线程 acquire 选择沙箱 ID 时,都会在事件循环上直接执行文件系统 `stat` 和路径解析; `DEER_FLOW_HOME` 位于较慢或网络存储上时会阻塞事件循环。现在这一步与 acquire 的 其他步骤一样在工作线程中执行。([#7507]) +- **运行时:** 确认恢复接管后回填终态事件。接管此前会在 Run 行上提交 + `status=error`,却不写入匹配的 `run.terminal` 证据。新增的 + `RecoveryTerminalPublisher` 把已提交的接管认领转换为一次性的 + `RecoveryTerminalTarget`,发布一条标记 `recovered` 的错误终态事件,并尽力 + 回填 `RunManager` 过去会写入的零投递 `run.delivery` 回执。发布针对 Run 删除与 + 身份漂移设有围栏(`RunEventLifecycleRejected` 映射为 `CONFLICT`);DB 存储在 + 活动事件会话中评估该护栏以避免二次池签出,memory 存储用按线程的锁为受保护的 + 写入与删除加围栏,发布失败只记录日志、已提交的 RunRow 保持不变;定时任务运行的 + 对账会发布相同的目标。([#7500]) + +- **项目:** 让 `read_file_chunks` 在文件 I/O worker 交接处可安全取消。 + 取消项目文档读取此前存在三个不安全窗口:取消落在 open worker 完成与结果到达 + 协程之间会泄漏句柄;在已准入的读取仍在运行时清理关闭就可能开始;重复取消 + 会让调用方在 close worker 落定之前逃逸。现在已准入的 open/read/close 操作都用 + `await_drained` 门控,取消只在操作落定之后才被投递; + worker 一返回打开的句柄就把它保存在一个列表单元中(清理关闭始终持有句柄,open + 失败则原样抛出 `FileNotFoundError`);通过 `yield` 等待下一个分片 + 仍可立即取消。([#7496]) + +- **持久化:** PostgreSQL 自动建库时把对端已建库视为成功。 + 两个网关同时启动同一个缺失的数据库时,都会在 `_auto_create_postgres_db` 中 + 执行 `CREATE DATABASE`,只有一个能赢;输掉的一方会以 `duplicate_database` + (`42P04`)中止启动,尽管赢方的 catalog 行已提交、目标库已可用。该辅助函数现在 + 通过共享的 `_has_sqlstate` 遍历器检查 SQLSTATE(沿 `orig`、`__cause__`、 + `__context__` 追溯,与旁边的缺库 `3D000` 检查一致),并记录数据库由对端进程 + 创建,然后照常进入其重试;其余所有失败(包括权限不足)仍原样传播。([#7489]) + +- **调度器:** 手动试运行的启动来源在重试间保持稳定。 + 手动试运行的合成排队行此前缺少 `scheduled_for`,且 `_launch_provenance` + 对手动触发使用当前时钟,因此稍后的队列排空重试会推导出不同的来源。该来源是 + `start_run` 每个 occurrence 幂等输入的一部分:重试要么与首次尝试冲突, + 要么提交了不同的请求。合成行现在携带已准入的 `scheduled_for`, + `_launch_provenance` 对每种触发都读取 occurrence 时间,仅在 legacy 调用方 + 未提供时回退到 `now`。([#7492]) + +- **网关:** 临时文件清理不再顶替调用方的结果。 + artifact 路由的 `_replace_artifact_atomically` 在 `finally` 中关闭暂存 + 描述符并 unlink 临时文件,因此一个被锁定或已被移除的临时文件会把一次已发布 + artifact 的编辑变成清理错误;Lark CLI 的 `_write_json_file_atomic` 用裸 + `unlink` 也有同样问题。两者现在都改用 `cleanup_failed_atomic_write`:已发布 + 场景保持静默,遗留临时文件无法删除时发出警告,且绝不让清理失败 + 顶替写入的结果。([#7505]) + +- **前端:** 认证密码表单允许非 ASCII 的本地部分。 + 登录、注册、管理员设置和修改密码表单此前依赖浏览器的 `type="email"` 约束校验, + 它会在提交处理器运行之前就拒绝任何非 ASCII 本地部分,因此通过 API、管理员或 + OIDC 创建的账号(后端身份 schema 有意接受这类地址)永远无法通过密码表单登录。 + 这些表单现在是 `noValidate`,并在处理器中运行 `core/auth/email` 中宽松的 + `isValidEmail` 辅助函数(恰好一个 `@`、本地部分与域名非空、无空白,严格弱于 + 后端接受的格式),提交去除首尾空白的地址,并恢复移除原生校验后会丢失的 JS 密码 + 最小长度(登录为 6,注册为 8)。([#7498]) + +- **上传:** 编辑过的 markdown 伴随文件现在会失效。 + 伴随文件的属主记录此前只包含 Markdown 的 device、inode、size 与 ctime,因此在 + Windows 上一次大小不变的就地编辑(那里 ctime 是出生时间)仍会通过校验,并把 + 修改后的文件当作转换后的伴随文件提供。`markdown_identity` 现在还会携带 + `st_mtime_ns`,编辑过的伴随文件将无法通过 `resolve_companion` 校验;legacy + 四字段记录按失败关闭处理并带有自己的 debug 诊断,而不是被回填,重新上传或重新 + 转换会登记新的关联。这些元数据检查能发现普通编辑,对能恢复时间戳的写入方则不 + 保证内容完整性。([#7348]) + +- **检查点:** 将缓存的历史负载与状态读取方隔离。 + memory checkpoint 缓存的 `_copy_entry` 此前在缓存与读取方之间共享快照种子 + (只复制 writes 列表),因此读取方修改嵌套消息负载会污染之后用于物化检查点的 + 种子。现在无论写入还是读取,条目都会被深拷贝,且复制在注册表锁之外进行,因为 + 用户负载可能实现会重入缓存的 `__deepcopy__`;`set_many` 在复制前先检查 + `enabled`。整条目深拷贝在调用方线程上同步执行(包括事件循环上的 + `aget_many`),开销已注明随负载大小增长,且在 N 个检查点的热扫描中为 + O(N²)。([#7259]) + +- **前端:** 文档搜索现在在桌面端与移动端都可用。 + 构建此前从未生成 Nextra 的 `<Search>` 所加载的 Pagefind 索引,自定义文档头部 + 也不渲染搜索框:`pnpm build` 现在会运行 `search:index`(固定使用 pagefind + 1.5.2)写入 `public/_pagefind/`,落地页头部及其移动端抽屉接受一个可选的 + `search` 插槽,只有文档布局会填充它。每个页面的 `<html lang>` 会在建索引前 + 根据其 `/{lang}/` 路由重写(根布局固定单一 lang,因此中文页面被当作英文分词, + 多字符词永远无法匹配),`DocsHtmlLang` 同步实际渲染的 `<html lang>`,让查询 + 命中正确的索引;结果面板用应用 token 重新设置样式并保持在移动端抽屉之上,抽屉 + 现在在结果导航之后而非之前关闭,不再在 Back 时重新打开,也不再与第二个屏幕外 + 的 `<Search>` 竞争 Cmd/Ctrl+K。([#6715]) + +- **沙箱:** `write_file` 现在会拒绝本身就是工具输出省略占位符的载荷,并返回 + 错误,提示模型用 `read_file` 读取目标路径并写入真实内容;不会写入任何内容, + 同步与异步工具都会预先执行该检查。该占位符正是预算中间件在发送给模型的 + 请求中用来替换被后续操作取代的写载荷的内容,因此模型可能把它从自己的历史 + 里复制出来并作为内容提交,用一个约 200 字节的占位符悄悄替换掉真实交付物 + (#7319)。匹配针对整个载荷进行(允许首尾空白),因此仅引用该措辞的载荷仍可 + 写入;模板移至 `deerflow.constants` 的 `WRITE_ELISION_PLACEHOLDER_TEMPLATE`, + 由中间件与该护栏共享,二者不会漂移。([#7374]) + +- **技能:** `ppt-generation` 生成的演示文稿现在会遵循照片方向:`generate.py` + 在为每张幻灯片图片定尺寸并嵌入之前先对其运行 `ImageOps.exif_transpose`, + 因此携带 EXIF 方向元数据的照片不再横躺或镜像地出现在文稿中(PIL 解码时会 + 忽略 EXIF 方向)。透明图片仍像以前一样保留 alpha 通道。([#7193]) + +- **配置:** 配置存储中的原子写入清理不再掩盖调用者的结果: + `ManagedModelStore` 的密钥持久化与 `secret_file.py` 的 + `_create_exclusive`/`_publish_under_claim` 现在通过 + `cleanup_failed_atomic_write` 丢弃临时文件和未发布的认领,而不是 + `Path.unlink(missing_ok=True)`。此前 `finally` 块中的一次 unlink 失败会盖过 + 真实结果——清理异常会替换掉中止写入的那个错误,或替换掉一次成功的发布。 + 现在所有退出路径上的清理失败都会被记录并抑制。([#7279]) + +- **持久化:** 基于文件的智能体与托管子智能体存储中的暂存写入清理现在是 + 尽力而为:`FileAgentStore` 和 `FileManagedSubagentStore` 通过 + `cleanup_failed_atomic_write` 丢弃暂存临时文件,而不是 + `Path.unlink(missing_ok=True)`,因此 `finally` 清理期间的 unlink 失败 + 不再盖过中止发布的那次错误——也不会盖过一次成功的发布——而是被记录并 + 抑制。([#7485]) + +- **中间件:** 恢复的工具输出保持普通的文件创建权限:当预算中间件从 blob + 存储把外置输出恢复到丢失本地副本的网关时,`_write_restored_output` 现在用 + 排他的 `open(..., "xb")` 创建临时伴生文件,而不是 `tempfile.mkstemp`,因此 + 恢复出的文件遵循与首次发布相同的 `0o666 & ~umask` 策略(外加文件系统默认 + ACL),而不是落成固定 `0600`、令不同 UID 的挂载沙箱读取者无法读取。 + 摘要匹配的本地文件仍会被复用,不改动其权限。([#7390]) + +- **模型:** `PatchedChatDeepSeek` 现在会保留 Moonshot 的缓存创建 token 计数: + `_create_chat_result` 以及流式 `messages-tuple` 路径上的 + `_convert_chunk_to_generation_chunk` 会在最终 usage 块上把原始 SDK 的 + `prompt_tokens_details.cache_write_tokens` 还原为 + `input_token_details["cache_creation"]`。锁定的 `langchain-openai` 1.2.1 + 转换会读取 `cached_tokens`(映射为 `cache_read`)但忽略 + `cache_write_tokens`,因此 Kimi K3.1 等提供方的这一计数在进入用量日志之前 + 就已丢失 (#7426)。([#7428]) + +- **前端:** 对话步骤现在会呈现工具的执行结果。结果消息报告出错的工具调用—— + 通过 `status === "error"` 或 `deerflow_tool_meta.status === "error"` 判定, + 由新的 `core/messages/tool-error.ts` 辅助函数分类——会在其步骤标签上显示 + destructive 样式的“执行失败”徽标,而不是把失败藏进折叠的详情面板;Debug + 详情的载荷标签也使用同一分类器。定时任务步骤会显示等待中标签(“定时任务 + 操作”/“暂停此定时任务”)直到收到有效的对象响应,而不是在发出请求时就宣告 + 完成;缺失、非对象或携带 `error`/`code` 的响应会回退到失败标签。([#7465]) + +- **utils:** `output_name_candidates()` 中的五个带时间戳的回退名现在总是 + 互不相同:到达回退列表时对 UTC 时钟采样一次,之后每次尝试都把它偏移 + 一微秒,而不是每次 yield 都调用 `datetime.now(UTC)`。在时钟精度低于微秒 + 时间戳的主机上(如 Windows 约 15 ms 的系统时间刻度),五个候选此前会重复 + 同一个名字,捕获工具因此丢失重试并以 + `could not reserve an output filename` 失败。([#7071]) + +- **记忆:** DeerMem 更新器中,模型记忆更新响应里的超范围数字不再让整批 + 归一化失败。非有限的 `extend_by_days`(`1e400` 会解析为 `inf`;`NaN` 也会 + 出现)此前会在 `int()` 中抛出,并丢弃响应里的每一个有效事实——此类条目 + 现在会被单独丢弃,而超出 float 范围的无界整数则以 int 形式透传,交由 + 应用路径做边界约束。合并巩固的 `confidence` 同样如此:`float()` 溢出或值 + 非有限时默认为 `0.9` 而不是抛出,且这一默认行为会记录日志,系统性错标 + 尺度的模型依然可见。([#7442]) + +- **调度器:** `briefReason()` 在为结果通知缩短定时运行摘要时,现在按码点 + 计数,而不是按 UTF-16 码元测量和切片。落在 emoji 代理对内部的 120 字符 + 截断会留下渲染为 U+FFFD 的孤立高位代理,而增补平面字符会在预算中被双倍 + 计数,导致本就放得下的摘要被缩短。([#7452]) + +- **技能:** 数据分析技能接受纯文件名的导出目标:`analyze.py` 的 + `_export_results` 现在把没有父目录的输出路径按当前工作目录解析 + (`os.makedirs(... or ".")`),而不是在 `dirname("")` 上抛出,因此 + `report.csv` 这类目标与嵌套相对路径(父目录会自动创建)和绝对路径一样 + 可用;目标处已有的文件会被无警告覆盖。适用于 CSV、JSON 和 Markdown + 导出。([#7132]) + +- **技能:** 用户作用域的技能存储现在遵循 `skills.container_path`: + `get_or_new_user_skill_storage` 会以解析后的应用配置为 `container_path` + 取初值(调用者显式覆盖仍然优先),`user_should_see_legacy_skills` 的 + kwargs 分支也以同样方式取初值。此前只有公共存储遵循配置的根目录, + `UserScopedSkillStorage` 保留默认的 `/mnt/skills`,因此运维者一旦移动挂载 + 根,容器注册表和技能激活就会在一个已不存在的挂载根下构建路径。([#6770]) + +- **脚本:** 在 stdio 编码无法表示其 ✓/✗ 字形与破折号的主机上(Windows 代码 + 页、POSIX C locale、`PYTHONIOENCODING=ascii`),`sync_labels.py` 不再以 + `UnicodeEncodeError` 崩溃——该崩溃会在 `gh label create --force` 已经执行 + 之后发生,留下只应用了一半的同步。`main()` 会在输出被重定向时把 + stdout/stderr 重新配置为带 `backslashreplace` 的 UTF-8(控制台保持其 + 编码),`gh` 子进程输出则按 UTF-8 解码并使用 `errors="replace"`,与 + `scripts/support_bundle.py` 一致,失败消息不再被宿主 locale 破坏。([#7473]) + +- **沙箱:** `LocalSandbox.write_file` 不再改写它存储的每个文件内部的容器 + 路径——翻译现在只适用于可执行内容,由 `_content_path_translation_applies` + 依据共享的 `is_code_file`/`is_code_path` 规则,外加沙箱自己的解释器后缀 + (`.awk`、`.sed`、`.lua`、`.r`、`.tcl`,宿主解释器会按路径打开它们)或 + 头部带 shebang 的无后缀目标来判定。文档、数据文件和配置按原样存储;此前 + 口述写入的 `README.md` 最终会含有宿主路径,而 `read_file` 又把改写掩回 + 虚拟路径,导致智能体确认了文件并不包含的内容 (#7461)。无后缀 shebang 嗅探 + 以 `O_NONBLOCK` 打开并做 `fstat` 常规文件检查,因此向 FIFO 追加不会阻塞, + 带后缀路径的分类不触碰目标文件。([#7462]) + +- **前端:** 后台“Conversation finished”通知的正文截断上限改为 200 个码点, + 而不是 UTF-16 码元。两个聊天页面此前用 `String#length` 和 `substring` 截断 + 最后一条消息的文本,落在增补平面字符代理对内部的截断会留下孤立代理,通知 + 将其渲染为 U+FFFD,而本就符合 200 码点的正文会被提前缩短。抽出的 + `frontend/src/core/notification/body.ts` 中的 `notificationBody()` 以 + `Array.from` 限定文本长度,预算内的正文保持完整,并在最后一条消息没有文本 + 时返回“Conversation finished”回退。([#7474]) + +- **前端:** 修复工件面板中长工件列表的滚动:把 `chat-box.tsx` 中包裹 + `ArtifactFileList` 的裸 `<main>` 容器换成共享的 `ScrollArea`。面板此前没有 + 滚动容器,超出可见高度的文件在桌面可调面板和移动端 Sheet 抽屉里都点不到; + 现在列表中的每个文件(包括最后一个)都可访问、可点击。新增一条 e2e 用例, + 在点击最后一个文件前先验证短列表与长列表两种场景。([#7284]) + +- **运行时:** 当所选检查点或当前 head 仍存在待处理的执行时,拒绝从更旧检查点 + 发起的 delta 模式恢复,不再将该分叉线性化并静默丢弃这些工作。 + `_linearize_delta_checkpoint_resume` 在重写状态前会检查两个检查点的 + `__pregel_tasks` 通道值;对于 `Command(resume=...)` 输入,还会拒绝合成 + `__start__` 条目之外的待处理图节点,在任何状态写入之前抛出新的 + `CheckpointResumeRejected` 错误。由于什么都没有写入,工作线程随后会跳过 + 检查点回滚与编辑重放补偿。 + 全模式恢复以及无待处理执行的重放保持原有行为。([#7441]) + +- **沙箱:** 沙箱预热基准中的投机性预热获取改按事件状态分类,而非单调时钟比较。 + 现在每次记录的获取操作都会在开始时从 `trace.tool_requested.is_set()` 捕获 + `precedes_tool_request` 标志,投机过滤器使用该标志,而不再比较 + `started < tool_requested` 时间戳。在时钟粒度较粗的平台(Windows)上,一次 + 真正的预热获取可能与工具请求落在同一时钟刻度内,严格比较会把它误判为 + 由工具触发;新增一个量化的单调时钟测试,固定这种并列情况的分类结果。([#7045]) + - **线程:** `POST /api/threads` 现在会以 `422` 拒绝无法对应任何智能体的 `assistant_id`。此前该值会被直接保存,之后读取该线程(`GET` 线程、状态、历史) 都会返回 `500 Internal Server Error`,直到删除线程。修复前已这样保存的线程 改为按主智能体的状态结构读取;从这类线程创建的分支不保存 `assistant_id`,因此使用 主智能体。([#7445]) +- **沙箱:** `LocalSandbox` 的 POSIX 与 Windows 命令运行器在 spawn 后失败时改为 + 由单一属主清理管道 fd。异常路径现在只关闭 stdout/stderr 管道的读端,写端仅由 + `finally` 块关闭;此前 `Popen` 成功后发生失败会让两处代码关闭同一批描述符, + 第二次关闭可能落在其他线程已认领的描述符编号上。启动失败时还会 kill 并用 + 有界的 `wait` 收割生成的子进程,同时抑制清理错误,从而保留原始的启动错误, + 而不是让子进程继续运行。([#7430]) + +- **沙箱:** 在配置加载时归一化 `container_path` 值中的尾部斜杠,同时覆盖沙箱 + 卷挂载与 `skills.container_path`。`/mnt/reports/` 这类带尾部斜杠的写法此前 + 会被 `VolumeMountConfig` 接受,只在 `LocalSandboxProvider` 构建路径映射时才被 + 归一化,而沙箱工具的路径门禁比较的是原始配置值,因此对挂载的每一次 + ls/read/write/glob/grep/bash 访问都会以令人困惑的 `PermissionError` 失败; + 共享的 `normalize_container_path()` 校验器让两层使用同一种写法。归一化后 + 为空的值——容器根 `/` 或空字符串——会在加载时被拒绝;此前空的 + `container_path` 会让门禁匹配所有绝对路径,把单个挂载的 `read_only` + 标志套用到所有路径上。([#7393]) + +- **渠道:** Windows 上运行时渠道配置 sidecar 文件锁改为重试等待,而不是约十秒后 + 直接失败。该锁此前使用阻塞的 `msvcrt.LK_LOCK`,约十秒后就会放弃,把跨 worker + 争用表现为 `PermissionError`,而没有串行化它本应串行化的读-改-写周期; + Windows 回退路径现在用 `LK_NBLCK` 获取锁,并通过 `retry_until_locked` 只对 + 争用错误每 0.2 秒重试一次,像 POSIX `flock(LOCK_EX)` 一样无限等待,并在争用 + 重试满 50 次后记录一条警告。`fcntl`/`msvcrt` 改为在模块级绑定, + 使回退分支可测试;新增的锁测试覆盖重试节奏、POSIX `flock` 行为以及真实的 + 跨线程争用。([#6888]) + +- **配置:** 在 Windows 上删除包含只读文件的线程目录。 + `Paths.delete_thread_dir()` 此前直接调用 `shutil.rmtree()`,而 Windows 不允许 + unlink 只读条目(WinError 5),POSIX 只需要可写的父目录即可,因此 + `user-data/` 下只要有一个只读文件,整个线程就无法删除: + `DELETE /api/threads/{id}` 返回 + 500 "Failed to delete local thread data",之后每次重试也都在同一文件上失败。 + 遇到 `PermissionError` 时,调用方现在会先清除整棵树中的只读属性(跳过 + 符号链接),再重跑一次 `rmtree`;其他错误,以及因为另一个进程打开着文件而 + 始终无法删除的目录树,仍然会抛出异常,而不是被静默跳过。([#7446]) + +- **技能:** 数据分析 JSON 导出在打开目标文件之前拒绝非有限值。CSV/Excel CLI 的 + JSON 导出此前用 `json.dump` 直接序列化到输出,因此 `NaN`、`Infinity` 或 + `-Infinity` 值——包括嵌套在列表或结构体中的——会被写成标准 JSON 解析器 + 拒绝的裸 token,且部分写出的文件会覆盖之前的结果。`_export_results` 现在先用 + `allow_nan=False` 序列化,成功后才写入,CLI 因此会报告错误并保留既有输出; + 有限数字、null 以及 `'NaN'` 这类文本仍然有效。([#7456]) + +- **控制台:** 在控制台用量细分中计入旧版运行的输入 token。早于每模型 + `token_usage_by_model` 映射的行会回退到该运行的 `model_name`,但此回退此前 + 只累加总 token 数,因此旧版运行记录的 `total_input_tokens` 从未进入 + `ConsoleUsageModelBreakdown.input_tokens`,各模型的输入总量被低估。回退现在 + 会把 `total_input_tokens` 与同一模型的较新用量桶一起累加, + 且不推断缓存命中。([#7411]) + +- **子智能体:** 从截断前完整记录的工具输出中判定 `tests_passed` 证据,而不是从 + 有界的输出尾部判定。bash 执行收集现在会用 `summarize_test_output()` 对整个 + ToolMessage 计算 passed/failed/unverified 的 `test_summary` 戳记,并按工具 + 调用缓存、跨流式块复用,因此滚出 1000 字符尾部之外的摘要——例如 `N passed` + 行靠前的超长 pytest 日志,或结尾的 TODO 堆栈和警告——不再抹掉测试证据; + 检查器消费该戳记,对旧条目仍保留只看尾部的旧检查。完整且一致的 Node 计数器 + 优先于 `12 passed` 这类偶然的日志文本,而形态独立的各运行器摘要(pytest、 + Maven、Gradle、Go、unittest)仍会参与贡献。([#7407]) + +- **Provisioner:** PVC 模式下每个沙箱 Pod 只声明一次 userdata PVC,并在各挂载 + 之间复用。设置 `USERDATA_PVC_NAME` 后,Pod spec 此前会把同一个 claim 先以 + `user-data` 发布一次,又为每个额外挂载和 Lark broker 凭据卷重复发布,而 + 不支持为一个 claim 提供多份卷声明的 NFS CSI 驱动会拒绝这种做法。 + Provisioner 现在由 `_build_volumes` 发出唯一的 `user-data` 卷,让额外挂载与 + broker 挂载以各自原有的 `subPath` 和逐挂载的 `read_only` 标志指向它,凭据 + 挂载仍仅限 broker;hostPath 模式保留各自独立的卷,且 `SKILLS_PVC_NAME` 必须 + 是与 `USERDATA_PVC_NAME` 不同的 claim,因为不受限线程仍会声明单独的 + `skills` 卷。([#7274]) + - **前端:** 后端接受、但前端更严格的格式检查不接受的邮箱账号(如 `a&b@example.com`、`a@b.c` 或含 Unicode 的地址)现在可以正常进入工作区。此前这些 账号的会话会显示网关离线界面,且只要存在一个这样的账号,所有管理员的 “设置 → 用户”列表都会加载失败。([#7449]) +- **图表可视化:** 在图表 CLI 中映射 `generate_spreadsheet`,并在任一生成失败时 + 以非零状态退出。`SKILL.md` 已经会选用 `generate_spreadsheet`,参考文档也有 + 记载,但 `generate.js` 的 `CHART_TYPE_MAP` 没有对应条目,批处理会对其报 + "Unknown tool";现在它映射到 AntV 的 `spreadsheet` 类型。CLI 还会逐项跟踪 + 失败——缺少 `tool` 字段、未知工具或图表服务请求失败——并在打印成功项的 + URL 之后以状态码 1 退出;此前这些错误只会记录到 stderr, + 进程以 0 退出。([#7395]) + +- **脚本:** 技能评审分析器子进程在管道两端都固定使用 UTF-8。分析器打印 + `ensure_ascii=False` 的 JSON,但父进程此前用环境 locale 编码解码其输出, + 子进程也继承环境的 stdio 编码,因此非 ASCII 的技能事实在 cp1252 的 Windows + 上会让评审流程崩溃(分析器内部编码失败),在 cp936 上则产生乱码。 + `review_env()` 现在为子进程设置 `PYTHONIOENCODING=utf-8` 与 `PYTHONUTF8=1`, + 父进程的 `subprocess.run` 则用 `encoding="utf-8"` 解码。([#7432]) + +- **钉钉:** 在缓存 access token 之前拒绝不可用的有效期。`expireIn` 为零、 + 负值,或处于 300 秒刷新余量(`_TOKEN_REFRESH_MARGIN_SECONDS`)之内的任何值 + 此前都会被原样接受,导致缓存 token 的过期时间落在当前时间或之前,每次调用 + 都重新获取 token;非数值、无穷大或超大的值(`inf`、`10**400`)会在已处理的 + `TypeError`/`ValueError` 之外抛出 `OverflowError`,使 `_get_access_token` + 崩溃。这些情况现在都会回退到默认的 7200 秒有效期,token 在多次调用间只缓存 + 一次;高于余量的有效期(如 301 秒)仍保留其计算出的过期时间。([#7450]) + +- **网页抓取:** Readability.js 静默退化时保留 `<a>` 链接。在 Windows 上, + `simple_json_from_html_string` 子进程可能成功返回却给出未抽取的文档而非 + 正文,而 pure-Python 回退会丢弃所有 `<a>` 目标,导致 `web_fetch` 产出没有 + 链接的 Markdown。`ReadabilityExtractor.extract_article` 现在把抽取结果中 + 出现的 `<body>` 或 `<title>` 标记——成功的抽取总会剥离它们——视为原始 + 文档信号,并替换为已解析 HTML 的 `<body>` 内部内容,锚点因此以 + `[text](href)` 保留。输入净化的权威标签清点会豁免新的正文抽取正则 + 所匹配的普通 HTML `body` 标签。([#7412]) + +- **知识:** 知识管理扩展现在接受大小写不敏感的上传媒体类型。 + examples/deerflow-extension-knowledge-management 中的 `POST .../files` + 处理器现在会先去除空白并把 `Content-Type` 头的媒体类型转为小写,再与 + `application/octet-stream` 比较;此前发送 + `Application/Octet-Stream; charset=binary` 或 `APPLICATION/OCTET-STREAM` + 的客户端即使请求体是合法的 octet 流也会得到 `415`。([#7397]) + +- **视频:** 视频生成技能保留参考图片的 MIME 类型,拒绝未识别的参考图片扩展名。 + `skills/public/video-generation/scripts/generate.py` 中的 Gemini 参考载荷 + 此前为每个参考硬编码 `image/jpeg`,`_guess_mime` 对未映射的后缀会静默回退到 + `image/jpeg`。现在两个提供方都从文件名扩展名推导 MIME(`image/png`、 + `image/jpeg`、`image/webp`、`image/gif`;不区分大小写), + 缺少或不受支持的扩展名会在任何 API 提交之前抛出 `ValueError`,报出文件名 + 与受支持的集合。图片字节既不校验也不转换,提供方侧的格式限制仍然适用; + 技能文档与 README 说明了这些限制。([#7340]) + +- **配置:** 在多次 `get_app_config` 调用之间固定显式指定的应用配置路径。 + `reload_app_config(config_path)` 现在把文件的绝对路径记入 `_app_config_source` + 固定项(与 `_extensions_config_source` 一致),`get_app_config()` 按该固定项 + 解析,因此内嵌的 `DeerFlowClient(config_path=...)` 在工作目录变化后仍跟随 + 显式指定的文件,而不是重新探测环境解析——此前环境解析可能把缓存切换到 + 同级的默认配置,或在不存在环境配置时报错。固定项只在加载成功后才记录, + 无参数的 `reload_app_config()`、`reset_app_config()` 或 `set_app_config()` + 会将其清除;所有单例读取、重载、重置与注入都在新的 `_app_config_lock` 下串行 + 执行,并发调用无法在另一个文件的固定项之下发布配置。([#7092]) + +- **上传:** 上传伴随记录的删除改为尽力而为。`unregister_companion` 与 + `invalidate_overwritten_upload` 的陈旧记录分支此前会放行删除属主记录时抛出的 + `OSError`——Windows 句柄仍被占用或挂载为只读时,一个结果本已确定的上传 + 会因此变成失败请求。两条路径现在都通过 `_warn_record_removal_failed` + 辅助函数记录孤儿记录,该辅助函数自身的日志失败也会被抑制;遗留记录仍保持 + 安全,因为 `resolve_companion` 会校验记录下来的来源身份,而不是解析到错误的 + 伴随对象。([#7409]) + +- **记忆:** `scripts/load_memory_sample.py` 中记忆样本导入的备份每次运行唯一。 + 备份此前仅以秒级时间戳命名,同一秒内启动的两次导入会静默覆盖较早的备份—— + 而那是坏导入与记忆丢失之间仅存的一道屏障。单目标备份现在在复制前用 + `tempfile.mkstemp` 预留唯一的备份名(`{target.name}.bak-{timestamp}-...`), + 复制失败则移除该预留;`--all-users` 运行在替换任何用户记忆之前,先用 + `tempfile.mkdtemp` 在 `.deer-flow/memory-sample-backups/` 下创建备份根目录。 + 文档说明备份会一直累积直至手动删除,且 POSIX 备份目录仅限创建它的操作系统用户 + 访问。([#7418]) + - **持久化:** 在已存在的数据库中 PostgreSQL 启动初始化失败时,现在会报告原始错误。 此前 Gateway 会把任何包含 "does not exist" 的错误(例如迁移时缺少列或表)当作数据库 不存在并执行 `CREATE DATABASE`,启动随之以 `database ... already exists` 或 `permission denied to create database` 失败,原始错误也不会出现在 traceback 中。 现在只有数据库确实不存在(SQLSTATE `3D000`)时才会自动创建。([#7410]) +- **上传:** 同名冲突时绝不截断已存在的上传。共享写入路径 + `open_upload_file_no_symlink` 先以 `O_WRONLY|O_CREAT` 打开目标、再 + `ftruncate(0)`,重复上传 `report.pdf` 会打开并清空已存储的 `report.pdf`, + 并发的同名上传还会在同一个 inode 上产生竞态——Gateway 摄取管道早已拒绝覆盖, + 但内嵌客户端的 `copy_upload_file_no_symlink` 与渠道入口路径仍走这条会截断的 + 打开。现在目标改用 `O_EXCL` 打开;遇到 `FileExistsError` 时通过 + `claim_unique_filename` 依据目录当前内容认领下一个空闲的 `_N` 后缀, + 并重试打开,两次上传分别落在 `name.ext` 与 `name_1.ext`、字节完好, + 且已存在的文件绝不被打开。`exclusive=True` 仍抛出 + `FileExistsError`;内嵌客户端与渠道入站文件响应现在报告字节实际落盘的名称 + (`filename`、`virtual_path`、`artifact_url`),而不是请求的名称。([#7388]) + +- **调度器:** 根据已处于终态的持久运行,对账仍在运行中的定时 occurrence。 + 持久运行已提交终态之后完成钩子才失败(瞬时的数据库、观察者或通知错误)时, + 该 occurrence 会停留在 `running` 直到重启——循环调度停止触发,单次任务 + 永远到不了终态的父状态,因为单实例的 `run_once` 只恢复过期的 launching 认领。 + 新增一条非破坏性的 `ScheduledTaskRepository.reconcile_terminal_runs` 扫描: + 选出其关联 `runs` 行已处于终态(`success`、`error`、`timeout`、`interrupted`) + 且调度元数据匹配的运行中 occurrence,并通过既有的 `finalize_occurrence` 路径 + 以该运行的状态、错误与目标判定收尾。每个候选都在自己的事务中提交,因此 + 一条被污染的 occurrence 不会回滚整条扫描;活跃运行绝不会被接管,服务还会 + 捕获剩余的扫描级失败,准入与队列排空照常运行。无 schema 变更。([#7269]) + +- **制品:** 制品注册表从工具结果捕获句柄时,CJK 直角引号「」/『』内的沙箱路径 + 保持完整。`artifact_registry.py` 的引号分隔符表(`_REF_QUOTE_DELIMITERS`、 + `_REF_TRAILING_NOISE_CHARS`、`_REF_CLOSING_DELIMITERS`)认识 ASCII 引号、 + 反引号、弯引号和 CJK 标点,却不认识 `「」`/`『』`,因此 + `已生成「/mnt/user-data/outputs/季度 报告.pdf」。` 会捕获到失效句柄 + `/mnt/user-data/outputs/季度`(路径模式在空格处停止),`已生成『…』。` + 则把闭引号 `』` 留在了 `real_ref` 里;任一句柄在后续读取时都会失败。直角引号 + 现在注册为包裹符——成对的闭引号保留在文件名内,空格得以保留;未成对的闭引号 + 会从裸词元中裁掉;以直角引号结尾的 URL 与引用也不再被切断。([#7377]) + +- **技能:** 合成 PowerPoint 幻灯片时保留透明度。ppt-generation 合成脚本此前把 + 每张幻灯片图片压平成 RGB 并存为 JPEG,PNG 幻灯片插图(包括带 `transparency` + 条目的调色板与灰度图)的透明区域因此在成片中变成黑色。`generate_ppt` + 现在通过 `img.getbands()` 或 `img.info` 检测 alpha 通道,将这些幻灯片以 + RGBA PNG 嵌入,不透明图片仍走原先的 JPEG 路径;技能文档说明透明幻灯片图片 + 会保留其 alpha 通道。([#7312]) + +- **沙箱:** 本地沙箱命令运行器在 `Popen` 失败时只关闭一次管道写 fd。 + spawn 失败时,`_run_windows_command` 与 `_run_posix_command` 会在 `except` + 块里释放全部四个管道 fd,随后又在 `finally` 块里再次关闭两个写 fd;繁忙的网关 + 中,另一个线程可能在两次关闭之间认领被释放的描述符编号,于是第二次 `os.close` + 会静默命中一个无关描述符——可能是并发命令的捕获管道,也可能是 HTTP 套接字。 + `finally` 块现在只在 spawn 成功(`process is not None`)后关闭父进程的写端; + 清理逻辑捕获 `BaseException`,因此 spawn 中途的 `KeyboardInterrupt` 仍能将 + 每个 fd 恰好释放一次。([#7394]) + - **MCP:** 普通用户现在可以在“能力中心 → 插件”中安装钉钉、企业微信和 HubSpot。 此前非管理员保存这些内置连接(包括之后编辑或启用)会返回 `500 Internal Server Error`,因为个人连接检查读取了请求模型未声明的 `cwd` 字段。 设置了工作目录的内置连接仍需管理员权限。([#7400]) +- **网关:** 为 `GET /api/threads/{thread_id}/runs/{run_id}/events` 接受的 + 运行事件类型过滤器加上上限。`list_run_events` 按逗号拆分 `event_types` + 查询参数后,不加限制地传给事件存储,因此携带任意长过滤器列表的请求会触发 + 基数无上限的存储查询。超过 `MAX_EVENT_TYPE_FILTERS`(100)个值时,现在会先返回 + 指明上限的 `422`,且在触碰存储之前完成;不超过 100 个值、缺省过滤器与 + 存储调用本身均保持不变。([#7370]) + +- **脚本:** `scripts/configure.py` 现在先解析参数,再触碰文件系统。这个引导脚本 + 此前完全忽略 argv,因此 `--help` 不打印用法,而是把引导流程当作副作用执行 + 了一遍——在全新检出中创建 `config.yaml`、`.env` 与 `frontend/.env`—— + 无法识别的标志也同样被静默忽略。参数现在经由 `argparse` 处理:`--help` + 打印用法并以 0 退出,未知标志以 2 退出,`make configure` 使用的无参数调用 + 保持不变。([#7404]) + +- **技能:** 网络文件系统上不再每轮重建线程技能视图。投影出的 `/mnt/skills` + 视图此前用 inode、大小与时间戳元数据做新鲜度摘要,这些元数据在 NFS 等 + 共享存储上会持续变动或延迟稳定,清单因此永远不匹配,每次沙箱获取都重新复制 + 整个视图。新的 `_thread_view_signature` 改为对投影出的字节与布局做指纹—— + 文件以有界分块读取并计算 sha256,符号链接按链接本身签名而不打开,目录位与 + 可执行位也包含在内——同时 `_thread_projection_is_fresh` 仍比较源签名,并在 + 下次获取时从规范技能修复被编辑的指令、缺失或被注入的文件以及已变化的链接。 + 完整性检查现在会读取投影文件,较大的包会增加获取时的读取 I/O; + `_THREAD_PROJECTION_POLICY_VERSION` 升到 2,一次性使旧清单失效。([#7333]) + +- **网关:** 在存储运行记录并调用图之前,为省略消息 ID 的运行输入补上消息 ID, + 使运行记录与图检查点携带相同的 ID,重新加入活跃运行的客户端得以对用户消息 + 去重。带键请求以 `deerflow:run-input:{thread_id}:{idempotency_key}:{index}` + 上的 `uuid5` 派生稳定 ID,无键运行则获得新的 `uuid4`;调用方提供的 ID 与 + 原始请求对象保持不变。幂等身份从补 ID 之前的输入计算,重试匹配也接受该形式, + 因此即使存储的输入现在持有网关签发的 ID,带键重试仍会复用其运行。滚动升级 + 期间,较旧的网关 worker 可能以 409 拒绝这类重试。([#7349]) + +- **扩展:** 让 `dispatch_system_model_observation`、排空快照与 + `reset_extension_notify_loop` 在同一把 `_dispatch_state_lock` 上串行,使分离 + 通知的准入与停机保持原子。此前停机可以在启用检查与 future 注册之间暂停观察 + 并对 `_pending_dispatches` 拍快照,因此恰好在停机时提交的观察会躲过排空, + 并在循环被拆除时仍处于 pending。done 回调经同一把锁丢弃结果,但在锁外注册, + 因为对已完成的 future,`add_done_callback` 可能内联执行。([#7383]) + - **示例:** 扩展示例的包测试重新通过。其测试用 registry 缺少 `knowledge_provider()`,不再满足 `ExtensionRegistry` 协议,按 README 运行 `pytest` 会失败。Extension Package Check 现在也会运行自带测试的内置示例的测试, 因此破坏这些测试的契约变更会在其 PR 中失败。([#7364]) +- **项目:** 在项目文档路由中,把移除暂存书架副本视为尽力而为: + `_discard_staged_copy` 现在捕获 `OSError` 并记录警告,而不再让 + unlink 失败压过一切。此前移除失败(Windows 上句柄仍被别处持有、 + 只读挂载)会从 `attach_project_document_to_thread` 的 `finally` + 中抛出,顶替端点的真实结果并跳过 `service.aclose()`,还把 + `upload_project_document` 与 `promote_thread_file_to_shelf` 的 + 空文件 `400` 路径变成 500。残留的暂存文件仍归暂存清扫负责。([#7229]) + +- **脚本:** `dev_multi_instance` 现在在任何写入者运行之前就把目录或文件祖先 + 拒绝为 `--out`,在 `main()` 中以 `parser.error` 失败并指明问题路径。 + `render-config`、`secrets` 与 `nginx-conf` 通过 `os.replace` 发布输出,因此 + 已存在的目录此前会暴露为原始的 `PermissionError` 回溯,文件祖先则报 + `mkdir` 的 `OSError`——而此时写入者已经做了部分工作。这三个命令的 + `--help` 文本现在说明了该文件契约。([#7289]) + +- **浏览器:** 当目标页面返回 HTTP 400-599 而抓取服务本身报告成功时,Unbrowse + 抓取的页面现在会附带一条警告前缀,并在可用时包含上报的最终 URL;空页面错误 + 也保留同样的状态详情,而不是只给一句 `Error: No content found`。此前对 + 失败目标的抓取与成功抓取无法区分。([#7257]) + +- **技能:** 播客生成 CLI 失败时现在以状态码 1 退出并把诊断信息写到 stderr, + 而不是在 stdout 打印 `Error generating podcast` 并以 0 退出。SKILL.md 现在 + 指示模型只在成功运行之后才展示 MP3,因为已存在的音频文件或新写入的转录 + 文本并不能证明生成成功。([#7236]) + +- **项目:** 取消时也会排空书架暂存磁盘操作,使 `stage_document_bytes` 仍会 + 关闭并 unlink 写了一半的文件。清理现在在 `BaseException` 上运行,每个 + `run_file_io` 步骤都包在 `await_drained` 中,且已打开的句柄在 worker 到 + 调用者的交接过程中保持持有;等待下一个上传分块仍然可以取消。此前 + `asyncio.CancelledError` 会绕过 `except Exception` 的清理,客户端在上传 + 中途放弃时会残留写了一半的暂存文件。([#7360]) + +- **CI:** `.github/labels.yml` 的注释改指 `triage.yml`,不再指向已删除的 + `pr-labeler.yml` / `pr-triage.yml` / `issue-triage.yml` 工作流;同时在 + `lint-check.yml` 中加入 `scripts/check_label_declarations.py`,当工作流 + 使用了 `labels.yml` 未声明的标签时即失败。GitHub 在任一名称缺失时会以 + 422 拒绝整批 `addLabels`,因此一个未声明的标签可能静默丢弃全部自动 + 打标签。([#7328]) + +- **子智能体:** 当一条 `AIMessage` 携带多个同 ID 的工具调用时,父上下文 + 快照不再把结果配错对。`ParentContextSnapshot` 现在把这样的 ID 标记为 + 歧义且不可认领:其结果既不会完成重复调用中的某一个,也不会回落到复用 + 该 ID 的更早待处理调用;歧义调用从快照中省略,而更早已完成且唯一的 + 调用保留不动。此前最后一个重复项会在 `call_positions` 中赢得该 ID, + 结果可能被记到错误的调用上。([#7372]) + - **网关:** 请求体格式错误的运行请求现在返回客户端错误,而不是 `500 Internal Server Error`。此前 `config.configurable` 或 `config.context` 不是对象、`assistant_id` 无效、`agent_name` 不是字符串,或 AI 消息的 `tool_calls` @@ -1240,20 +2230,257 @@ `422`(消息问题返回 `400`)。在线程路由上,此前会被接受的 `config.configurable` 取值 `[]`、`""`、`0`、`false` 或键值对列表,现在同样返回 `422`。([#7362]) +- **上传:** `upload_files` 现在会报告被拒绝的附件,而不是静默丢弃:不安全的 + 文件名与未命名的 multipart 部分都会记录到 `skipped_files`,`success` 表示 + 至少存储了一个文件(`bool(uploaded_files)`)。此前两条丢弃分支都返回 + `success: true` 且 `files` 列表为空,因此前端阻止聊天发送的门槛会在缺少 + 用户所需附件的情况下放行;全部被拒的批次现在失败,混合批次成功并列出 + 被丢弃的内容。([#7165]) + +- **网关:** Gateway 关闭期间会关闭已初始化的沙箱提供方:`langgraph_runtime` + 退出且 MCP 会话池拆除之后,lifespan 在同一个 `finally` 内于工作线程上排空 + `shutdown_sandbox_provider()`,覆盖由工作线程工具调用首次创建的 AIO 沙箱。 + 此前 Gateway 从不关闭该提供方,其沙箱会比进程活得更久。该钩子不会为了 + 清理而初始化提供方,失败会记录日志且不阻塞 worker 退出。([#7347]) + +- **沙箱:** Tenki 的终止属主关系在提供方退役后仍然保留:待关闭的句柄会停放 + 在 `_pending_closes` 中,由空闲检查器与后续的关闭流程重试,而不是关闭失败 + 一次就被放弃;退役之后创建的沙箱由创建它的 worker 负责关闭,在途的获取 + 会被计数并以 `RuntimeError` 拒绝。终态失败回调现在携带来源适配器 + (`expected_sandbox`),因此被替换一代延迟上报的失败无法注销 `acquire` + 已经重建的 scope。`shutdown()` 保持可重试,失败时抛出 + `_ShutdownIncompleteError` 并附上待处理获取与终止计数,而不是假装完成排空; + atexit 包装器记录该警告但不带堆栈。`TenkiSandbox.close` 只在终止被接受或 + 收到权威的已不存在错误时才标记为已关闭,因此含糊的传输失败仍可重试。([#7311]) + +- **技能:** 数据分析 JSON 导出现在拒绝结果列名重复的情况,抛出的 + `ValueError` 会列出重复的名称并建议使用 SQL `AS` 别名;CLI 会向 stderr + 打印一行简短的 `Error: ...` 并以非零码退出,已有的输出文件保持原样。 + 此前重复的列在构建 JSON 记录时互相静默覆盖,每个名称除最后一个值外 + 全部丢失。`main()` 现在还在 `finally` 中关闭数据库连接,因此拒绝时 + 不会泄漏连接。([#7305]) + +- **智能体:** `read_file` 成功结果中的 JSON `error` 字段现在视为文档数据, + 工具消息标记为 `status="success"` 且 `source="content_analysis"`,而不是 + 归类为工具错误。读取已保存的 API 错误响应不再记录一次失败的读取,也不会 + 在启用可选的 `tool_progress` 护栏时阻塞后续读取;带错误前缀的显式读取 + 失败仍然优先。([#7369]) + +- **技能:** 技能扫描器的 `shell-curl-pipe-shell` 规则现在从共享的 + `_SHELL_NAMES` 词表构建 shell 名称的交替分支,并把该集合从五种 shell + 扩到十种(新增 `ash`、`csh`、`ksh`、`mksh`、`tcsh`),因此 + `curl ... | ksh` 及其未列出的同类现在会产生该发现,而不是静默运行抓取 + 到的脚本。此前下载管道匹配器硬编码了 `bash|zsh|dash|fish|sh`,而结构化 + 的 `env` 回退读取 `_SHELL_NAMES`,尽管沙箱审计中间件早已把 `ksh` 算作 + shell;仅共享 shell 前缀的程序(`ashley`、`tclsh`)仍然不匹配。([#7378]) + +- **扩展:** 扩展与技能保存失败时的临时文件清理现在经由 + `cleanup_failed_atomic_write`,unlink 失败或日志错误不再顶替原始的发布 + 错误。扩展快照回滚、受管 `plugins` 块重写、Gateway 技能归档上传副本及其 + 安装后丢弃、自定义技能与按用户技能状态写入都走该路径; + `_write_bytes_atomically` 在 `fdopen` 失败时关闭描述符,plugins 重写在写入 + 前先记录临时路径,短写时仍能删除;上传路由在关闭 multipart 表单(抑制 + 关闭错误)之前先丢弃归档,因此 `SpooledTemporaryFile` 的 flush 失败不会 + 遮蔽安装结果。([#7164]) + +- **网关:** 线程状态更新(`POST /threads/{id}/state`)现在遵循请求体中的 + `checkpoint` 对象,而不只是已弃用的 `checkpoint_id`:校验其 `thread_id` + 与 URL 一致(不一致返回 400),并把 `checkpoint_ns` 沿状态访问器链传递, + 使更新可以指向子图检查点而不是总是根。仅改状态的变更图现在会显式绑定 + checkpointer(`CONFIG_KEY_CHECKPOINTER`),让请求的命名空间直接在图本身上 + 读写;无法解析的命名空间或检查点返回 404;`title` 值只在更新指向根命名 + 空间时才更新线程元数据,子图标题不再泄漏进对话标题。([#7115]) + +- **基准测试:** `bench_provider.py` 在 CLI 校验阶段拒绝负数 `--idle-timeout`, + 报错 `SystemExit("idle timeout must be nonnegative")`,且发生在创建任何提供方 + 之前;基准测试 README 也说明了零值会禁用空闲回收。测试为三个已注册提供方 + (`opensandbox`、`boxlite`、`aio-docker`)固定了这一拒绝行为:任何提供方工厂 + 被调用,测试即失败。([#7363]) + +- **沙箱:** 对端先完成同一次拆除时,AIO 沙箱隔离区拆除现在保持幂等: + `retire()` 与 `retire_replaced_generations()` 通过 `_records()` 列出围栏目录、 + 通过 `_drop()` 摘除记录,两者都把目录或记录缺失视为工作已完成。拆除互斥依靠 + TTL 租约而非锁,因此此前对端的 `retire()` 若落在存在性检查与列表之间(或列表与 + 摘除之间),会让沙箱创建顶部未做包裹的 `_retire_absent_quarantine()` 调用抛出 + `FileNotFoundError`;而对端 `mark()` 仍打开着新记录做 `fsync` 时,Windows 下 + `PermissionError` 则有意保持为错误。([#7313]) + +- **前端:** 助手消息中的相对链接(如 `[Report](report.md)`)现在会解析到 + 线程制品:在消息列表内,一个无 scheme、不以 `/` 或 `..` 段开头、且恰好后缀匹配 + 一个制品的路径会在新标签页中打开它;匹配为零个或多个时渲染为不可点击的纯文本, + 而不是让浏览器把链接按聊天路由解析成损坏的 `/workspace/chats/<file>` 线程 + (网关返回 422)。制品列表经 `MessageArtifactPathsContext` 到达链接渲染器; + 澄清与 `present_files` 文本改用感知线程的渲染器;重新打开的线程会依据消息中 + 的 `present_files` 调用重建制品列表,因为历史头部携带消息却不带 `artifacts`。 + ([#7356]) + +- **沙箱:** 本地沙箱现在在 spawn 时保存命令的进程组 id(`process.pid`,因为 + `start_new_session=True` 会让子进程成为组长),超时时会杀死这个已保存的组, + 而不是在 `_terminate_process_group()` 里再次调用 `os.getpgid()`。此前若组长在 + 超时与杀死之间退出——这一窗口同样覆盖排空管道线程的启动——`getpgid` 会抛出 + `ProcessLookupError`,而同一个处理器恰好捕获该异常,超时处理随之降级为只杀死 + 直接子进程,组的其余成员继续运行;现在只有真正消失的组才会回退到直接子进程。 + ([#7353]) + +- **事件:** 数据库运行事件存储把显式为空的 `event_types` 列表现在视为匹配不到 + 任何事件的空过滤器,与记忆存储和 JSONL 存储保持一致;此前真值检查会丢掉 + `event_type IN (...)` 谓词,返回该运行的所有事件。`event_types=None` 仍表示 + 不过滤。([#7352]) + +- **acp:** `invoke_acp_agent` 现在将 ACP 交换与智能体进程赛跑,智能体退出或 + 关闭连接时快速失败,而不是等满 `timeout_seconds`(默认 30 分钟)并建议调大; + 在短暂的 `_EXIT_DRAIN_SECONDS` 宽限(让仍缓冲在 stdout 的响应到达)之后,它会 + 报告退出码并指向安装与认证问题。该工具还会把智能体的 stderr 持续排空到一个 + 有界的 8 KiB 尾部(`_StderrTail`)——固定版 SDK 以 `stderr=PIPE` 启动智能体 + 且从不读取它,因此啰嗦的智能体若阻塞在下一次 stderr 写入上,也会以同样方式卡住 + 调用——并在超时或退出时记录该尾部,同时不让它进入模型可见文本。([#7354]) + +- **评估:** 上下文快照基准的评分现在在所有取值比较处都区分布尔与数字:新增的 + `_same_value()` 辅助函数会拒绝 `false` 顶替 `0` 这类不匹配,并递归遍历字典与 + 列表,取代原先只在 JSON 检查顶层捕捉布尔/数字混淆、在 Python 检查(包括输入 + 变更检测)中完全漏掉的比较。`1.0 == 1` 这类普通数值相等保持不变。([#7358]) + +- **子智能体:** 在输出上限处被提供方截断响应(`finish_reason=length`、 + `stop_reason=max_tokens`、`MAX_TOKENS`、Responses `max_output_tokens`)的 + 子智能体,现在会以 `completed` 加 `subagent_stop_reason=model_length_capped` + 到达主智能体,而不是干净的 `completed`;在该边界被截断的工具调用也不再带着 + 残缺参数执行。`ModelLengthFinishReasonMiddleware` 此前只挂主智能体链, + 现在加入子智能体链,位于扩展之后、Safety 之前,按上下文 `run_id` 记录检测到 + 的每个上限,并暴露 `consume_stop_reason`,让执行器既有的鸭子类型守卫收集呈现 + 该上限;该值进入子智能体状态契约(Python 枚举、契约 fixture、前端白名单), + 标注为“模型输出长度上限”。([#7355]) + +- **沙箱:** 本地沙箱的 `glob()` 与 `grep()` 现在能搜索未映射的虚拟父目录—— + 例如技能根目录,它只以按类别的挂载形式存在、没有聚合的宿主目录——方式是通过 + `_virtual_search_mounts()` 遍历每个显式配置的子挂载,并以虚拟路径返回匹配项 + (`find_glob_matches()` 与 `find_grep_matches()` 新增 `relative_prefix`), + 每个挂载做一次单条前瞻,使结果上限跨挂载保持全局。此前把虚拟父目录对宿主 + 解析会让搜索失败,即使类别挂载确实存在;忽略规则仍在每个挂载内生效,损坏的 + 挂载会以其虚拟路径告警并重新抛出,而不是遍历宿主父目录;已映射的根保持正常 + 遍历。([#7338]) + +- **沙箱:** 沙箱审计中间件现在会拦截把进程替换作为第一个操作数交给解释器的命令 + (`bash <(curl url)`),它会把取回的字节当程序运行;此前只识别 stdin 重定向 + 写法(`bash < <(curl url)`),因此操作数形式会让 `curl`/`wget` 取回的内容不受 + 阻碍地穿过审计执行。该规则要求解释器(`sh`、`bash`、`python`、`perl`、`ruby`、 + `node`、`php`)处于命令位置,且替换(`curl`、`wget`、`base64` 或其他列出的 + 可执行文件)作为其第一个操作数;前面带脚本参数(`bash script.sh <(curl url)`) + 时替换仍是普通参数,noexec shell(`bash -n <(curl url)`)也仍只做解析。 + ([#7290]) + +- **模型:** MindIE 提供方的转义换行解码现在同时保护波浪号围栏(`~~~...~~~`)与 + 反引号围栏,一次性 `_decode_escaped_newlines_outside_fences()` 与流式的 + `_EscapedNewlineStreamDecoder` 都涵盖在内,波浪号围栏代码块内的字面 `\n` 不再 + 变成真实换行。围栏只由自己的分隔符闭合;未闭合的围栏延伸到回复末尾,同时原样 + 保留另一分隔符的一对完整匹配,与流式解码器一致;此前只识别反引号分隔符。 + ([#7264]) + - **TUI:** 按 `Ctrl+C` 中断运行后再退出,不再卡在该运行仍在完成的步骤上。此前运行 线程会在退出时被等待,TUI 已关闭但 shell 提示符一直阻塞,直到较长的工具调用或模型请求 返回,再按 `Ctrl+C` 也只会打印 traceback。现在退出时会提示正在等待该步骤,并在其完成后 关闭;再按一次 `Ctrl+C` 即可立即退出。([#7326]) - + +- **目标:** 目标评估器的响应若正常结束(`stop` / `end_turn`)却不包含有效判定, + 现在会重试一次评估,而不是直接判定目标检查失败:`evaluate_goal_completion()` + 针对这种情况抛出新的 `GoalEvaluationFormatError`,显式拒绝以及长度/安全停止 + 保持既有失败行为。工作线程只有在重读目标与检查点,并确认目标实例、检查点 id + 和可见对话签名均未变化后才重试。证据截断上限还会利用 + `MAX_GOAL_CONVERSATION_CHARS` 的剩余空间恢复缩短的用户输入,而不是在预算尚有 + 余量时让长的用户行保持截断。([#7345]) + +- **上传:** 在截断前把缺失或非字符串的上传失败 `message` 强制转换为空字符串, + 这样省略该字段的网关或代理会给出干净的“Upload failed”错误,而不是在处理失败 + 时抛出 TypeError。([#7342]) + - **TUI:** 流式对话记录不再丢弃与已显示文本重复或在其基础上延伸的 token。此前增量 按前缀合并,`ha` + `ha` 显示为 `ha`,`1` + `10` 显示为 `10`,以 Markdown 标记开头的 回答还会丢失之后单独出现的同一标记(闭合的 `**`、列表符号、表格竖线、闭合代码块)。 现在每个增量都按原样追加。([#7325]) +- **制品:** 自由文本制品扫描保留带分隔符的沙箱路径整体,而不是在第一个空格处 + 截断:旧的 `/mnt/user-data/\S+` 匹配在工具引用带空格的文件名时,会把第一个 + 空格之前的前缀注册句柄,因此 `/mnt/user-data/outputs/quarterly report.pdf` + 变成 `/mnt/user-data/outputs/quarterly`,在 compact 或检查点重载后解析为空; + 单引号与反引号也以同样方式失败。`_classify_span` 按分隔符给每个候选分类—— + 以直引号、弯引号或反引号开启且在同一行闭合的跨度会保留其中的空格并识别转义的 + 分隔符;未闭合的跨度不产生引用,而不是留下截断的前缀;裸路径保持保守的空白 + 边界,仍限定在单行与现有大小上限之内。([#7308]) + +- **记忆:** 在 `search` 与 `list_memories` 的 `_read_or_fallback` 内把记录 + 转换为事实,让 Mem0 的 `read_policy` 同样覆盖记录转换失败:`categories` 或 + `metadata` 字段格式错误、或 `score` 非有限的记录,现在遵循配置的失败放开/失败 + 关闭行为,而不是在客户端调用已经成功之后从读取中抛出异常。转换错误以 + `Mem0APIError` 呈现,指明出错的字段和净化后的记录 id(id 不是有界的 + `[A-Za-z0-9_-]` 字符串时显示 `<unavailable>`),且不回显远端字段值。([#7307]) + +- **记忆:** 将格式错误的 Mem0 HTTP 响应规范化为 `Mem0APIError`,使其受配置的 + `read_policy` 管辖:`Mem0Client._request` 现在还会捕获响应体解码产生的 + `UnicodeDecodeError` 和 `RecursionError`,并拒绝非对象 JSON 载荷; + `_memory_results` 则拒绝非列表的 `results` 字段或非对象记录——这些形态此前 + 会从下游 `.get` 调用中以 `AttributeError` 或 `TypeError` 的形式抛出,绕过 + 失败放开的空结果回退。错误携带操作上下文,包括 `POST /v3/memories/` 列表 + 操作的分页 `page`。([#7202]) + +- **前端:** 为 `useBrowserStream` 中的 Live Browser WebSocket 握手加 10 秒的 + `HANDSHAKE_TIMEOUT_MS` 定时器:停滞的升级现在会进入有界重连路径并关闭套接字, + 而不是无限等待可能永远不会到来的错误或关闭事件;已废弃的尝试会对迟到的 + `open`/`message` 回调加设围栏,使其无法接受输入或重置重连预算。面板以本地化的 + 状态行区分 `handshake_timeout` 与通用的 `connection_failed` 关闭原因,并更新 + Live 按钮和空状态文本。([#7301]) + +- **模型:** Claude 提供方重试现在会遵循 HTTP 日期形式的 `Retry-After` 提示。 + `ClaudeChatModel` 此前会忽略日期形式的提示并沿用默认延迟,而错误处理中间件和 + OpenAI Codex 提供方各自保留私有的日期解析器;`deerflow/utils/retry_after.py` + 中新增的共享 `parse_retry_after_date_ms` 以 UTC 解析 HTTP 日期(把 naive 的 + asctime 形式当作 UTC),并以 24 小时的 `MAX_RETRY_AFTER_MS` 策略限定结果上限, + 三处调用点现在都使用它。([#7295]) + +- **前端:** 在令牌用量调试视图中,把仅含思考的 AI 步骤标注为“Thinking”而不是 + “Final answer”:`buildTokenDebugSteps` 此前使用 `hasContent`,它会把非空内容 + 数组计入,即使其中每个块都是 Anthropic `thinking` 块,因此只携带推理令牌的步骤 + 被归类为最终答案。现在改用 `extractContentFromMessage`,它会把 `thinking` 块 + 映射为无可见文本。([#7175]) + +- **调度器:** 拒绝 `once` 计划中在任务时区里不存在的 `run_at` 与 `end_at` 墙上 + 时间(即夏令时春季前拨的空档)并返回 422(`run_at` 为 `invalid_schedule`, + `end_at` 为 `invalid_request`),而不是悄悄移动该时刻:naive 值会附加到任务的 + `ZoneInfo` 时区上,且 UTC 往返必须复现所提交的墙上时间。有歧义的本地时间保留 + 较早的一次(`fold=0`),显式偏移量保留指定时刻,未变更的现有 `once` 定义在 + no-op 编辑时保持不变,而变更过的定义或将终态任务重新激活的 PATCH 会重新 + 校验。([#7281]) + +- **沙箱:** 在 Linux/macOS 上本地文本修订失败时保留原文件,而不是在打开时截断: + 沙箱 `write_file` 现在通过已打开的描述符检查写权限(拒绝非普通文件),并暂存 + 隐藏的 `.deerflow-write-` 兄弟文件,再通过 `publish_bytes_atomically` 进行 + 原子重命名,因此写入失败时旧内容保持已发布状态,暂存也会被清理。新增的 + `file_revision_metadata` 模块会在写入内容之前,从源描述符转移属主、权限位、ACL + (macOS)与 xattrs(Linux),并拒绝 setuid/setgid 位、macOS 文件标志以及与内容 + 绑定的 `security.*` 特权,而不是把它们重新附加到新内容。追加模式与其他平台 + 保留原有的就地行为,硬链接别名在 inode 替换后保留旧版本。([#7299]) + - **沙箱:** acquire 发现 E2B VM 已不存在(缓存客户端已失效,或暖池中的 VM 在停放期间被 回收)时,释放该 VM 的所有权租约。此前租约会一直保留并续约到 Gateway 关闭,每个被回收的 VM 都会留下一个跟踪 ID 和一个持续续约的所有权键。([#7277]) - + +- **Helm:** 每条 Secret 名称优先级链只编码一次:新增的 + `deer-flow.databaseUserSecret` helper(先取 + `postgresql.external.existingSecret`,再取 `postgresql.existingSecret`) + 同时供网关的 `DATABASE_URL`、postgres StatefulSet 的 `POSTGRES_PASSWORD` 以及 + `postgres-secret.yaml` 渲染门控使用,因此它们不会再解析到不同的 Secret。它和 + `redisUserSecret` 现在都改用 `default` 串联而不是 `or`——后者在值为假时也返回 + 其最后一个操作数:此前 `existingSecret: false` 会渲染出字面名为 `false` 的 + Secret 引用,并跳过创建 chart 托管的 Secret,导致 redis StatefulSet + 无法启动。([#7291]) + +- **目标:** 把 Goal 无进展计数器改为基于新观察到的成功操作,而不是只基于评估器 + 的文本进展键——后者只要可见证据变化就会重置,使一次运行可以在不触发自动暂停停止 + 规则的情况下无限重复同样的工具工作。`observe_goal_operations` 把每个工具调用 + 与自本轮发起输入以来的结果配对,按工具名、参数以及输出的哈希和字节数为成功操作 + 生成指纹——排除仅用于 UI 的沙箱 `description` 参数;有工具活动的轮次没有产生 + 新的操作哈希时,`compute_no_progress_count` 递增,纯文本轮次保持原有比较。带 + 非零退出标记的 Bash 结果会被重新归类为错误,以纠正旧检查点上遗留的内容分析 + 成功标记,且已见哈希会跨压缩与重启持久保存在 `last_evaluation` 中。([#7271]) + - **认证:** OIDC 提供方以 `200` 返回非预期 JSON 对象的响应体(例如维护中的 HTML 页面)时,SSO 登录与回调不再以 `500` 失败。discovery、token、JWKS 与 UserInfo 响应体、格式错误的 ID token 头以及缺少可用端点的 discovery 文档,现在都走既有的 @@ -1261,6 +2488,260 @@ 格式错误的 UserInfo 响应体会像 UserInfo 请求失败一样被跳过,不再让 ID token 有效的登录中断。([#7258]) +- **认证:** 把声明有误但签名正确的 JWT 视为格式错误,而不是泄漏 + `pydantic.ValidationError`:`decode_token` 此前只捕获 `jwt.PyJWTError`,因此 + 缺少 `sub`/`exp` 的令牌会从认证路径以 500(或中断的 WebSocket 握手)抛出, + 而不是返回 `TokenError.MALFORMED` 和 401——这类令牌正是 OIDC state cookie 的 + 形状:它共享 `jwt_secret`,且由公开的 OAuth 启动端点签发。 + `_verify_state_signed` 也有同样的缺口,现在对这类 cookie 返回 `None`,让 + `/api/v1/auth/callback/{provider}` 处理器应答 403;而同样缺少声明的过期令牌仍 + 报告 `EXPIRED`,因为 `jwt.decode` 会先失败。([#7266]) + +- **渠道:** 把无法解码的渠道状态文件视为损坏:读取 `runtime-config.json` 与微信 + 游标/认证状态的读取器此前用 `encoding="utf-8"` 解码,却只捕获 + `json.JSONDecodeError` 和 `OSError`,于是 `UnicodeDecodeError` 会逸出,阻止 + `ChannelService.start()` 启动任何 IM 渠道,中断一次性旧文件导入,并拖垮微信 + 渠道自身的启动。`ChannelRuntimeConfigStore._load`、`_read_legacy_file` 以及 + `WechatChannel._load_state`/`_load_auth_state` 现在会在现有处理器旁边指名捕获 + `UnicodeDecodeError`,获得渠道存储已有的同一套损坏文件恢复:记录警告、使用内存 + 回退,并把文件留在原地。([#7273]) + +- **脚本:** 对传给 `support_bundle --out` 的目录,改以 CLI 干净的 `error: ...` + 消息并以退出码 2 拒绝,而不是从 `zipfile.ZipFile` 内部抛出原始的 + `PermissionError` 回溯(Windows 上的失败形态): + `create_support_bundle` 现在抛出 CLI 已映射的 `ValueError`,且 + `--out` 帮助文本改为“Output zip file path”。([#7256]) + +- **工作区:** 不再从截断的工作区快照推断文件创建或删除:受 `max_scanned_files` + 限制的扫描会让缺失保持未知,因此 `get_changed_paths()` 只在前置快照完整时才 + 把路径报告为已创建,且只在后置快照完整时才报告为已删除。此前,运行前已存在但 + 落在运行前截断扫描之外的文件会被报告为由该运行创建,而落在运行后扫描之外的 + 文件则被报告为已删除。扫描器现在还按排序顺序遍历子目录,使截断边界在前置与 + 后置扫描之间可复现;当截断的运行前扫描得不到可证明的输出变更时,worker 会在 + `run.delivery` 回执中记录 + `verification_skipped=truncated_pre_run_snapshot`。([#7172]) + +- **记忆:** 把 `additional_kwargs` 中带 `hide_from_ui` 的人类消息排除在 Honcho + 记忆写入之外:这样的消息不会进入外发批次,除非 `should_keep_hidden_message` + 宿主钩子接受它——框架注入的上下文不再到达 Honcho 的服务端派生,而用户的正当 + 澄清回答仍会到达。没有可调用钩子的独立构造会丢弃每条隐藏人类消息;抛异常的 + 钩子只跳过该消息,并记录一条不携带消息内容或异常文本的通用警告。([#7159]) + +- **调度器:** 手动试运行时保留周期任务的 `next_run_at` 游标: + `should_advance_schedule_cursor()` 把对周期任务的手动触发当作一次额外的运行, + 不得消耗下一次计划运行,因此存储的节奏在 runs 仓库中的启动、运行收尾与崩溃修复 + 记账中都得以保留。此前每次启动都会从启动时间重算 `next_run_at`,一次手动试运行 + 会把下一次自动运行推后一个完整间隔。一次性任务保留其既有边界语义。([#7209]) + +- **API:** 前端认证 fetch 包装器现在保留 `Request` 输入:`fetcher.ts` 此前只取 + URL 并转发 `{...init}`,用 `Request` 对象调用会静默丢失其 method、headers 和 + abort signal。现在只要 `init` 未覆盖,包装器就会从 `Request` 读取 `method`、 + `headers` 和 `signal`,把它们转发给静态演示解析器,并把原始输入而不是纯 URL + 字符串传给 `globalThis.fetch`。([#7254]) + +- **引用:** 提取引用来源时,行内代码遮蔽只作用于单个段落:`maskInlineCode` 此前 + 在整个文档范围内配对反引号,不同段落里的两个杂散反引号因此组成一个 CommonMark + 永远不会渲染的 span,夹在中间的引用从 Sources 列表中消失,而被抢走的开引号还会 + 让下文一个真正的代码 span 失去遮蔽。现在行内 span 在段落边界(感知 CRLF)以及 + CommonMark 使用的其他块边界处结束;围栏代码遮蔽也能识别嵌套在列表项与引用块中 + 的围栏——感知标记的开围栏与闭围栏、相对容器的缩进、tab 展开后的列——此前它们 + 只是碰巧闭合,让示例引用浮现成来源。容器前缀现在逐位置扫描,携带数千个引用标记 + 的消息不再会触发指数级正则回溯而拖慢渲染。([#5876]) + +- **上传:** 原子写入的清理不再覆盖调用方原本的结果:上传伴随记录器以及 Lark + broker 和 CLI 暂存路径现在通过 `cleanup_failed_atomic_write()` 释放临时文件, + 它会保留原始异常,并把关闭描述符与 unlink 变成只在失败时记录日志的尽力而为步 + 骤。此前 `register_companion` 在 `finally` 块里 unlink 暂存文件,其中的 + `OSError` 会覆盖调用方本来要看到的错误,Lark 归档下载的网络错误也可能因清理失 + 败而丢失。([#7200]) + +- **脚本:** `start-daemon.sh` 兼容包装脚本现在启动生产模式:它此前调用的是 + `serve.sh --dev --daemon`,而 Makefile 的 `start-daemon` 目标使用 + `--prod --daemon`,于是在期望后台生产启动的场景下,包装脚本拉起的是开发栈。包 + 装脚本现在与目标保持一致,并仍然透传额外的参数。([#7261]) + +- **工作区:** 快照文本缓存在所有主机上都按字节一致地发布:扫描器以 + `newline="\n"` 写入,快照文本读取器以 `newline=""` 打开,解码后的 CRLF 内容经 + 过缓存往返保持不变。此前的文本模式默认值在两个方向上转换换行符:在 Windows + 上,发布过程把每个 `\n` 重写为 `os.linesep`(把 CRLF 内容变成 `\r\r\n` 字 + 节),而 universal-newline 读取又会无声地把 CRLF 归一化为 LF,损坏从缓存提供的 + 文件文本。Windows workflow 现在会在触碰 `workspace_changes` 的拉取请求上运行文 + 本缓存守护模块,而不是只在它的每周计划中运行。([#7110]) + +- **Helm:** 遵循用户托管 Secret 中的 `redis-password`:内置的 Redis StatefulSet + 此前只从 `redis.auth.password` 推断 AUTH 模式,于是同时携带 `redis-url` 与 + `redis-password` 的 `redis.existingSecret` 会在不带 `--requirepass` 的情况下启 + 动服务器,而网关却按 URL 中的密码认证,两个探针也在无认证下 ping——Pod 报告健 + 康,网关却无法连接。容器现在在运行时从选定的 Secret 读取可选的 + `redis-password` 键,仅在其非空时启用 AUTH,readiness 与 liveness 探针遵循同样 + 的选择;只携带 `redis-url` 的 Secret 保持无认证模式,过期的 + `redis.auth.password` 不再胜过选定的 Secret。([#7161]) + +- **前端:** 让折叠侧边栏的导航在移动端保持可达:原有侧边栏在移动端只渲染一个模 + 态抽屉,抽屉关闭时工作区便失去对话列表、插件与渠道导航,也没有可见的入口可以重 + 新打开它。现在 `WorkspaceSidebarFrame` 在移动端抽屉关闭时保持挂载一条图标宽度 + 的折叠侧栏,在同源导航时关闭抽屉以释放焦点陷阱,并把焦点还给打开者或侧栏的触发 + 按钮;顶部触发按钮在移动端始终可见。同一时间只挂载一棵内容树, + `ProjectsSection` 与 `RecentChatList` 跟随抽屉的展开状态而非保存的桌面偏好, + Agents 页的头部操作也会在移动端视口内换行。([#7198]) + +- **沙箱:** 把映射路径替换进 PowerShell 命令时进行转义:本地沙箱此前把 + `/mnt/user-data` 容器路径解析成主机路径后原样拼接,于是包含空格或 PowerShell + 元字符(`$`、`&`、引号)的主机路径会破坏命令或被重新解析。 + `escape_powershell_path()` 现在会读取调用方的引号上下文——包括 here-string、 + 注释与子表达式——在单引号字符串内把引号翻倍,在双引号与无引号位置用反引号转义 + 元字符,同时保留通配符与调用方的收尾引号。沙箱命令解析器和 + `replace_virtual_paths_in_command()` 都应用它。([#7245]) + +- **客户端:** 发布转换生成的 Markdown 伴随文件时不再覆盖同名现有文件:只要调用 + 开始时基名已存在,`upload_files` 就会把伴随文件写到该现有名称上,于是在 + `report.md`(用户所写或此前上传调用留下的)之后上传 `report.xlsx`,会覆盖那份 + Markdown 的内容。伴随文件现在总是以不覆盖方式发布,并登记字节实际落盘的名称, + 因此现有 Markdown 得以保留,新伴随文件取下一个空闲的 `_N` 名称;而重新上传文档 + 仍会替换原上传,并获得新的伴随文件。([#7054]) + +- **运行时:** 保留被中断运行的助手输出:流式文本此前从未进入持久化历史,因为 + `llm.ai.response` 事件只由 `on_llm_end` 产生,而在生成中途被中断的运行永远不会 + 触发它,持久化的消息历史因此丢掉了客户端已经看到的文本。现在工作线程在运行流式 + 输出时累积主智能体的流式文本,一旦运行的结果为 `interrupted`, + `RunJournal.record_interrupted_ai_responses()` 就把这些待定响应作为 feed 事件 + 发出,并在元数据中标记 `interrupted` 与 `partial`;它们不进入检查点状态,隐藏 + 消息会被跳过,回调已持久化的响应按消息同一性胜出。([#7153]) + +- **网关:** 历史搜索结果包含活跃任务消息:归档全文匹配此前带有固定的 + `LIMIT 8`,活跃消息只在其后追加,因此一旦有八条归档行匹配,同样匹配的活跃消息 + 就被截断在结果之外。归档查询现在把上限作为参数传入,先收集活跃匹配并填满结果窗 + 口,归档行再对它们去重,同一来源两边都有时优先采用活跃副本的文本。([#7169]) + +- **技能:** `music-generation/scripts/generate.py` 在触碰输出路径之前,先对 + MiniMax 音乐音频解码并做头部检查:hex 载荷提前解码,空字节会抛出 "MiniMax + audio decoded to empty bytes",没有 ID3 tag 头或 MPEG 帧签名( + `_has_mp3_header`)的响应会抛出 "MiniMax audio does not have an MP3 + header"——这些都发生在创建输出目录或打开文件之前。此前畸形响应只在写入时才碰 + 到 `bytes.fromhex`,那时 `open(output_file, "wb")` 已经截断了现有输出;有效的 + 生成流程不变,签名检查只是健全性检查,不是音频解码。([#6688]) + +- **技能:** 把 `skill-creator` 的 `quick_validate.py` 对齐到运行时技能 schema, + 使其不再拒绝加载器接受的技能,并开始拒绝加载器丢弃的技能:`SKILL.md` 以 + `utf-8-sig` 读取(`utils.parse_skill_md` 同样如此,因此通过校验的 BOM 文件不会 + 再破坏 `run_eval`/`run_loop`),frontmatter 分隔符可以携带尾随空白,并允许 + DeerFlow 字段 `argument-hint`、`required-secrets`、`secrets-autonomous`、 + `version` 与 `author`。`allowed-tools` 现在采用运行时解析器的形态规则(列表项 + 非字符串、工具名为空、不匹配的右括号、未闭合的引号、未闭合的括号模式), + `required-secrets`/`secrets-autonomous` 的形态也像运行时校验器一样检查;同时拒 + 绝空的 `name`/`description`,并移除 `compatibility` 的类型/长度检查,因为运行 + 时从不检查该字段。非字符串 YAML 键会在未预期键检查之前先字符串化,而不是在 + `sorted()` 中抛出 `TypeError`。([#7180]) + +- **沙箱:** 本地 `list_dir` 与 glob 遍历现在上报权限错误,而不是报告一个空的成 + 功结果:`list_dir` 不再逐目录吞掉 `PermissionError`,`find_glob_matches` 给 + `os.walk` 传入一个 `onerror` 处理器,重新抛出 `_GREP_RACE_ERRNOS` 遍历竞态白名 + 单之外的任何错误,因此只有瞬时的文件系统竞态仍会静默降级。 + `LocalSandbox.list_dir`/`glob` 把 `PermissionError.filename`/`filename2` 反向 + 解析回容器路径,让 `ls_tool`/`glob_tool` 以 `Error: Permission denied:` 报告被 + 拒绝的路径,而不是总是点名请求的根目录,自定义挂载的主机路径因此不会出现在消息 + 里。([#7234]) + +- **反思:** `resolve_variable` 现在会在尝试任何导入或诊断之前,以路径格式的 + ImportError 拒绝畸形的变量路径。此前 `"json:"` 会走到 `getattr(module, "")` 并 + 报告 "Module json does not define a attribute/class";`"json:loads:extra"` 把 + 多余的冒号留在模块一侧,被误诊为缺失依赖,并给出不可用的 `uv add json:loads` + 提示;前导点路径会因 `import_module` 抛出的 `TypeError` 而崩溃(调用方只捕获 + `ImportError`);杂散空白或末尾的点会让渲染出的提示给出 `json.`、`json ` 这样 + 的发行版名。这些形态现在都得到同样的格式指引;格式良好的路径不变。([#7016]) + +- **脚本:** `scripts/run-with-git-bash.cmd` 现在从 Git 安装根目录定位 Git + Bash,而不是假定 `git.exe` 的父目录持有 `bin\bash.exe`:包装脚本现在还会向上探 + 测一层目录,因此把 `git.exe` 放在 `<root>/mingw64/bin` 的 PortableGit 与 + GitHub Desktop 布局能找到 `<root>/bin/bash.exe`,不再报告 "Could not locate + Git for Windows Bash" 并破坏每一个运行仓库 shell 脚本的 Makefile 配方。 + `backend/tests/support/shell.py` 镜像同样的双根探测(`_git_roots`),shell 脚 + 本测试套件在这些主机上不再跳过。([#7214]) + +- **调度器:** 在 `ScheduledTaskService._is_overlap_conflict` 中仅把 + `ConflictError` 与 `BusyThreadConflict` 视为可重试的重叠冲突。`start_run` + 对幂等键不匹配、线程/助手不匹配这类永久性失败同样会抛出普通的 HTTP + 409,而调度器此前会对任何 409 重新排队该次执行,重试一个永远无法成功的 + 启动,直到 `scheduler.queue_timeout_seconds` 超时;现在普通 409 会把该 + 次执行终结为失败,并把 409 原因持久化为失败详情。`BusyThreadConflict` + 改为在分类器内部导入,调度器模块因此在加载时不再依赖 + `app.gateway.services`。([#7196]) + +- **图片搜索:** DuckDuckGo 图片搜索失败现在作为搜索错误上报,而不是返回 + 空结果:`_search_images` 把异常传播到异步边界,由 `image_search_tool` 返回 + `{"error": "DuckDuckGo image search failed: <type>: <message>", "query": ...}`, + 只有返回空集合才会得到 `No images found`。此前每种失败(缺少 `ddgs` + 安装、超时、限流)都只记入日志并被吞成 `[]`,模型会把一次失败的搜索 + 当成查询没有匹配图片的证明;SDK 的 “No results found.” 哨兵仍按错误 + 处理,因为 DDGS 9.14.1 在后端错过分发器时限时也会抛出它。([#6680]) + +- **MCP:** 控制认领在整个 poll 或 cancel 调用期间保持存活:每个控制操作 + 在调用驱动程序之前先通过新增的 `renew_control_claim` 校验并延长自己的 + 认领,并按 `mcp_tasks.lease_seconds` 的三分之一随心跳续约。此前,慢于 + 认领租约的状态或取消请求,其快照应用会以过期为由被拒绝——终结性结果 + 随之丢弃——另一个 worker 还可能在半路重新认领该行。现在,失败、超时或 + 被拒绝的续约会终止本地调用;属主丢失围栏(`_require_control_lease`) + 阻止会吞掉取消的驱动程序保存后续快照或重试失败; + `_apply_cancel_snapshot` 则丢弃针对已丢失租约写入的结果。取消认领事务 + 以 `BEGIN IMMEDIATE` 取得 SQLite 写锁,两个延迟扫描便无法认领同一条 + 到期行;排空中的调用保留自己的容量槽位,并在子任务落定之前经由 + `exclude_task_ids` 排除在后续认领之外。没有 schema 迁移或新增配置; + `mcp_tasks.lease_seconds` 仍是崩溃恢复窗口,而不是远程调用超时。 + ([#6780]) + +- **渠道:** 渠道线程绑定存储对无效 UTF-8 的恢复方式与畸形 JSON 相同: + `backend/app/channels/store.py` 中的 `_read_legacy_file` 现在会捕获 + `UnicodeDecodeError` 并套用调用方的 `on_corrupt` 策略,因此 SQL 迁移会 + 警告并跳过该文件(保持文件原样,让渠道得以启动),内存 JSON 存储则 + 警告后以空状态启动,并在下次写入时替换该文件。此前无法解码的 + `channels/store.json` 会以未处理的 `UnicodeDecodeError` 冲出存储加载, + 而不是降级运行。([#7233]) + +- **harness:** Lark 设备授权改由一个归流程代际所有的 single-flight 登录 + 运行完成:第一个 `/lark/auth/complete` 轮询为每个 (user_id, generation) + 启动一个后台 `lark-cli auth login`,后续轮询附加到该运行上,因此 + `wait_timeout_seconds` 只约束调用方的等待(504 契约不变),绝不会杀掉 + 子进程。此前每次轮询都在该超时下运行 CLI,若在一次性设备码已兑换之后、 + 凭据持久化之前杀掉进程,整个流程就会搁浅——之后每次轮询都以 + “device_code is invalid” 失败。已结束运行的结果在 120 秒内仍可附加; + 推进代际或重发设备码会放弃旧进程;持久化的标记/结果文件(属主 pid 加 + 启动时间,附 10 秒的属主存活发布宽限)让另一个 worker 能附加到已完成的 + 尝试,而不是重新拉起一个已兑换的设备码。凭据树遍历能容忍在加固中途 + 消失的条目,结果发布在凭据锁下串行化。([#7182]) + +- **浏览器:** Browser Live 视图转发按键时保留 Shift 和 Alt 修饰键: + `browser-view-panel.tsx` 现在在 ctrl/meta 之外同时传递 `shiftKey` 和 + `altKey`,`decideBrowserKeyInput` 则用 `keyChord` 为单字符组合键和转发 + 的命名按键组装完整组合键。此前组合键会被折叠成单独的 + `Meta+X`/`Control+X`,命名按键则被裸转发,Shift+Enter 或 Alt+Arrow 这 + 类快捷键在到达页面之前就丢了修饰键。([#7213]) + +- **脚本:** `scripts/cleanup-containers.sh` 现在只停止名字确属沙箱的 + 容器。Docker 的 `name=` 过滤器匹配子串, + `--filter "name=deer-flow-sandbox"` 可能停掉名字里恰好含有这段文字的 + 无关容器;脚本现在查询 `deer-flow-sandbox-`,并在停止前只保留匹配锚定 + 的 `"$PREFIX"-*` 模式的名字;Apple Container 路径改用 `startswith` + 匹配 `configuration.id`,不再做子串测试,并经由环境变量传递前缀, + 而不是字符串插值。名字中其他位置带有该前缀的容器不会被触碰。 + ([#7189]) + +- **工作流:** 顺序 Agent 交接的任务输入现在可以使用与文件引用任务相同 + 的 16,000 序列化 JSON 字节上限(即 + `examples/deerflow-extension-workflows/.../service.py` 中的 + `AGENT_TASK_BYTES`);普通 Agent 任务此前上限 8,000 字节,无法在附带 + 一条简短指令的情况下把已保存的 12,000 字节步骤输出传给下一个 Agent。 + 步骤输出仍以 12,000 字节为上限(`AGENT_OUTPUT_BYTES`),JSON 转义计入 + 两个上限,超限任务仍会在准入前被拒绝。([#7231]) + +- **GitHub 研究:** `get_readme` 与 `get_file_content` 在抓取失败时现在 + 抛出 `GitHubAPIError`,而不是返回 `[README not found: HTTP 404]` 这类 + 标记字符串。此前每次失败都以字符串形式到达调用方,由 `main()` 打印到 + stdout 并以退出码 0 结束,限流运行、仓库缺失与真正的 README 正文在 + stdout 阅读者看来无法区分。现在失败会进入 CLI 既有的异常处理器,在 + stderr 上指明操作、`owner/repo` 目标、文件路径和 HTTP 状态,并以退出码 + 1 结束,成功通道保持为空;成功抓取的行为不变,403 限流不再被误读为 + 仓库缺失。`requests` 传输与 `urllib` 回退以相同方式抛出。([#7221]) + - **MCP:** 使用 OAuth `refresh_token` 授权的 MCP 服务器,在提供方轮换 refresh token (Auth0、Okta、Google)时,跨 Gateway 重启、工具缓存重置、个人连接的多次运行以及多个 Gateway 实例都能继续工作。此前令牌只保存在进程内存中,每个新进程或新 manager 都从配置里 @@ -1277,6 +2758,30 @@ `refresh_token` OAuth 服务器时,缺少 `DEER_FLOW_CREDENTIALS_KEY` 现在会拒绝启动。 ([#7207]) +- **渠道:** 每个 IM 渠道 JSON 状态写入都固定 `encoding="utf-8"` 与 + `newline=""`——涵盖 `JsonChannelStore._save`、 + `ChannelRuntimeConfigStore._save`、`BuzzSeenEventStore._write_snapshot`、 + Discord 线程映射以及微信游标和认证状态写入器——并以显式 UTF-8 编码 + 读回 Discord 的线程存储。此前这些调用继承平台 locale 与换行翻译, + Windows 上持久化的状态带着 CRLF 行尾和按 locale 编码的文本,非 ASCII + 的渠道、线程和用户 ID 在 locale 不同的主机上可能无法编码或解码。新增 + 一个基于 AST 的测试,在各写入器/读取器调用点所属的方法内固定这些 + 关键字参数,未固定的调用无法藏在无关的模块级设置后面。([#7228]) + +- **配置:** `doctor.main` 与 `setup_wizard.main` 在开头以 + `errors="replace"` 重新配置 stdout,保留该流的原有编码,让旧式 locale + 终端和管道(Windows cp936、ASCII)能完成健康报告与安装流程,而不是在 + `✓` 这类状态符号上以 `UnicodeEncodeError` 中止;`reconfigure` 调用对 + 缺少该方法的内嵌流做了保护。`configure.py` 还从成功行中去掉了 `✓`, + 新增的子进程测试在 ascii、cp1252、cp936 和 utf-8 stdout 下驱动这三个 + 脚本。([#7102]) + +- **渠道:** 钉钉上传同步完成后释放独立临时沙箱:向共享的 `sandbox_files` + 同步传入 `release_on_last=True`,与飞书一致——最后一次沙箱操作之后 + 释放执行持有者,改为向提供方请求释放,而不是滞留沙箱。此前钉钉保留 + 其不释放的持有者行为,每次向未挂载的沙箱上传都会留下一个独立临时 + 沙箱。挂载沙箱与面向并行运行的共享客户端保护保持不变。([#7203]) + - **社区工具:** 拒绝 LightRAG 与 RAGFlow 的 `knowledge_search` 数值设置中的 布尔值,不再静默把 `true` 当作一秒超时、一条结果或一个字符的输出。无效设置返回 现有的配置错误。([#7042]) @@ -1780,26 +3285,26 @@ 两个实例为同一会话的第一条消息各自创建线程时,绑定现在以“不存在才插入”的方式在 `channel_conversations` / `channel_thread_bindings` 中认领:落败的实例改用胜出者的线程 并删除自己创建的空线程,`/new` 仍会重新绑定到新线程。 (issue #7103, [#7104]) - + - **技能:** agent 通过 `skill_manage` 工具创建、编辑、修补或删除的技能,现在会像通过 技能 API 所做的修改一样,传达到共享同一 home 卷的每个 Gateway 进程。此前该工具只刷新 本进程的技能提示缓存,对端副本会继续为该用户渲染旧的技能列表;现在它在本地刷新之后、 于事件循环之外发布同一个按用户限定的共享缓存重置标记。写入、历史记录、刷新与标记作为 一个整体完成,因此写入途中被取消的运行不再让本进程或对端继续使用过期列表。标记无法写入 时,工具会报告修改已生效但未能通知其他进程,而不会让人误以为写入失败。 ([#7099]) - + - **provisioner:** 两个 provisioner 副本在 `K8S_NAMESPACE` 尚不存在时同时启动,输掉 创建竞争的一方不再在启动时崩溃:它收到的 `409 AlreadyExists` 现在视为命名空间已存在, 与 Pod 和 Service 的创建一致。Helm 部署中 provisioner 运行在其管理的命名空间内,从不 经过这条路径。 ([#7099]) - + - **项目:** 向调用者无权写入的项目上传书架文档或从对话保存文件时,不再遗留目录, 也不再返回 `500`。这两个路由此前先把请求体暂存到 `users/{user_id}/projects/{project_id}/` 下才检查项目,因此不存在、属于他人或已归档 的 id 会留下一个回收站对账永远不会清理的空 `{project_id}/documents/` 目录,而超过单个 路径分量长度的 id 会因 `ENAMETOOLONG` 失败。现在会先确认项目归调用者所有且处于活跃 状态,不写任何内容即返回同样的 `404`;书架插入仍会在项目行锁下再次检查。 ([#7040]) - + - **skills:** 在尚无 `extensions_config.json` 时切换技能开关,现在会在项目根目录 (`DEER_FLOW_PROJECT_ROOT`,未设置时为工作目录)创建该文件,这与 MCP 编辑器已有的 创建位置一致,也是配置解析最先查找的位置。此前技能开关使用工作目录的父目录:当 @@ -1815,7 +3320,7 @@ 交换帧(仍每隔 20 ms 检查取消、故障和 worker 退出),因此空闲等待不再消耗交互次数, `timeout_seconds` 成为唯一的实际耗时上限;持续驱动作业队列的脚本仍受交互次数上限 约束。(issue [#7022], [#7024]) - + - **浏览器:** `browser_screenshot` 不再替换线程 outputs 目录中已存在的文件。此前以 `report.png` 保存的截图会覆盖用户自己的 `report.png`、更早的截图,或同一秒内生成的 另一张默认命名截图;现在会像 `web_capture` 一样改用下一个空闲名称(`report-1.png`、 @@ -1824,6 +3329,7 @@ 上传相同,outputs 目录需要支持硬链接;不支持时工具会报错,而不会覆盖已有文件。发布前 被替换的暂存截图(例如换成指向宿主机文件的符号链接)绝不会被链接到 outputs,工具会 报错。 ([#6913]) + - **llm:** 流式响应已返回 `200` 之后才在流内报告的 provider 错误,现在会像以 HTTP 状态码返回的同一错误一样重试。为了用 SSE keepalive 维持慢请求而提前返回 `200` 的 OpenAI 兼容网关,会把之后发生的 504 作为 SSE `error` 事件发出;openai SDK 将其抛出为 @@ -2735,22 +4241,26 @@ 两者合成一行无法解析;被截断的多字节字符还会让整个文件解码失败,所有完好记录都读不出来,序号恢复也随之失效。现在追加前 若文件末尾不是换行会先补一个分隔符,读取时逐个物理行单独解码、只跳过损坏的那一行,批量追加失败时仍会截回原始大小。 ([#6520]) + - **渠道:** 在启用 Gateway 认证(默认配置)时,从 IM 渠道发送的 `/goal <目标>` 和 `/goal clear` 恢复正常。此前这两个写请求只携带内部认证令牌,而 Gateway 的 CSRF 检查不会 豁免内部认证,因此返回 403,渠道回复“Failed to set goal.”或“Failed to clear goal.”。 现在它们会发送与渠道 SDK 客户端相同的 CSRF Cookie 和请求头。`/goal` 状态查询不受影响。([#6537]) + - **认证:** 在 Docker 部署中,某个客户端输错 5 次密码不会再让所有用户 5 分钟内无法登录。登录失败按客户端 IP 计数,而 Gateway 只在 TCP peer 属于 `AUTH_TRUSTED_PROXIES` 时信任 `X-Real-IP`,compose 文件却从未设置该变量;所有浏览器请求都经由 `nginx` 容器到达 Gateway,因此所有登录共用 nginx 的地址和同一个锁定。现在 `AUTH_TRUSTED_PROXIES` 也接受主机名,在事件循环外解析并 缓存 10 秒(包括解析失败),两个 compose 文件默认将其设为内置的 `nginx` 服务。nginx 在每个转发到 Gateway 的路由上都用 `$remote_addr` 覆盖 `X-Real-IP`,客户端无法自选地址。`.env` 中设置的 `AUTH_TRUSTED_PROXIES` 仍然优先,`make docker-start` 也是如此:它现在会像代理变量一样导出该值供 Compose 插值。位于其他反向代理之后的 部署还需要为该代理配置 nginx 的 `real_ip` 模块,详见 `.env.example`。([#6519]) + - **调度器:** 在一次性任务的执行时间之前点击“立即运行一次”,不会再取消原定的执行。此前这次试运行被当作任务本身的 执行:任务被标记为 `running`,试运行结束后又按其结果把任务终结为 `completed`、`failed` 或 `cancelled`。轮询器只认领 `enabled` 的任务,因此到了 `run_at` 也不会执行,尽管 `next_run_at` 仍显示该时间。现在在执行时间之前启动的试运行 不会改变任务的状态和 `next_run_at`,与周期任务的试运行一致,其结果只记录在试运行自己的运行记录上。执行时间已过之后 的试运行仍算作任务本身的执行。若试运行的启动记录在轮询器认领已到期的任务之后才写入,也不会再清除该认领的租约;此前这会让 该认领的入队失败,任务停留在 `running` 且没有任何待执行的运行。([#6512]) + - **认证:** 登录锁定现在按客户端 IP 在所有 Gateway 副本间只计一次。`POST /api/v1/auth/login/local` 此前把失败计数 存在进程私有的字典里,因此同一个负载均衡器后挂 N 个副本时,攻击者能获得 N × `max_login_attempts` 次尝试机会, 一个副本上的锁定对其他副本不可见。该计数现在由 `LoginThrottleStore` 管理:新增的 `auth.local.throttle_storage` @@ -2778,6 +4288,7 @@ - **技能:** `/技能名` 激活现在在模型调用重试时不再丢失。此前激活在调用模型之前就被标记为已完成,因此调用失败 (限流、过载、超时)或返回空响应而重试时,重试请求不再包含 `SKILL.md` 正文,而该技能的工具限制仍然生效。现在重试 会携带与首次尝试相同的激活内容,不会重新读取技能或重复记录激活,重试得到的响应也保留技能使用记录。([#6506]) + - **技能:** 通过一个 Gateway 副本做出的技能变更,现在会进入其他所有副本系统提示词中的技能列表。`SkillStorage` 每次 调用都会重新扫描磁盘,但提示词层按进程缓存已启用技能列表和渲染好的 `<skill_system>` 段落,而安装、编辑、删除、 回滚或启停技能(以及 `POST /api/skills/reload`)只刷新处理该请求的进程,因此在多个 uvicorn worker 或多个共享同一 @@ -2794,6 +4305,7 @@ 所配置数据库的 `threads_meta` 表读取;若存在 legacy 线程但无法读取该表,脚本会在移动任何数据前退出。对于 从未记录过线程归属的安装,可传 `--allow-missing-thread-owners`,此时所有 legacy 线程归入 `default`。 已运行过旧脚本的安装可按 `docker/provisioner/README.md` 中的步骤恢复线程。([#6450]) + - **沙箱:** e2b 沙箱中失败的命令现在会保留输出和退出码。e2b SDK 在退出码非零时抛出 `CommandExitException`,而不是返回结果,因此 `E2BSandbox.execute_command` 返回的是 `Error: Command exited with code N and error: ...`:stdout 被丢弃,`Exit Code: N` @@ -2802,6 +4314,7 @@ 与退出码,现在会按返回结果的方式格式化。stderr 中恰好包含 "sandbox not found" 的失败命令 也不再把沙箱标记为已回收。e2b 的 `list_dir` 也会处理同一异常:目录不存在时抛出 `FileNotFoundError`,超过 500 条被截断(SIGPIPE 141)的列表会正常返回,不再以 `OSError` 失败。([#6441]) + - **上传:** Gateway 启动时对孤立的 `.upload-*.part` 暂存文件的清扫现在会跳过 24 小时内的新文件。此前清扫会删除找到的 每一个暂存文件,这对单个 Gateway 没问题,但对共享同一 home 卷的多个副本不成立:滚动更新期间启动的副本会删掉另一个 副本仍在写入的上传的暂存文件,该上传随后在原子提交时失败。分块写入会刷新暂存文件的 mtime,因此进行中的上传始终 @@ -2821,27 +4334,33 @@ 引用、对话引用、已暂存的项目文件和已保存的草稿,因此上传失败后文字和文件虽仍在,重试发送却缺少这些上下文。 现在这些一次性状态只在发送真正派发(上传完成)后才清除;若上传完成时用户已切换对话或离开页面,只清除该次发送 携带的已保存草稿和暂存文件,保留此后保存的草稿或附加的文档。([#6412]) + - **客户端:** `DeerFlowClient.list_threads(limit)` 现在限制的是线程数而不是检查点数。此前 `limit` 被传给跨所有线程的 检查点扫描,而一轮对话会写入多个检查点,因此一个较长的对话就会占满上限:TUI 线程选择器和 `--resume <标题>` 只能看到 最近一两个线程,较早线程的标题无法解析。现在 SQLite 和 Postgres 通过检查点索引列出线程,每个返回的线程只加载首个和最新 检查点;Gateway 分支也会列出,排序按检查点写入顺序,因此仅写入目标也算作活动。新增的 `sort_by="updated_at"` 选项让 `--continue` 继续恢复最近活跃的线程;默认顺序仍为按创建时间从新到旧。([#6426]) + - **客户端:** TUI 和内嵌 `DeerFlowClient` 在 SQLite 和 Postgres 检查点后端上现在可以正常使用目标(goal)。这两种 同步检查点保存器定义了异步方法,但调用时会抛出 `NotImplementedError`,而目标读写只要异步方法存在就会调用它,因此 `/goal` 只会显示 "Could not set goal.",`get_goal`/`set_goal`/`clear_goal` 则直接抛出异常。现在目标读写在这类保存器上 会改用同步方法。Web UI 不受影响。([#6448]) + - **community:** Browserless 的 `web_fetch` 提供方现在可以通过认证。此前它把配置的令牌放在 `/content` 请求的 JSON 正文里, 而 Browserless 只从 `token` 查询参数或 `Authorization` 请求头读取令牌,并在读取正文之前完成校验。按照配置指南设置 `BROWSERLESS_TOKEN` 后,每次 `web_fetch` 都返回 `Browserless HTTP 401`(对未设置 `TOKEN` 启动的实例则因正文模式拒绝 未知字段而返回 HTTP 400),而使用同一令牌的 `web_capture` 正常工作。现在两个工具都以查询参数发送令牌。([#6484]) + - **mcp:** 配置了 `task_toolsets` 的 MCP 服务器在工具发现时无法连接或超时,不再导致所有 MCP 工具丢失。此前发现阶段以空 工具列表跳过失败的服务器,随后任务工具集检查把它的 submit、status、cancel 工具报告为缺失,抛出的错误连同所有正常服务器 的工具一起丢弃;由于缓存从未发布,每次构建智能体都会重新发现全部服务器,重启 stdio 服务器并重新请求 OAuth 令牌。现在发现 失败的服务器与其他失败服务器一样被跳过;已应答但缺少所配置工具的服务器仍按配置错误处理。([#6481]) + - **前端:** 侧边对话发送失败时不再清空输入框。侧边对话的提交处理在弹出错误提示后仍以成功返回,输入框据此视为成功, 因此在创建侧边对话或上传附件失败时,已输入的文字和附件都会丢失;发往新侧边对话的第一条消息也会在排队时(实际发送前) 就被清空。现在提交处理会在提示后抛出错误,排队的首条发送也以其自身结果完成提交,因此草稿会保留以便重试,只有消息 实际发出后才会清空。([#6407]) + - **沙箱:** AIO 沙箱在凭据放置与容器复用上不再信任未经验证的状态。provisioner 能力探测失败不再被解读为“broker 关闭” (此前会在负缓存 TTL 期间把明文 lark 凭据目录 bind-mount 进无 broker 的 Pod);broker 模式按 Pod 依据 provisioner 的 原子观测加以证实;lark 供应配置冲突会携带能力刷新标记,使部署变更在下一次 acquire 时自愈;隐式 shell 结局不明确的 @@ -2853,19 +4372,23 @@ 因此实时流在再次刷新前一直无法恢复。现在该重连失败时(包括流中途断开),恢复逻辑会释放指针; 若服务端仍报告该运行处于活动状态,则按现有的有限重试(立即一次,随后 1s、2s)重新加入。 提交失败的运行行为不变。([#6400]) + - **渠道:** 在 IM 渠道会话的运行尚未结束时从网页打开它,用户消息不再显示两次。渠道运行输入此前不带消息 id, Gateway 在运行记录中按原样保存无 id 的消息,而检查点中的副本会被分配新的 uuid;网页客户端重连时按 id 合并这两份副本,因此在运行结束前两条都会显示。现在渠道发送的人类消息自带 id,两份副本共享同一 id。([#6401]) + - **部署:** 在全新检出的仓库上,`make up`、`make down` 与 `make prod-logs` 不再因 `env file .../.env not found` 而中止。`.env` 与 `frontend/.env` 已被 gitignore,`make up` 也不会创建它们,但生产 compose 文件此前要求两者都存在。 现在其 `env_file` 条目与开发 compose 文件一样为可选:文件存在时加载,不存在时跳过。此写法需要 Docker Compose 2.24 或更高版本,即 README 已注明的最低版本。([#6370]) + - **记忆:** 安装 jieba(`memory-zh` 扩展)时,DeerMem 相关度排序不再把标点计为查询词。 `tokenize()` 只过滤了 `jieba.cut` 输出中的空白,而 jieba 会把 `,`、`。`、`,`、`!` 切成独立 token,因此同样含逗号的查询与无关事实也会得到非零分,近似去重的相似度被抬高, 标点还会在后续查询词之前耗尽 128 个 token 的预算。现在不含字母或数字的 token 会被丢弃, 与无 jieba 的回退分词及 FTS5 查询过滤保持一致。([#6388]) + - **网关:** 经过会压缩响应的代理时,运行流现在会逐帧到达。所有 SSE 响应 (`POST /api/threads/{id}/runs/stream`、`GET .../join`、 `GET`/`POST .../runs/{run_id}/stream` 与 `POST /api/runs/stream`)改由同一个 @@ -2873,6 +4396,7 @@ 因此会压缩响应的代理(例如不经 nginx、直接 `pnpm start` 时的 Next.js 改写代理) 不再先缓冲整段流、再成批送出。`X-Accel-Buffering: no` 与 `Content-Location` 保持不变,使用 nginx 的部署不受影响。([#6393]) + - **配置:** 共享同一份 `extensions_config.json` 的每个 Gateway 进程,现在都能看到其他进程做出的 MCP 与技能变更。此前 解析结果按进程只缓存一次,只有处理写入的进程会重新加载,因此在多个 uvicorn worker、或共享卷上的多个 Pod 之下, 其他进程一直沿用启动时的副本:智能体可用的 MCP 服务器,以及更重要的、由文件系统 MCP 服务器推导出的 local-bash @@ -2890,16 +4414,19 @@ `Command exited with code N`;存储不可用时的回退截断也像沙箱截断一样,从 `fallback_max_chars` 预算中为它预留位置。仅影响 `bash` 与 `bash_tool` 的结果, 落盘的完整输出保持不变。([#6354]) + - **make:** `make clean` 现在会说明它删除的内容,并拒绝在运行中的 Docker Gateway 下执行。`make help` 此前称其清理"临时文件",但它实际删除 `backend/.deer-flow`:本地数据库、用户、线程、上传、记忆和密钥。 两套 Docker 栈都把该目录挂载进 `deer-flow-gateway` 容器,而 `make stop` 不会停止它,因此数据可能在 Gateway 运行时被删除。现在帮助文本和删除前的提示会写明该目录;当该容器仍在运行时,`make clean` 会在停止任何服务之前退出,并提示先执行 `make down` 或 `make docker-stop`。([#6351]) + - **TUI:** 新的提示不再与同一会话中被中断的运行发生竞争。按下 `Ctrl+C` 后,该运行的工作线程会 一直执行到当前步骤返回,因此正在进行的工具调用(例如耗时较长的 shell 命令)仍会完成并写入检查点, 而下一条提示已在同一线程上运行;最后写入检查点的运行会成为会话历史,新的一轮可能因此悄然消失。 现在在被中断的工作线程返回之前,在该会话中发送提示会显示提示信息而不会启动第二个运行; 其他会话仍可通过 `/new` 和 `/resume` 使用。([#6350]) + - **Helm:** 默认的 Helm 安装现在又能创建沙箱了。chart 启用沙箱 provisioner 并把 `config.sandbox.provisioner_url` 指向 它,但没有任何环节渲染 `PROVISIONER_API_KEY`:provisioner Deployment 没有该环境变量,`<release>-app` Secret 没有该 键,内嵌配置也没有 `provisioner_api_key`。自 provisioner 开始强制要求该密钥(#4116)以来,只要密钥为空或不匹配,其 @@ -2915,6 +4442,7 @@ 该视图只在 host bash 关闭时维护,但 `LocalSandboxProvider` 只要它存在就会挂载,导致该线程之后 不受限的运行仍沿用旧的 allowlist,读不到 `/mnt/skills` 下其他已启用或新增的技能。现在这些运行 使用共享技能视图;线程视图会保留,并在关闭 host bash 后重新生效。([#6344]) + - **沙箱:** 本地沙箱不再改写换行符。`read_file` 此前把 CRLF 转成 LF,导致 `str_replace` 编辑 CRLF 文件后整份文件都变成 LF,且读后写校验无法察觉仅改动换行符的 变更;在 Windows 上 `write_file` 会把 LF 内容写成 CRLF,使 `bash run.sh` 等脚本失败。 @@ -2923,23 +4451,27 @@ 本地 `grep` 与 `read_file` 一样只在 `\n` 处断行,即使文件含有单独的 `\r`,命中的行号也与按行号范围读取的结果一致。 同样在 Windows 上,保存到 `outputs/.tool-results/` 的超长工具输出此前也被写成 CRLF,与其 blob 引用不再一致,未配置 blob 存储时会在下一次模型调用时被删除;现在按原样逐字节写入。([#6343]) + - **技能:** 编辑自定义技能时,不再在事件循环上执行文件系统操作。 `PUT /api/skills/custom/{name}` 此前在事件循环上构建用户级技能存储、探测自定义、 内置、旧版共享与集成目录、把草稿写入临时目录以校验 frontmatter,并读取将被替换的 内容;只有最后的写入与历史追加已移出事件循环。现在这些步骤都在工作线程中执行, 与回滚路由一致。自定义技能的删除与归档安装路由,以及智能体的 `skill_manage` 工具(其存储查找每次调用都会 stat `config.yaml`)也改为在工作线程中构建存储。([#6332]) + - **网关:** 知识检索目录加载自定义 Agent 配置时不再阻塞网关事件循环。 `GET /api/knowledge/retrieval-catalog/datasets` 与 `.../datasets/{id}/documents` 此前在事件循环上通过同步 Agent 存储读取 Agent:`file` 后端为文件 IO,`db` 后端 为一次同步 SQLAlchemy 往返,磁盘或数据库变慢时会拖住所有其他请求。现在与其他 读取 Agent 配置的网关路由一样,在 `asyncio.to_thread` 中加载;响应内容以及未知 Agent 返回的 404 保持不变。([#6313]) + - **网关:** 删除工作区较大的线程时,移除文件期间不再冻结 Gateway 的其他所有请求。 `DELETE /api/threads/{id}` 此前在事件循环上对线程目录执行 `shutil.rmtree`, 因此其他请求与进行中的 SSE 流都要等到整棵目录树删除完毕(本地 SSD 上 20,000 个 小文件约 0.7 秒,挂载卷上更久)。现在移除在文件 IO 线程池中执行;请求被取消时, 线程预留会保持到移除完成,因此文件仍在删除的线程上不会启动新的运行。([#6319]) + - **沙箱:** 中风险审计警告不再让子智能体证据丢失失败的 shell 退出码。 `SandboxAuditMiddleware` 原先把警告追加在结尾的 `Exit Code: N` 标记之后,并只用 四个字段重建 `ToolMessage`,导致 `_bash_evidence_status` 找不到标记,退回到报告 @@ -2947,6 +4479,7 @@ 验收条件。现在警告插在结尾的 `Exit Code: N` 或 `Command exited with code N` 之前,整段只有 `Command exited with code N` 的输出保持原样,结果保留 `deerflow_tool_meta`、`artifact` 与 `id`。([#6307]) + - **部署:** Helm chart 与 docker-compose 不再自带多副本陷阱。chart 的默认 `config` 启用 `run_ownership.heartbeat_enabled` 和 `run_events.backend: db`(这样启动中的 Pod 只会认领属主确实已消失的运行,消息历史跨 Pod 共享);按 `gateway.replicas` 或显式的 `gateway.multiInstance` 为 Gateway 的启动门槛导出 `DEER_FLOW_MULTI_INSTANCE`(用 `kubectl scale` 或 HPA 扩容前 @@ -2977,6 +4510,7 @@ 30 秒)同样作用于这条语句,因此任何超过该时限的迁移都会让等待中的实例中止。 现在获取改为轮询非阻塞的 `pg_try_advisory_lock`:等待时长与持锁方的迁移一致, 每次尝试仍受 `command_timeout` 约束,等待只记录一次日志。([#6306]) + - **项目:** 在 SQLite 上首次读取书架文档时,文档转换期间不再阻塞所有其他数据库 写入。此前懒转换在 `BEGIN IMMEDIATE` 事务内运行 pymupdf/markitdown,以便与移入 回收站和彻底删除串行化发布;而 SQLite 的这把锁作用于整个数据库,运行状态、线程 @@ -2984,6 +4518,7 @@ 现在转换在任何事务之外写入 `.staging/`,只在重新校验文档行并原子重命名输出时 持锁,期间被移入回收站或彻底删除的文档仍不会发布任何内容。同一文档的并发首次读取 共享一次转换,不再各自占用文件 IO 工作线程。([#6305]) + - **网关:** 单次运行的读取现在能返回 IM 渠道所有者的数据。`start_run` 用原始 受信所有者 ID(例如 `feishu:owner-777`)标记运行行与运行事件,但多个运行级路由 按内部调用方规范化后的 ID 过滤,因此在 SQL 存储上,只要所有者 ID 含有 @@ -2993,6 +4528,7 @@ 源运行查找退回兜底路径或返回 409。这些读取现在使用与线程消息路由(#5448)相同 的数据身份,所有事件存储的 `list_messages_by_run()` 都接受 `user_id`。浏览器与 API 会话仍保留按用户过滤。([#6282]) + - **运行时:** 多 worker 部署中已成功结束的运行,不会再在其 worker 仍在收尾时被 当作孤儿运行回收为 `error`。配置了事件存储时,worker 先在内存中记录终态,直到 journal 刷新、交付回执、工作区扫描与时长 checkpoint 写入完成后才写入运行存储。 @@ -3000,6 +4536,7 @@ 30–40 秒),其他 worker 或本 worker 自己的回收器就会接管这条仍处于活动状态的 记录。心跳现在会持续续期直到这次延迟写入被执行,若期间被其他 worker 接管则隔离 本地运行。仅影响启用 `run_ownership.heartbeat_enabled` 的部署。([#6263]) + - **渠道:** 通过 `/connect` 绑定的 Buzz 作者在已参与的话题中回复时,无需再次提及 机器人。开启 `channel_connections.enabled` 后,管理器只在连接仓库中记录已绑定作者 的话题映射,而 Buzz 的话题跟随判断只读取 JSON 渠道存储,导致机器人正在回复的话题 @@ -3008,6 +4545,7 @@ 将复用的话题,旧的 JSON 映射也不再对该作者生效。管理器的斜杠技能白名单检查中有同一读取逻辑 的副本,也一并修复:尚无话题的已绑定用户此前会按同一会话旧 JSON 话题的智能体进行检查, 可能被告知已启用的技能不可用。([#6232]) + - **记忆:** 读取 DeerMem 智能体记忆时,不再因另一写入同时删除事实而失败。 `load()`、`reload()` 与全量 `rebuild_index()` 扫描在不持有存储锁的情况下列 出事实文件,若删除恰好在列出之后、打开文件之前提交,就会对完好的数据抛出 @@ -3015,11 +4553,13 @@ corrupted"),提示词注入在该轮丢弃整个记忆块(在 `failure_policy.read: fail_closed` 下则使运行失败),全量索引重建则把该事实计为失败。列出后消失的事实现在被视为已删除;仍然存在但无法读取的条目(例如悬空符 号链接)仍会报告为损坏。([#6255]) + - **记忆:** DeerMem 记忆重新加载不再把旧文档固定在缓存中。`reload()` 此前先读取文档、后计算缓存签名,若两者之间有写入提交(例如后台记忆更 新器),旧文档就会以新签名写入缓存,之后每次 `load()` 都返回过时的记 忆,直到下一次写入。`reload()` 现在与 `load()` 一样先计算签名,竞争写 入只会触发重新读取。([#6238]) + - **渠道:** Discord 的渠道连接数据库操作现在在 Gateway 事件循环上执行。 discord.py 在客户端线程的私有事件循环上投递消息,而 Discord 适配器此前就在该 循环上 await 连接仓库,但仓库的 SQLAlchemy 引擎与连接池属于 Gateway 循环。在 @@ -3040,6 +4580,7 @@ 应答可以携带的 IPv4 映射形式 `::ffff:100.100.100.200`。原有的标志位检查仍然保留, 因为部分非公网形式(例如元数据地址的 NAT64 写法)依然被判定为全局地址。有意通过这些 工具访问 tailnet 或 CGNAT 主机的运维人员现在需要设置 `allow_private_addresses: true`。([#6202]) + - **浏览器:** 智能浏览器不会再因为 SSRF 检查之后发生变化的 DNS 应答而被引向 内网或云元数据主机。导航检查和逐请求守卫会解析主机名进行筛查,但 Chromium 建立连接时会再次解析,因此重绑定 DNS 服务器可以对检查返回公网地址、对连接返回 @@ -3048,11 +4589,13 @@ 并且只连接筛查通过的地址。回环流量同样经过代理。WebRTC UDP 不经过代理,不在覆盖范围内。 通过 CDP 连接的 Chrome 不受影响; 委托抓取服务(crawl4ai、Browserless、fastcrw)仍在其自身一侧解析,Gateway 无法固定。([#6201]) + - **渠道:** Discord 现在会在智能体生成回复期间真正显示"正在输入"提示。`_start_typing()` 调用的 `channel.trigger_typing()` 已在 discord.py 2.0 中移除(项目要求 `>=2.7.0`),而其循环吞掉了 所有异常,因此每次都抛出 `AttributeError`,提示从未发送。现在改为 await 2.x 的 `channel.typing()` 发送一次提示。每个输入提示循环的首次失败以 WARNING 级别记录(缺少权限或持续限流 在默认日志级别下即可见),之后的失败以 DEBUG 级别记录,而不是直接丢弃。([#6138]) + - **社区工具:** SSRF URL 校验在解析主机名时不再阻塞 Gateway 事件循环。 `validate_public_http_url` 通过阻塞的 `socket.getaddrinfo` 解析主机名,而 crawl4ai 与 Browserless 的 `web_fetch`、`web_capture`、`browser_navigate`、 @@ -3062,18 +4605,21 @@ 会让所有浏览器会话的 Live 画面与输入一同停滞。这些调用方现在通过 `asyncio.to_thread` 运行校验,放行与拒绝的结果不变。 严格的阻塞 IO 检测新增 `socket.getaddrinfo` 规则,因为 Blockbuster 默认只包装 socket 方法,不包装模块级解析函数。([#6140]) + - **智能体:** 循环检测的整数阈值现在会拒绝 YAML 布尔值,而不是把 `true` 静默转换为 `1`。此前若配置 `warn_threshold: true` 和 `hard_limit: true`,第一组工具调用就会达到硬上限并强制终止智能体; 跟踪窗口、工具频率和按工具覆盖项中的布尔值也会把对应限制缩小为 1。现在所有整数阈值字段都会在配置加载阶段按字段名报错,同时保持 有效整数和数字字符串的既有行为。([#6017]) + - **智能体:** 应用配置的整数设置现在会拒绝 YAML 布尔值,而不是把 `true` 静默转换为 `1`。此前若配置 `recursion_limit: true`,所有未自带递归上限的 Gateway 运行都会在第一个 LangGraph 超级步就触达递归上限;`llm_call` 下的 整数字段(`retry_max_attempts`、`max_concurrent_calls` 与两个退避延迟)中的 布尔值也会把重试次数和并发上限缩小为 1。现在全部七个整数字段都会在配置 加载阶段按字段名报错,同时保持有效整数和数字字符串的既有行为。([#6171]) + - **上传:** 文档转换时现在会记录原文件与 Markdown 的归属关系。 `list_uploaded_files` 只隐藏归属已验证的转换文件,文档大纲也只读取 记录中指定的 Markdown;用户自行上传的同名文件会正常显示,不会被 @@ -3081,6 +4627,7 @@ 用户自行上传的 Markdown 安全地区分。升级后,它们会作为独立文件 出现在历史列表中,原文档也不再从同名文件推断大纲或预览。启用 自动转换后重新上传原文件,可以生成有归属记录的新转换文件。([#6101]) + - **智能体:** 计划模式下被重试的模型调用不再丢失 `TodoMiddleware` 已为其排队的待办完成提醒。该中间件在 `wrap_model_call` 中取出提醒;由于 `LLMErrorHandlingMiddleware` 包裹着它并通过再次调用自己的 handler 来重试, @@ -3088,6 +4635,7 @@ 现在 `TodoMiddleware` 会在 handler 抛出时把取出的提醒放回队列,且不重复计数, 从而让重试带上提醒,上限仍允许发送第二次提醒。成功调用的行为不变; 若该 run 的提醒状态在此期间已被清除,提醒不会被恢复。([#6132]) + - **渠道:** 停止或重启 Slack、飞书、钉钉、Discord 渠道不再冻结 Gateway 事件循环。 `SlackChannel.stop()` 直接调用 `SocketModeClient.close()`,它会 join SDK 的消息处理线程 (每次约 0.7 秒),并等待正在执行的事件监听器,而监听器中阻塞的 Slack Web API 调用 @@ -3097,27 +4645,32 @@ 都会停顿,`POST /api/channels/{name}/restart` 也不例外。现在这些清理都在工作线程中执行; Slack 的 close 会被 shield 并跟踪,因此被取消的关闭流程不会中断它,重试的 `stop()` 会等待同一个 close,而不会再关闭一次。([#6134]) + - **中间件:** `_externalize_to_sandbox` 现在校验外部化工具输出的完整字节大小, 而不再仅使用 `test -s` 检查文件是否非空。此前当远程沙箱写入中途被截断时(如磁盘满 或管道故障),残缺的文件也会通过校验并把错误路径交付给模型;现在会严格核对字节数与负载一致, 发生截断时返回 `None` 并回退到内联截断。([#6112]) + - **Gateway:** 含非 ASCII 字符的 CSRF token、GitHub webhook 签名、内部认证 token、 OIDC `state` 或 provisioner `X-API-Key` 现在按常规返回 403/401,而不是 500。 `hmac.compare_digest` 遇到含非 ASCII 字符的 `str` 参数会抛出 `TypeError`,而 Starlette 以 latin-1 解码请求头字节,一个 `0xE9` 字节就能让比较崩溃。Gateway 现在通过统一的 `app.gateway.utils.constant_time_equals` 比较 UTF-8 字节,独立部署的 provisioner 则在原处编码后比较。此前不存在绕过,请求本就会失败,只是状态码错误。([#6076]) + - **智能体:** 上下文压缩的 fraction 触发器与 fraction 保留量现在使用当前运行 模型的上下文 profile;单独配置的 `summarization.model_name` 只负责生成摘要。 这避免运行模型与摘要模型的窗口不一致时压缩过晚或过早。中间件发布身份现在 分别记录 `profile_model` 与 `summary_model`,任一归属变化都会有意刷新该身份。 ([#5566]) + - **事件:** 在 JSONL 后端上,按 run 读取时,若 run ID 无法用作文件名,不再返回 500。`GET /api/threads/{thread_id}/runs/{run_id}/events`、`.../messages` 与 `.../workspace-changes` 会把 URL 中的 run ID 原样传给事件存储;在 `run_events.backend: jsonl` 下,`run.1` 这类 ID 会抛出 `ValueError`,而内存与 数据库存储返回空结果。现在 JSONL 的读取与删除把这类 ID 视为不存在的 run,写入 仍会拒绝它。([#6070]) + - **智能体:** 运行上下文中带有 `"max_total_subagents": null` 时,现在会使用配置的 `subagents.max_total_per_run`,而不是抛出 `TypeError`。该键存在时 `dict.get(key, default)` 会返回 `None`,构建 `SubagentLimitMiddleware` 时该次运行 @@ -3125,10 +4678,12 @@ Gateway lead agent、`DeerFlowClient` 与系统提示词现在通过同一个辅助函数解析上限: `null` 视为未设置,并限制在 1-50,因此面向扩展的 host policy 与 release policy 报告的也是实际执行的上限,而非超出范围的请求值。([#6088]) + - **调度器:** 固定小时的 cron 任务在夏令时回退(DST fall-back)当天不再重复运行两次。 `croniter` 会返回模糊本地时间的两个实例(首个为 `fold=0`,第二个为 `fold=1`)。 对于分和时字段不包含通配符的固定任务,现在会跳过第二个重复实例(`fold=1`),保持每天只运行一次 的契约(Vixie cron 规范),同时通配符计划(如 `0 * * * *`)仍会在重复的小时内每小时正常触发。(Issue #6052, [#6066]) + - **持久化:** SQLite `checkpointer.connection_string` 若写成 `file:` URI,现在会在 启动时报错,而不是悄悄写到别处。LangGraph 的 SQLite checkpointer 与 Store 打开 连接串时未传 `uri=True`,SQLite 会把 URI 当作字面文件名: @@ -3155,6 +4710,7 @@ 的对话交付的 `sandbox` 通道状态会被 LangGraph 的 `Overwrite` 包装。此前直接判断 `isinstance(sandbox_state, dict)` 会返回 `False`,导致大工具输出无法外部化到沙箱而退化为内联 硬截断,以及写前读锁作用域丢失有效沙箱 ID。([#6015]) + - **doctor:** `make doctor` 现在检查 Gateway 实际加载的配置文件。此前它 固定检查 `<checkout>/config.yaml`,忽略 `DEER_FLOW_CONFIG_PATH` 与 `DEER_FLOW_PROJECT_ROOT`:指向不存在路径、会让 Gateway 无法启动的覆盖值仍会 @@ -3178,18 +4734,21 @@ `DateTime` 列,于是 SQLite 抛出 `StatementError`("only accepts Python datetime"),Postgres 抛出 `DataError`;只有改变节奏才能成功,因为那条路径会重新计算出 datetime。现在仓储在 `update()` 中 会像 `update_after_launch()` 一样,把接收到的所有序列化时间戳转换回来。([#5964]) + - **网关:** `GET /api/skills`、`GET /api/skills/custom` 与 `GET /api/skills/{name}` 不再在事件循环上 遍历技能目录。三者此前都内联调用 `load_skills()`:它会解析调用者的存储、扫描所有公共与自定义 技能目录并解析每个 `SKILL.md`,工作量随已安装技能数增长。#5747 已经为自定义技能内容路由把同一 调用移出事件循环并注明原因,但这三条路由被遗漏了,因此严格的 Blockbuster 门禁会在它们上抛出 `BlockingError`;在生产环境中,技能树很大或磁盘很慢时,扫描期间该 worker 上的其他所有请求都会 停顿。现在三者通过一个共享辅助函数用 `asyncio.to_thread` 卸载加载过程。([#5945]) + - **网关:** `GET` 与 `PUT /api/user-profile` 不再在事件循环上执行文件系统操作。这两个处理器 此前在循环上直接解析按用户隔离的 `USER.md` 路径(每次调用都会构造绝对路径)、stat、读取、 创建用户目录并写入文件,而自定义智能体路由里的其他所有处理器都通过 `asyncio.to_thread` 卸载这类工作。在严格的 Blockbuster 门禁下这一对处理器会抛出 `BlockingError`;在生产环境中, 磁盘变慢时会让该 worker 上的其他所有请求随之停顿。现在两者都把整段 解析-stat-读取 / 解析-mkdir-写入 的流程卸载到线程。([#5935]) + - **Docker:** 通过统一入口上传超过 1 MB 的项目文档不再被 nginx 直接以 `413` 拒绝。 `POST /api/projects/{id}/documents` 是 multipart 上传,Gateway 接受至多 `uploads.max_file_size`(默认 50 MiB)的文件,但没有任何 nginx location 匹配它,于是请求 @@ -3197,12 +4756,14 @@ 一个 2 MB 的 PDF 会被拒,而同一个文件上传到会话里却没问题。三份维护中的配置(Docker、 `make dev`、Helm)现在都为 `/api/projects/{id}/documents` 单独设置 location,沿用会话上传的 设置(100M 上限、请求体流式转发)以及兜底块原本给予的读超时;兜底块自身保持 nginx 默认值。([#5934]) + - **Docker:** 生产栈(`make up` / `scripts/deploy.sh`)现在可以在禁用 IPv6 的主机上启动。 `docker/nginx/nginx.conf` 同时监听 `[::]:2026`;在以 `ipv6.disable=1` 启动的内核上,这条监听会让 nginx 在启动时退出,容器因此反复重启,`make up` 永远无法就绪。开发用 compose 文件自 #2027 起 会在 `/proc/net/if_inet6` 不存在时去掉 IPv6 监听,Helm chart 也做了同样处理;生产用 compose 文件是唯一还没有这层保护的启动器。现在它使用相同的启动脚本,并以 `exec` 启动 nginx 使其成为 PID 1。([#5900]) + - **部署:** `make up` / `scripts/deploy.sh` 现在会采用写在仓库根目录 `.env` 中的 `BETTER_AUTH_SECRET` 与 `DEER_FLOW_INTERNAL_AUTH_TOKEN`。此前脚本只检查 shell 环境,随后 就重新加载已持久化的密钥或生成新密钥并 export;而 Compose 插值时 shell 变量优先于 @@ -3213,12 +4774,14 @@ 因此 `KEY: VALUE` 写法和值内的 `${VAR}` 插值在所有 Compose v2 客户端上都按 Compose 的规则 计算;解析结果为空的值(例如已 export 但为空的 shell 变量)仍会触发生成,因为 Compose 否则 会把空值直接传下去。([#5928]) + - **技能:** `skill_manage(action="remove_file")` 与 `write_file` 现在可以处理二进制支持文件, 并会干净地拒绝目录。`.skill` 压缩包可以包含 `assets/logo.png`(安装器只拒绝*可执行*二进制), 但这两个操作在改动文件之前会先把原有内容按 UTF-8 文本读出(仅用于历史记录),于是二进制 成员会抛出 `UnicodeDecodeError`,文件既不会被删除也不会被覆盖。像 `assets` 这样的裸支持目录 也能通过路径校验并抛出 `IsADirectoryError`。现在非文本内容会记录为“无原有文本”,目录路径 则作为校验错误返回,而不是崩溃。([#5893]) + - **模型:** 在旧路径(没有 `reasoning:` 块)上,`when_thinking_enabled` 与 `when_thinking_disabled` 不再整体替换模型档案的 `extra_body`。两个模板此前都用浅层 `dict.update` 套用,因此档案里与 `when_thinking_enabled.extra_body.thinking` / `when_thinking_disabled` 模板并列的 @@ -3235,14 +4798,17 @@ `budget_tokens` 之类仅在开启时有意义的键应放在 `when_thinking_enabled` 里,而不是基础 `extra_body`; 合成的禁用载荷与契约路径此前已是这一行为。模板在合并时会被深拷贝,因此构造参数不会与缓存的 档案共享对象。([#5894]) + - **项目:** 会话文件视图不再为尚无标题的成员会话显示空标题。会话的 `display_name` 在标题 生成运行之前(或从未运行时)在接口上为 `null`,但文件分组类型将其声明为必填字符串并原样 渲染,因此在首次回复之前上传的文件会挂在一行空白之下。这类分组现在显示为“未命名”,与 项目的会话列表一致。([#5879]) + - **技能:** 删除包目录为运维方管理的符号链接的自定义技能现在可以成功。存储契约允许 `custom/` 下的一级链接,但 `delete_custom_skill` 用 `shutil.rmtree` 删除包目录,而它拒绝 符号链接:删除在写入历史记录之后以 `OSError` 失败,退出时清空了用户的技能投影视图, `DELETE /api/skills/custom/{name}` 返回 500。现在只移除链接本身,绝不触碰其指向的外部目录。([#5881]) + - **前端:** 子任务渲染状态不再在 `MessageList` 渲染过程中被就地修 改。子任务同步从渲染阶段移入 effect,因此即使任务上下文尚未发布 更新,卡片也能立即拿到纯派生自消息的快照——修复了最终流式参数与 @@ -3250,11 +4816,13 @@ 归。消息参数提供标题、prompt 与子智能体类型;实时状态保留生命周 期、模型、用量与步骤历史,终态快照仍以 status/result 为准。 ([#3157]) + - **技能:** 技能启用/禁用请求失败时会在设置页明确呈现,而不是被当 作成功。前端 skills 辅助函数解析响应 JSON 时没有检查 `response.ok`,因此 skills 列表或切换端点返回的 4xx/5xx 会被当作 成功处理,即便后端已拒绝变更,UI 仍会翻转开关。非 OK 响应现在会 留在错误路径上并展示后端错误信息。([#3823]) + - **智能体:** 无人值守运行的提示词现在与工具策略保持一致。非交互 运行会从工具集中移除 `ask_clarification`,系统提示词却仍要求模型 在遇到歧义时调用它,导致 GitHub、webhook 与定时运行既无法提问也 @@ -3262,6 +4830,7 @@ 示词——`webhook` 从 issue 与事件上下文推断,上下文不足时以结构 化的阻塞信息退出;`scheduled`/`autonomous` 做最小且可逆的假设, 并将高风险歧义报告为阻塞。([#4919]) + - **MCP:** 持久化 MCP 任务认领的生命周期现在是取消安全的。在获取 认领之后调用方被取消,可能让相关行一直被租约占用直到过期、使已启 动的释放被中途抛弃,或让清理失败覆盖原始的 `CancelledError`;例 @@ -3270,6 +4839,7 @@ 个认领的 lease token 会原子性地拒绝过期代际的写入(堵住一个 SQLite 竞态)。迁移 `0026` 新增可空的 lease-token 列;MCP 公开 API 无变化。([#4966]) + - **前端:** 流式输出的答案文本不再进入 Thinking 面板。一条同时携 带推理与答案文本的流式 AI 消息,此前会被归入处理过程的折叠面板直 到本回合结束,长时间运行时答案可能一直滞留在 Thinking 面板里;这 @@ -3277,6 +4847,7 @@ 组。bash 工具对模型可见的契约也写明了跨平台本地环境探测方式—— 先执行 `uname -s`,在 Darwin 上使用 `sw_vers`,遇到被阻断的绝对 路径时用仅命令式探测恢复。([#5001]) + - **运行时:** 加固 provider 边界处的模型响应恢复。空的已完成响应 可能让 run 在没有可用答案的情况下结束,被长度上限截断的响应则可 能把截断的工具调用带入循环。真正的空停止现在会在模型边界重试一次 @@ -3284,35 +4855,41 @@ 收场,而不是隐藏的图级恢复回合;检测到长度上限时会丢弃该响应中的 所有工具调用,即使参数已解析成功。DeepSeek 思考模式的历史会在 assistant 工具回合上保留 `reasoning_content`。([#5080]) + - **沙箱:** 沙箱所有权租约 TTL 溢出现在会在配置校验时被拒绝。 `renewal_interval_seconds` 与 `ttl_multiplier` 各自都校验了有限 性,但二者的乘积可能溢出为 `inf`,直到第一次写入租约才失败,或悄 悄改变承诺的时序。非有限的派生 TTL 与亚毫秒的 Redis TTL 现在都会 被拒绝,Redis TTL 上限为有符号 64 位毫秒范围的一半,且小数毫秒向 上取整,使存储的租约绝不会短于校验值。([#5104]) + - **子智能体:** 在释放运行时资源之前先关闭子智能体图流。协作式取 消可能在未显式关闭活跃的 `agent.astream()` 迭代器的情况下返回终 态结果,导致执行器在图流拆除仍在进行时就释放沙箱租约并发送任务停 止通知。现在图流会被保留并在终态化与释放之前关闭;若关闭也失败, 则以原始的流错误或宿主取消为准;拆除超过 10 秒仍未完成会记录告 警。([#5221]) + - **composer:** 将 `context` 保留为斜杠命令别名。 `/context compact` 是 composer 的内置命令,但 `context` 此前未在 共享 slash-skill 契约中保留,因此某个技能可能抢占该别名并将其遮 蔽。共享契约现在保留 `context`,前端与后端的斜杠解析经由它保持一 致,IM 渠道的命令分类不受影响。([#5279]) + - **调度器:** 一次性任务的执行时刻在编辑标题与 prompt 后保持不 变。调度控件在挂载时会把存储的时间戳转换为分钟精度的本地时间再转 回去,因此在纽约 `2026-11-01T06:30:00Z` 会被保存成 `05:30:00Z`, 普通时间戳则会丢失秒与毫秒——一次仅改元数据的编辑就悄悄移动了运 行时刻。编辑表单现在会快照原始时间戳、本地输入值与时区,并在时间 或时区变更之前始终重新发出原始的 `run_at`。([#5330]) + - **调度器:** 一次性调度表单现在会拒绝被夏令时跳过的墙上时间。纽 约的 `2027-03-14T02:30` 在转成 UTC 再转回来后变成 `01:30`,因此 创建与编辑可能保存下与预览不同的时刻。表单现在会校验“本 地→UTC→本地”的往返一致性,不一致时给出本地化的内联提示并把输 入标记为非法,在校正之前发出空的 schedule spec,并保持 Create (以及编辑保存)禁用。([#5348]) + - **沙箱:** 在异步重绑期间排空(drain)上一个沙箱的释放。异步重绑 路径此前通过裸的 `await asyncio.to_thread(provider.release, ...)` 释放上一个沙箱 @@ -3321,6 +4898,7 @@ 旧释放重叠。三条异步的“释放上一个”路径现在改经具备取消安全性的 `run_sync_lifecycle_operation()` 辅助函数,它会持有串行器直至旧 释放完成,然后再传播原始的 `CancelledError`。([#5498]) + - **子智能体:** 关闭被停止的 run 留在委派台账中处于进行中的条目。 点击 Stop 时,`task` 工具会取消子智能体并重新抛出取消异常,因此 不会写入 `ToolMessage`,台账中的委派也就一直停留在 @@ -3330,12 +4908,14 @@ `cancelled`(“已取消的尝试;可在调整计划后重试”)。恢复的 run、共享同一 run id 的目标延续以及已有结果的条目均不受影响。 ([#5507]) + - **前端:** 让澄清(clarification)文本不再落入执行步骤面板。伴随 `ask_clarification` 调用的回答文本此前渲染在带边框的执行面板内 部,提交一条隐藏回复后,先前已完成的回答气泡又会在续流期间被挪回 面板内。澄清结果现在充当已完成 run 的边界,因此更早的回答在续流 与重连期间都能留在会话中;伴随文本则与原始消息一起在面板外渲染一 次,消息与工具结果的关联以及用量统计均保持不变。([#5508]) + - **projects:** 在 Gateway 关机被取消时排空(drain)回收站对账的 文件工作线程。取消启动期的回收站保留清扫只会取消 asyncio 等待 者,其 executor 工作线程仍会继续遍历/stat/unlink 文件,因此 @@ -3345,6 +4925,7 @@ 辅助函数排空:已启动的工作线程先到达终态,然后取消才传播,后续阶 段不再启动。钩子超时仍是宽限完成的预算,而非取消后的硬上限。 ([#5511]) + - **MCP:** 裸文件名重写改为按字面插入,而不是当作正则替换模板。 `_rewrite_unique_bare_filenames` 此前把关联得到的 `/mnt/user-data/...` 路径作为模板传给 `Pattern.subn`,因此文件名 @@ -3354,6 +4935,7 @@ 后让整个调用失败;要么——对 `re` 接受的转义——把转义字节(`\r` 变成 CR)静默替换进返回的路径,随其进入对话记录与 checkpoint。替 换现在改经可调用对象,路径按原样插入。([#5522]) + - **nginx:** 允许绑定模型的 `/api/` 与 `/api/skills` 请求存活超过 60 秒。兜底的 `location /api/`(例如 `POST /api/runs/wait`—— nginx 掐断上游连接时还会连带取消该 run——以及 @@ -3363,6 +4945,7 @@ 都设置 `proxy_read_timeout 600s`,与绑定模型的 location 一致;适 用于 Docker、`make dev` 与 Helm 的 nginx 配置。更具体的 location 不受影响。([#5524]) + - **子智能体:** 在调用方被取消时排空(drain)其自有的 batch-service 停止过程。`SubagentRuntime.stop()` 此前在等待 `SubagentBatchService.stop()` 之前就清掉了自己的 batch-submitter @@ -3372,6 +4955,7 @@ 停止现在在一个强持有的任务中运行,该任务被 shield 并在反复取消下 排空;调用方第一个 `CancelledError` 只在自有停止到达终态之后才传 播。([#5525]) + - **记忆:** 在 lifespan 被取消时排空(drain)Gateway 的记忆关机工 作线程。flush 与后端关闭此前是两个独立的 `asyncio.to_thread()` 等待,因此取消 lifespan 任务只会取消等待者:`manager.close()` 可 @@ -3380,6 +4964,7 @@ 析、`shutdown_flush()` 与 `close()` 现在组合为单一的自有关机操 作,经具备取消安全性的 `await_drained()` 辅助函数排空;调用方第 一次取消只在已启动的工作到达终态之后才传播。([#5531]) + - **沙箱:** 把恰好填满的沙箱搜索结果报告为完整。AIO、E2B、 OpenSandbox、Tenki 与 BoxLite provider 中的 `glob` 与 `grep` 此 前把“收集到 `max_results` 个匹配”当作 `truncated=True`,于是一 @@ -3387,6 +4972,7 @@ 劳地重跑或收窄搜索。过滤后匹配的截断判断现在会多看一个匹配再下结 论——真正被丢弃的匹配仍报告为截断——匹配本身与远端原始输出的上 限均不变。([#5534]) + - **threads:** 线程删除时安全清理持久化记录。 `DELETE /api/threads/{id}` 此前会移除线程目录、checkpoint、浏览 器会话与 meta 行,却把 `run_events`、历史的 `runs` 行和 @@ -3397,6 +4983,7 @@ 因此进行中的删除预留得以幸存。持久化事件存储采用显式的每线程变更 围栏(fence),并发获准的写入者再也无法在删除返回后为已删除的线 程提交行。([#5535]) + - **threads:** 重新打开聊天后重新加入进行中的 run。SSE 重连路径此 前只知道存于当前标签页 `sessionStorage` 中的 run id,因此重新打 开浏览器或新开标签页会丢失该指针,而服务端的 run 仍在继续。现在 @@ -3404,12 +4991,14 @@ run,在加入之前先持久化指针(使停止/取消仍指向正确的 run),并重 新加入其可恢复的流;恢复失败会以有界的 1s/2s 延迟重试两次;指针 一致时会抑制重复加入。([#5536]) + - **渠道:** 在启动被取消时回滚已部分启动的渠道服务。 `start_channel_service()` 此前在等待 `ChannelService.start()` 之 前就发布了全局单例,因此在部分获取资源后被取消会直接退出而不停止 服务,留下一个启动从未完成的单例。启动失败/取消现在会回滚部分启 动的服务,并用 `await_drained()` 在反复取消下排空回滚;单例只在 清理成功后才清除,清理失败则保留它,供后续关机重试。([#5537]) + - **Docker:** 新增 `make prod-logs` 作为生产栈的日志入口,并修复 `make docker-logs` 在生产环境启动后静默退出的问题。`make up` 运行的是 compose 项目 `deer-flow`,而 `make docker-logs` @@ -3420,6 +5009,7 @@ 在没有 `.env` 的检出上 compose 无法解析生产环境的 volume 定义);开发栈日志在没有任何容器时会打印一条指向 `make prod-logs` 的提示。([#5538]) + - **前端:** 推理内容抽取现在会保留围栏、缩进与行内代码中的字面 `<think>` 标签,包括流式输出期间尚未写完的代码。 此前解析器只保护紧跟在反引号之后的起始标签,因此一段讲解 @@ -3429,6 +5019,7 @@ 字面代码之后真正的已闭合或流式推理仍会被正常抽取, 且真实推理内部的 Markdown 分隔符不再妨碍定位其闭合标签, 因此推理中未闭合的围栏无法再藏住回答。([#5540]) + - **上传:** 构建文档摘要时会处理上传 Markdown 中的 UTF-8 BOM。 行首 BOM 会让大纲抽取器无法识别第一个标题或首个代码围栏—— 在围栏场景下,代码注释会被当成文档标题、 @@ -3436,6 +5027,7 @@ 组成的首行还会占用一个兜底预览槽位。大纲与预览现在改用 `utf-8-sig` 读取,它会消费可选的 BOM,同时保留普通 UTF-8 内容、内嵌的 U+FEFF 字符、物理行号以及原始上传字节。([#5541]) + - **网关:** 准备重新生成(regenerate)线程时, 会保留已确认的澄清卡片答案。重新生成端点此前只挑选可见的 human 消息,而澄清卡片答案是以带 `human_input_response` 元数据的隐藏 @@ -3444,6 +5036,7 @@ `human_input_response` 结构合法的隐藏消息, 并保留其隐藏与请求关联元数据;摘要、目标控制消息、 格式非法的隐藏响应以及无关的内部消息仍会被拒绝。([#5544]) + - **沙箱:** 把 `remote_list_dir_command` 与 `remote_search_command` 探测脚本包进子 shell,使其末尾的 `exit` 无法杀掉 AIO 沙箱隐式的常驻 shell。裸 `exit` 会终止会话 @@ -3452,6 +5045,7 @@ 调用都成为确定性的卡死点,并让发出并行 `[ls, bash]` 工具调用的整个 run 陷入死锁。输出标记与退出码仍会从子 shell 正常传递;发生变化的只有探测脚本。([#5546]) + - **上传:** 删除的是请求的上传条目, 而非符号链接解析出的目标文件。`delete_file_safe` 此前会先解析请求路径再删除解析结果,因此放置在上传名上的符号链接 @@ -3462,6 +5056,7 @@ 符号链接与其他非普通文件条目一样返回 `404`; 指向上传目录之外的链接仍返回 `400`;配套 `.md` 的名字取自请求的文件。普通文件的删除行为不变。([#5547]) + - **扩展:** 让扩展服务的关闭在宿主取消之下仍能完整排空(drain) 到底。Gateway 用 `AsyncExitStack` 注册扩展服务清理,却直接 await `stop_services()`,因此当扩展的 `stop()` @@ -3469,6 +5064,7 @@ 存活时继续进行,破坏了该栈本应提供的所有权顺序。 关闭现在经由可取消安全的 `await_drained()` 辅助函数执行, 后续运行时拆卸会等扩展服务在每服务超时预算内到达终态。([#5549]) + - **记忆:** 按键名报告非法的 `memory.backend_config` 取值, 而不是抛出不具名的 traceback。Honcho 后端通过仅判断假值的 `or {}` 回退读取 `failure_policy`、`workspace_overrides` 与 @@ -3481,6 +5077,7 @@ mem0 与 OpenViking 后端也为数值开关(`top_k`、`timeout_seconds` 等)加上同样的键名保护,取代会漏出不具名 `TypeError` 的裸 `int()`/`float()` 转换。([#5555]) + - **MCP:** 按线程化身(incarnation)限定 MCP 会话与持久任务(durable task)的访问范围。删除后重建同一线程 ID 会复用上一个生命周期的持久 MCP 会话作用域, @@ -3493,6 +5090,7 @@ 提交会被原子地拒绝。显式的遗留 `NULL` 化身保留激活前的作用域, worker 轮询与取消仍通过创建会话正常工作, 化身字段也不会出现在公开的任务响应中。([#5556]) + - **子智能体:** 远程子智能体验收探测现在能识别空的普通文件。GNU `stat -c %F` 会把零字节文件报告为 `regular empty file`, 而远程大小与可读性探测此前只接受 `regular file`, @@ -3501,6 +5099,7 @@ UNVERIFIED。现在两种标签都被接受:存在且可读的空文件可以满足 `exists` 与 `file_written`,`non-empty` 则给出确定性的否定结果。 符号链接、FIFO 与路径包含检查不变。([#5559]) + - **前端:** `web_fetch` 工具步骤标题现在取自抓取所得 Markdown 首个非空行。`extractTitleFromMarkdown()` 此前只接受开头字符正是 `# ` 的文档,因此第一个标题之前出现的空行或最多三个空格的缩进—— @@ -3508,6 +5107,7 @@ 会让会话记录失去步骤标题并静默回退到原始 URL。 缩进的代码块仍不计为标题;空标题则不产生标题,而不是留下字面的 `#`。([#5560]) + - **沙箱:** 阻止 E2B 对账(reconciliation)复活预热池中的沙箱。 每一轮对账都会对停放在预热池中的沙箱发起 `Sandbox.connect()`——SDK 会把它规范化为一次 300 秒的超时更新, @@ -3519,6 +5119,7 @@ `sandbox.idle_timeout` 的预热条目,释放其所有权租约、 挂载结果与容量跟踪。预热沙箱现在会真正按配置的 `sandbox.idle_timeout` 过期。([#5562]) + - **utils:** 对无内容的消息返回空文本,而不是字面字符串 `"None"`。`message_content_to_text(None)` 此前会把参数字符串化,因此无内容的子智能体最终消息绕过了 @@ -3526,6 +5127,7 @@ 错误回退会隐藏其 `error_detail`, 任务连续性归档也未能跳过空文本。只有 `None` 内容被视为空—— 字面字符串 `"None"`、`0`、`False` 与列表块转换均不变。([#5563]) + - **doctor:** `make doctor` 现在会校验基于 CLI 的模型凭据内容。 此前只要凭据路径存在,检查就会把 Codex 与 Claude 认证报告为健康,因此空文件、已登出、格式非法或已过期的文件, @@ -3536,6 +5138,7 @@ JSON、空令牌与无效的有效期取值都会按提供方分别报告失败, 并附上既有的修复提示。该检查仍只用标准库、离线且经过脱敏; 环境变量与文件描述符来源仍是不消费的存在性检查。([#5567]) + - **中间件:** 让触到长度上限的回合干净收尾,而不是让 `TodoMiddleware` 重新唤起模型。当模型在发出 `write_file` 调用的同时达到单次响应的输出上限(`finish_reason=length`), @@ -3547,6 +5150,7 @@ `append_visible_text` 会静默丢弃字符串内容的潜在缺陷);并且 `write_file` 的模型可见描述会标注配置的 `max_tokens` 输出预算,以免模型把 80 KB 流式天花板当成单次写入上限。([#5569]) + - **RAGFlow:** 文档选择现在按每批最多 100 个 ID 分批校验。在 1000 篇文档的 scope 上限内,单个数据集选超过 100 篇本就合法, 但此前的校验把全部 ID 放进一个请求发送,提供方会拒绝,导致 @@ -3554,12 +5158,14 @@ 现在选择按提供方可接受的批次大小校验,保持输入顺序, 并对缺失或不可搜索的文档保留 fail-closed 行为, 同时在多个数据集之间共享同一个并发预算。([#5572]) + - **持久化:** PostgreSQL 引导期 advisory 解锁现在能跨取消完成。 `_postgres_lock()` 的 `finally` 中该解锁此前是直接 await 的, 因此当 `pg_advisory_unlock` 仍在执行时到达的启动/lifespan 取消可能中止释放,并把仍持有引导互斥锁的连接归还连接池。 现在释放通过取消安全的 `await_drained()` 辅助函数, 在获取锁的同一连接上完成。([#5573]) + - **持久化:** Gateway 关停时引擎处置现在能跨取消完成。 `close_engine()` 此前直接 await `AsyncEngine.dispose()`, 因此处置被阻塞期间的取消会让关停提前返回,而进程级全局的 engine @@ -3567,6 +5173,7 @@ 现在处置通过取消安全的 `await_drained()` 辅助函数完成, 全局引用在完成前保持发布,过期的清理也无法再抹掉替换后的新引擎。 ([#5576]) + - **渠道:** 发给 Telegram 机器人的照片和文件现在能下载并送达智能体。两个 bug 叠加: 下载在管理器事件循环上新建连接,以笼统的 `NetworkError` 失败; @@ -3574,18 +5181,21 @@ `Permission denied`。下载现在经由绑定到该事件循环的下载 bot, 入站文件获得沙箱可读的权限,失败时会记录底层原因并对含 token 的 Bot API URL 打码。([#5581]) + - **模型:** 顶层不是 JSON object 的 Codex 认证文件现在按“此来源无凭据”处理。`~/.codex/auth.json`(或 `$CODEX_AUTH_PATH`)中的数组、字符串或数字此前会在 `CodexChatModel` 构造时抛出 `AttributeError`,抢在文档记载的 `Codex CLI credential not found` 错误之前; 加载器现在会记录日志并继续回退。([#5584]) + - **记忆:** 显式的 `confidence: 0.0` 不再被当作 0.5 默认值参与评分。两处读取点用 `or 0.5` 兜底该字段, 无法区分“显式为零”与“未设置”,因此零置信度的事实被默认权重抬高, 排名也因写入路径所用的索引不同而不一致。现在无论走 FTS5 索引还是子串回退,零都严格排在 0.5 之下;缺失或 null 的 confidence 仍默认为 0.5。([#5586]) + - **技能:** 现在能加载带 BOM 保存为 UTF-8 的 `SKILL.md`。 front-matter 锚点此前会拒绝记事本和 PowerShell 的 `Set-Content -Encoding UTF8` 写入的前导 `U+FEFF`,导致 Windows @@ -3593,30 +5203,35 @@ 该标记现在由共享的编译锚点消费,不会进入技能元数据、 渲染后的指令或 skill-context 描述,且该模式在加载器、 校验器与中间件中的三份副本也不会再漂移。([#5588]) + - **模型:** 跳过 `expiresAt` 非数字的 Claude Code 凭据来源。 字符串、null、列表或对象值会让加载器抛出 `TypeError` 并中断查找循环,因此 `$CLAUDE_CODE_CREDENTIALS_PATH` 处的一个损坏文件会让所有 `ClaudeChatModel` 永远到不了 `~/.claude/.credentials.json`; 现在该来源会像同类守卫一样被跳过并记录 debug 日志。([#5591]) + - **技能:** 显式为空的 `allowed-tools` 现在渲染为无工具。 技能元数据渲染器此前对该字段做真值判断,因此声明 `allowed-tools: []` 的技能会展示 `Allowed tools: (all)`, 而工具策略却剥掉所有业务工具——模型在规划时依据的正是这些会被拒绝 的工具。现在空声明渲染为 `(none)`; 省略或非空的声明仍按原样渲染。([#5593]) + - **网关:** 将 Windows 的 `image/svg` MIME 别名归类为活动内容。 在 Windows 上,MIME 数据库把 `.svg` 映射为 `image/svg` 而非标准的 `image/svg+xml`, 而共享的活动内容分类器只识别标准类型,因此 SVG artifact、 `.skill` 归档成员和项目文档可能被内联提供而不是强制下载。 现在所有主机均视该别名为活动内容。([#5594]) + - **模型:** 在 Codex 的 `account_id` 进入请求 header 前容忍 null。`or` 链式兜底让 JSON `null` 以 `None` 形式穿过加载器, 导致 `CodexChatModel` 构造在 `self._account_id[:8]` 处崩溃; 数字型 `account_id` 则稍后因 `httpx` 拒绝非字符串的 `ChatGPT-Account-ID` header 而失败。null 现在回退到未知账户值 `""`,其他非字符串会被字符串化。([#5601]) + - **LLM:** 熔断器探针结算现在按代次与归属加围栏。 `LLMErrorHandlingMiddleware` 此前无条件结算成功与失败, 因此一个在较旧闭合代次下准入的请求, @@ -3625,6 +5240,7 @@ 现在每次准入都会捕获熔断器代次,且成功、 失败或释放只有在代次匹配——半开状态下还要求探针 token 精确匹配——时才会改动熔断器。([#5602]) + - **记忆:** 拒绝永远无法解析的 Honcho `base_url`。该字段此前 读取时没有任何校验,因此内部主机的自然省略 scheme 写法 (`base_url: honcho:8000`)在启动时被接受——httpx 把主机名 @@ -3634,6 +5250,7 @@ `http://` 前缀。现在格式错误的值会在 Gateway 启动时快速失败, 与文档声明的行为一致;而今天能够解析的每一个 URL 都继续照常 工作。([#5607]) + - **子智能体:** 显式为 0 的批量上限现在按名字上报, 而不再替换为默认值。`SubagentBatchService.submit()` 此前 用 `or` 解析 `max_live_items` / `max_running_items`, @@ -3643,6 +5260,7 @@ 违反的上限被拒绝。两处解析现在改为与 `None` 比较,显式的 `0` 会到达范围守卫并按名字被拒绝;键缺省仍然表示 "使用配置的默认值"。([#5609]) + - **沙箱:** 远程 `list_dir` 输出现在会丢弃被忽略的目录。 本地沙箱会跳过 `node_modules`、`.git` 等忽略模式, 但同一调用在远程沙箱中会把它们下面的每一条路径都列出来, @@ -3651,6 +5269,7 @@ 包含被忽略目录名的条目,并像本地遍历那样相对列举根进行匹配: 显式列举一个被忽略的目录仍然可行,完全被忽略的目录则返回 空而不是 `FileNotFoundError`。([#5612]) + - **中间件:** 上下文压缩时会丢弃过期的 todo 提醒。 todo 提醒注入之后,压缩可能把旧的 task 状态摘要进去, 或把过期提醒留在保留的消息尾部,这也会阻止 @@ -3659,18 +5278,21 @@ 排除 `todo_reminder` 快照,当前 todo 状态得以保留: 当不再有可见的 `write_todos` 调用时,中间件会在下一次 模型调用前重新生成提醒。([#5614]) + - **沙箱:** Tenki 的 `read_file` 现在支持 `start_line`/`end_line`。Tenki provider 此前只接受 `path`, 因此区间读取——智能体继续被截断读取的常规方式——会抛出 `TypeError: ... unexpected keyword argument 'start_line'`, 并被渲染成笼统的读取错误。它现在与同类 provider 一样进行切片, 负数边界的钳制方式与本地沙箱一致。([#5616]) + - **持久化:** PostgreSQL schema 引导连接关闭现在能跨取消排空完成。 `ensure_postgres_schema_async()` 此前用一个裸 `await` 关闭 专用的 psycopg 连接,因此 lifespan 拆除期间到来的取消 可以打断关闭并直接返回,而连接仍然存活。 关闭现在经过取消安全的 `await_drained()` helper, 它会把调用方的取消推迟到持有的连接完成关闭之后。([#5617]) + - **运行时:** 运行时 provider 的拆除现在能跨取消排空。 checkpoint-cache 与 stream-bridge 的 provider 上下文管理器 持有自己的后端,并在上下文退出时直接 await @@ -3678,6 +5300,7 @@ 可以打断关闭并返回,而 memory 或 Redis 后端仍然存活。 memory 与 Redis 后端的这两处关闭现在都经过取消安全的 `await_drained()` helper;取消只在持有关闭完成后传播。([#5622]) + - **Helm:** 启用共享 home PVC 时,provisioner 的状态根目录 现在与 Gateway 对齐。chart 此前只设置 Gateway 的 `DEER_FLOW_HOST_BASE_DIR`,provisioner 仍停留在 @@ -3687,6 +5310,7 @@ provisioner 现在收到相同的逻辑根目录,投影因此映射到 PVC 上的 `deer-flow/<suffix>`;`persistence.home.enabled=false` 时 该变量仍不设置。([#5625]) + - **client:** 内嵌客户端现在会尊重智能体的 MCP 插件选择。以 `mcp_plugins: [installation-A]` 保存的智能体通过 `DeerFlowClient` 仍会收到所有已启用 MCP 服务器的工具, @@ -3695,6 +5319,7 @@ 将其传给委派任务(包括缓存图 run); 选择以无序集合的形式加入图缓存键,`reset_agent()` 之后 保存的配置变更仍然生效。([#5630]) + - **持久化:** Alembic 迁移 worker 现在能跨取消排空。 `bootstrap_schema()` 在 `stamp`/`upgrade` 于 `asyncio.to_thread()` 中运行期间持有 SQLite 进程内 @@ -3703,6 +5328,7 @@ schema 状态时从引导临界区返回,让第二次引导与 进行中的迁移重叠。现在每个 Alembic worker await 都经过取消安全的 `await_drained()` helper;worker 完成后取消才传播。([#5631]) + - **沙箱:** AIO 沙箱现在会强制执行命令超时。 `AioSandbox` 此前忽略调用方提供的超时, `sandbox.bash_command_timeout` 从未生效—— @@ -3712,6 +5338,7 @@ (被冻结的旧 `:latest` 镜像只获得有界的主机端等待), 结果不明确时绝不重放,`sandbox.bash_command_timeout` 现在成为 provider 默认值。([#5634]) + - **渠道:** Slack Socket Mode 的连接失败现在会被记录, 而不是被丢弃。`SlackChannel.start()` 通过 `run_in_executor()` `SocketModeClient.connect()` 并丢弃返回的 future, @@ -3720,6 +5347,7 @@ "Future exception was never retrieved" 形式浮出水面。 连接失败现在连同 traceback 一起记录,与 Telegram、Discord 渠道已有的做法一致。([#5640]) + - **MCP:** stdio MCP 服务器配置的 `cwd` 现在会被尊重。 `McpServerConfig` 以 extra 字段接受了该设置,但 `build_server_params()` 在任何一条启动路径看到它之前 @@ -3728,6 +5356,7 @@ 随后在池化工具调用中的相对文件读取上失败。 现在该目录在非空时(包括环境变量引用)会转发给 stdio 连接, HTTP/SSE 则省略;省略、`null` 或空值保持默认行为。([#5643]) + - **技能:** SkillScan 现在从 AST 读取 Python 密钥赋值。 `secret-env-assignment` 规则此前用 `name[:=]value` 正则 扫过所有文本文件,这会误读 Python:带类型的可选参数默认值 @@ -3737,6 +5366,7 @@ 评审门禁。Python 源码现在从其 AST 分析,只报告真正的字面量值; 非 Python 文件的覆盖不变,并有一个测试钉住 内置技能保持干净。([#5648]) + - **持久化:** 数据库自动建库维护引擎的释放现在能跨取消排空。 `_auto_create_postgres_db()` 此前用一个裸 `await maint_engine.dispose()` 释放一次性引擎, @@ -3745,6 +5375,7 @@ 这个泄漏无法自愈,因为下次启动走的是数据库不缺失的路径, 永远不会重试释放。dispose 现在经过取消安全的 `await_drained()` helper;首次启动成功时行为不变。([#5649]) + - **技能:** 每用户技能安装的目录创建不再占用事件循环。 `UserScopedSkillStorage.ainstall_skill_from_archive` 此前 在任何实际工作派发之前就用阻塞的 `os.mkdir` @@ -3754,12 +5385,14 @@ 因为它只覆盖 host 作用域的基类。`mkdir` 现在与周围的解压和校验一样 在工作线程上运行,锚点也覆盖了用户作用域路径。 安装语义与磁盘上的布局不变。([#5650]) + - **沙箱:** 在 e2b、OpenSandbox 与 BoxLite provider 中像 `LocalSandbox` 那样对 `read_file` 的负数区间边界做钳制。 此前负值会落入 Python 负索引切片:`start_line=-1` 会静默返回文件末尾几行,`end_line=-1` 会返回截断的区间, 而不是从第一行开始读取、返回空区间。工具层早已拒绝负数边界; 直接使用 `Sandbox` 的调用方会踩中这一缺口。([#5655]) + - **技能:** 让内置 Vercel 部署技能的包目录与其声明名称一致, 并把部署命令指向真实挂载路径 `/mnt/skills/public/vercel-deploy/scripts/deploy.sh`。 @@ -3767,11 +5400,13 @@ `/vercel-deploy` 激活,且其说明引用了 DeerFlow 从不挂载的脚本路径,因此一次部署请求可能选中正确的技能, 却拿到一条指向不存在文件的命令。([#5656]) + - **前端:** 技能导出面板对显式为空的导出要求不再显示空白值。 导出对话框此前用 `join(", ")` 格式化 `allowed-tools` 与 `required-secrets`,显式空声明会渲染成空字符串, 与省略声明无法区分——尽管二者策略语义不同;现在未声明显示为 "Not declared",显式为空显示为本地化的 "None"。([#5659]) + - **沙箱:** 为 AIO `list_dir` 增加专属的目录级截止时间。 目录列举此前在沙箱级锁下运行,仅有 600 秒无变化超时并继承 SDK 传输预算,因此卡住的 relay 或僵死的 `find` @@ -3780,6 +5415,7 @@ 秒不重试的宿主级信封:`hard_timeout` 抛出 `TimeoutError` 并保持 shell 代际可复用;结果不明时抛出 `OSError` 并将其隔离。 未新增配置。([#5662]) + - **运行时:** 在记忆 run 存储中, 一次原子线程操作现在共享同一个变更位置。 `create_thread_operation_atomic` 此前按行推进变更时钟, @@ -3788,6 +5424,7 @@ 读取方可能只看到原子集合的一半; 现在每个被认领的行和新行都携带同一位置,与 SQL 存储一致。 无持久化数据库的默认部署选择的就是该存储。([#5663]) + - **运行时:** 同步 checkpoint 变更现在跨取消排空。goal 写入与回滚卸载会通过 `asyncio.to_thread()` 把同步 checkpointer 移入线程执行,此前取消调用方只会停止等待,而 `put`、 @@ -3795,12 +5432,14 @@ 调用方已观察到取消,变更却仍可能在之后提交。 这些回退路径现在经由取消安全的 `await_drained()` helper 排空; 读回退 `get_tuple` 仍可直接取消。([#5664]) + - **智能体:** 智能体装配指纹现在会区分技能未声明与显式为空的 `allowed-tools`。此前两种声明状态都哈希为空列表,把技能从旧的 allow-all 切换为无业务工具策略时, 运行时工具可用性变了而指纹不变;现在未声明的 `allowed_tools` 按 `None` 哈希,显式 allowlist 仍保持为排序列表。Frontmatter 解析与运行时工具策略均未改变。([#5669]) + - **上传:** 删除文档时不再连带删除其转换生成的 Markdown 伴生文件。`delete_file_safe` 此前会重新计算 `stem.md`, 因此共享词干的两个文档(`a.docx`、`a.pdf`) @@ -3809,16 +5448,19 @@ `DELETE /api/threads/{id}/uploads/{filename}` 现在只删除被请求的文件;孤立的伴生文件仍保留在列表中, 可单独删除。API 形态与配置均无变化。([#5673]) + - **沙箱:** 远端 `ls` 列举现在先剪除被忽略的条目, 再应用列举上限。此前的 500 条上限先于忽略路径的剔除生效, 依赖树可以填满整个窗口并把旁边的普通文件挤掉, 返回列表有时只剩根目录;现在被忽略的后代在共享 `find` 命令内、 `head` 之前剪除,深度与输出限制保持不变。([#5676]) + - **持久化:** `database.postgres_schema` 现在也会作用于 DB 后端自定义智能体与托管子智能体所用的同步 SQLAlchemy 存储。异步 ORM 与 LangGraph 存储早已遵循所配置的 schema,但同步 agent-store 查询会回退到 `public`,找不到应用表;连接 URL 现在在保留既有 libpq 选项的同时追加 `search_path`。([#5678]) + - **日志:** 所有非绝对路径的 Redirecting 槽位现在折叠为 `/<redacted>`。#5225 的 URL 脱敏流程此前对不以 `/` 开头的槽位原样保留,因此 `download?sign=…` 这类不带斜杠的相对 @@ -3826,17 +5468,20 @@ `data:` 的目标,会在 DEBUG 重定向日志中原样暴露签名 query; 现在只有位置 0 处带 scheme 的目标才交给通用绝对 URL 流程处理, 非 URL 的 `-> /path` 沙箱挂载箭头仍原样通过。([#5680]) + - **渠道:** 钉钉入站文件被跳过时现在会记录日志, 而不是静默丢弃附件。此前返回空内容的下载会不带任何日志地消失, 与消息本就无附件无法区分;现在跳过时会输出一条中性的 guard 日志并点名文件,与微信渠道的形态一致,准确原因仍由下载 helper 内部记录。行为无变化。([#5683]) + - **网关:** 就绪探针的连接关闭现在跨取消排空。`/health/ready` 此前用裸的 `await connection.close()` 关闭 SQLite 与 PostgreSQL 探针连接,teardown 期间的第二次取消可能将其打断, 让探针在连接仍存活时返回,使数据库清理脱离请求生命周期; 现在关闭经由取消安全的 `await_drained()` helper 排空。 响应形态不变。([#5684]) + - **日志:** 含空格的 Redirecting 槽位现在折叠为 `/<redacted>`。 Round 15 此前对带 scheme 的槽位原样保留、交给通用绝对 URL 流程, 但该流程的模式在空白处截断,因此形如 @@ -3844,6 +5489,7 @@ 的槽位只会重写空格前的文本,把其余部分泄露进 DEBUG 日志; 现在只有当槽位被一个不含空白的绝对 URL 完整填满时才原样保留, 普通的绝对重定向对仍保留其 host。([#5687]) + - **技能:** SkillScan 的 `secret-env-assignment` 规则重新报告绑定 在普通语句赋值之外的密钥。#5648 引入的 AST 遍历此前只识别裸常量 赋值,因此关键字实参(`connect(api_key="...")`)、参数默认值、海 @@ -3852,6 +5498,7 @@ `RecursionError`,而扫描器会把它记为按文件错误,丢弃该文件的全部 发现。#5648 带来的精确性(注解、`os.getenv` 调用)保持不变。 ([#5691]) + - **社区工具:** Browserless `web_fetch` 现在像 `web_capture` 一样 读取文档中声明的等待设置。`wait_for_timeout_ms` 改用宽容的 `_as_int` 辅助函数解析,因此 `2s` 这类取值会回退到默认值,而不是 @@ -3859,6 +5506,7 @@ `wait_for_selector_timeout_ms` 则从工具配置读取,而非硬编码的 `5000`(默认值不变,既有配置行为一致)。两个键现在均已在 `config.example.yaml` 的 `web_fetch` 条目中补充文档。([#5702]) + - **社区工具:** SearXNG 超过一页的 `max_results` 现在会被遵守。 SearXNG API 没有 `limit` 参数,客户端此前发送了一个被忽略的 `limit`,只请求第 1 页并从已截断的列表中切片——`max_results: 20` @@ -3866,6 +5514,7 @@ 并累积结果,直到达到上限、某页返回为空或某页没有新增内容(按 URL 去重)为止,最多 5 页;无效的 `limit` 参数不再发送。默认的 `max_results` 仍只需一次请求。([#5705]) + - **沙箱:** 身份不明确的 AIO 会话创建现在既不能重放也不能执行。 shell 与 bash 创建平面通过 pending/owned/absent/tombstoned 状态 机跟踪每个尝试过的 id:超时、传输错误、5xx 响应与格式异常的 @@ -3873,6 +5522,7 @@ 创建、仅做有边界的尽力清理且从不清除 tombstone——同时该平面上的新 创建会被阻断。脏沙箱通过既有的 teardown 路径释放,而不是回到预热 池;shell 的模糊不影响 bash 创建。([#5711]) + - **模型:** 通过 Settings 添加的 DeepSeek 官方模型现在使用原生 `PatchedChatDeepSeek` 适配器。托管 profile 及其连接探测此前总是 构建 `ChatOpenAI`,导致添加 `deepseek-flash` 时连接测试以 @@ -3882,6 +5532,7 @@ 模式仅在受限的强制工具探测中被禁用——正常对话设置不受影响——且匹配 仅限 `api.deepseek.com`,第三方兼容端点保持原行为。既有已保存的 profile 无需目录迁移即自动获得该适配器。([#5718]) + - **社区工具:** Browserless `web_fetch` 现在从工具配置读取 `reject_resource_types` 与 `reject_request_pattern`。这两个参数 此前只是声明并传给客户端,却始终被赋值为 `None`,因此配置的值被 @@ -3889,6 +5540,7 @@ 与带宽的主要手段——根本无法开启。取值接受 YAML 列表或逗号分隔字符 串;不可用的值会直接省略该参数而非发送。默认值不变,两个键均已在 `config.example.yaml` 中补充文档。([#5719]) + - **技能:** 自定义技能回滚路由不再在事件循环上读取技能的编辑历 史。`POST /api/skills/custom/{skill_name}/rollback` 此前在请求路 径中内联构造存储、探测存在性并解析 @@ -3896,12 +5548,14 @@ 新版技能内容,一次回滚就会让这次解析拖住 worker 上的所有其他 run 与流。该工作现在移到工作线程执行,与相邻的历史处理器保持一致;状 态码与响应结构不变,且路由已有 blocking-IO 锚点守护。([#5729]) + - **MCP:** 持久 MCP 任务调用现在把连接建立与 MCP 初始化约束在与进 入 session 相同的 `session_init_timeout` 截止时间内。该超时此前 只在进入 session 上下文之后才开始计时,因此一个接受 HTTP 却从不 发送 MCP endpoint 事件的 SSE server 会让调用无限等待。初始化成功 后该截止时间即失效,改用既有的独立工具调用超时;`None` 仍可关闭 初始化限制。([#5733]) + - **技能:** `required-secrets[].optional` 现在必须是 YAML 布尔 值。形如 `optional: "false"` 的带引号取值此前能通过 frontmatter 校验,之后却按 Python 真值语义解析,于是 `bool("false")` 把必填 @@ -3910,70 +5564,85 @@ `optional=False` 并给出警告(仅含取值类型与密钥名)。技能评审分 析器将该形态标记为 `structure.invalid-required-secrets-optional`。([#5738]) + - **技能:** 技能评审提取资源引用时不再保留句末标点。句末的裸路径 (如 “See references/setup.md.”)此前会连同尾部句号一起被提取, 产生误报的 `resource.missing` 发现,并为真实文件制造一个虚假孤儿 引用。提取的引用现在会剥掉尾部的 `.`、`?` 与 `!`,而 `references/config.yaml` 这类真实的点号文件名与 `references/v1.0.md` 这类链接目标保持完好。([#5739]) + - **上传:** 分阶段上传的 `.part` 文件现在只在文档转换描述符释放后才删除。启用 `uploads.auto_convert_documents` 时,提交环节会在转换仍持有已打开描述符的情况下先行 删除分阶段文件,因此在 Windows 上所有可转换的上传(PDF/DOCX/PPTX/XLSX)都会以 `WinError 32`(500)失败,而 POSIX 上只是被静默容忍。分阶段文件名现在随描述符的释放 在同一轮清理中移除;中止路径的清理改为尽力而为,避免二次删除失败掩盖原始错误。([#5740]) + - **composer:** IME 组合中的 Enter 不再触发斜杠技能目录。主输入框的联想处理器缺少兄弟 调用点都有的 `isIMEComposing` 守卫,因此在 IME 激活且 `/` 目录打开时,用于提交 CJK 组合内容的 Enter 会被当作选中高亮技能,正在输入的文本随之丢失。被 IME 消费的按键现在 落入正常的提交路径;普通 Enter 仍选中高亮技能。([#5741]) + - **沙箱:** BoxLite 的同步桥接等待超时后,其提交到事件循环的任务现在会被取消。 `run_coroutine_threadsafe(...).result(timeout)` 向调用方抛出异常,却让已提交的协程 继续在私有事件循环上运行,沙箱可能在操作已被上报为超时之后仍在变更状态或持有 acquire 所有权;现在会先取消未完成的 future 再重新抛出超时,已完成的 future 则保持不动。 ([#5748]) + - **MCP:** 仅当生效的 MCP 配置真正变化时才使 MCP 工具缓存失效。此前失效逻辑以整个 `extensions_config.json` 的签名为准,切换一个无关的技能也会重建缓存工具并退役所有 stdio 连接池会话。缓存现在对比已启用服务器解析后设置的快照(按声明顺序)与全局 `mcpInterceptors`,只有发生变化才退役;当没有启用任何服务器时,非初始化的刷新会清理 过期状态而不引入额外发现开销;配置损坏时的错误日志也不再输出已解析的凭据。([#5750]) + - **网关:** 请求被取消时,artifact 更新现在会先完成再释放所有权。取消 `PUT /api/threads/{thread_id}/artifacts/{path}` 会在线程操作预留与沙箱租约尚未完成 非挂载远程同步的情况下被拆除,导致远端与主机副本字节不一致,也让后续工作与被放弃的 变更重叠。完整的远程同步/本地替换事务现在会先完成排空再释放所有权,回滚语义保持不变, 主提交失败会先记录日志再回滚。([#5755]) + - **上传:** 分阶段上传清理失败时保留原始 link 错误。在转换描述符尚未关闭的 link 提交 失败场景中,Windows 会以共享冲突拒绝错误处理器中的 unlink,这个次要错误顶替了真正的 端点错误;通用 link 失败路径现在遵守 `unlink_staged=False`,所有分阶段清理均为尽力 而为。link 成功之后的清理失败也不再对已发布的目标返回 500。([#5756]) + - **运行时:** 调用方被取消时,数据库 provider 上下文退出会先完成再传播。异步 SQLite/PostgreSQL checkpointer 与 Store provider 使用裸 `async with`,若取消恰好在 后端 `__aexit__()` 阻塞时到达,退出本身会被取消,provider 在数据库/连接池清理不完整的 情况下结束——六条 provider 路径全部如此。新增的 `drained_async_context()` 辅助函数在 把退出排空至完成的同时保留异常与抑制语义。([#5757]) + - **渠道:** Buzz 已读事件存储路径的解析移出事件循环。`ChannelService._start_channel` 内联构造默认 `seen_event_store_path`,在 `POST /api/channels/{name}/restart` 可达的 路径上于事件循环内解引用 `Paths.base_dir`(一次 `realpath`);解析现在与其他步骤一起 放入工作线程,并有 blocking-IO 锚点将其固定在严格门禁之下。([#5759]) + - **沙箱:** AIO 沙箱 acquire 锁路径的解析移出事件循环。 `_discover_or_create_with_lock_async` 把 acquire 的每一步都放到了线程中,唯独内联的 锁文件路径构造没有,它在每次沙箱工具调用的热路径上解引用 `Paths.base_dir`(一次 `realpath`);解析现在并入周边步骤一起在工作线程执行,兄弟的 ownership-publish 锚点 也重新覆盖生产路径层。([#5760]) + - **threads:** LangGraph 运行被拒绝时不再留下孤儿会话。独立 run 准入会在 LangGraph 复核 assistant 归属前预创建缺失的线程,因此对他人非系统 assistant 发起的运行虽按预期 返回 404,却已经持久化了一个属于调用者的空闲线程。准入现在在 `Threads.put` 之前校验 assistant 的服务端来源(期间暂时清除环境中的认证上下文);普通用户仍可为注册的系统 assistant 隐式创建线程。([#5762]) + - **setup:** pre-commit 钩子改经 `uv` 运行,`make install` 在 uv 的工具 bin 目录不在 PATH 上时不再失败;Node.js 前置检查也变得可操作:nvm 用户会得到安装命令,其他用户会 得到官方下载页链接。([#5767]) + - **调度器:** 在 SQLite 写锁之下读取派发租约的属主。`release_dispatch_lease` 是唯一 绕过 `_lock_task` 的写路径,而 SQLite 会忽略 `FOR UPDATE`,其守卫判断的是过期快照: 落在派发窗口内的暂停可能被覆盖,任务保持 `enabled` 并再次触发,即使 API 已经确认了暂停。 读取现在先取写锁;PostgreSQL 行为不变。([#5777]) + - **utils:** 初始化 `Article.url`,`to_message()` 对包含图片的页面不再抛出 `AttributeError`。`ReadabilityExtractor` 此前从不向 `Article` 传入页面 URL,经 `urljoin(self.url, …)` 解析相对图片地址时必然崩溃;该属性现在默认为 `""`,抽取器会 传入真实 URL。([#5785]) + - **工具:** DDG 网页搜索的结果上限现在会从环境变量做类型强制转换。 `max_results: $DDG_MAX_RESULTS` 此前保留替换后的字符串, DDGS 9.14.1 在为其 worker 定容时抛出 `TypeError`, @@ -3981,18 +5650,21 @@ 接受数字字符串;非法或非正值记录警告并回退到默认值 5, 整数配置、默认值以及"配置优先于调用参数"的次序均保持不变。 ([#5792]) + - **网关:** 空白的 `GATEWAY_HOST` 或 `GATEWAY_PORT` 现在视为未设置。 compose 文件中的 `GATEWAY_PORT=`(或 `PORT` 未设置时的 `GATEWAY_PORT=${PORT}`)会把空字符串交给 `int()`,让 Gateway 在 uvicorn 绑定端口之前的导入阶段就崩溃。空白值现在回退到声明的默认 值(`0.0.0.0` / `8001`);`GATEWAY_PORT=0` 仍会向操作系统申请空闲 端口,所有非空值的解析与之前完全一致。([#5801]) + - **渠道:** 渠道运行时配置现在以 UTF-8 持久化。该存储读取 JSON 时 用 UTF-8,写临时文件时却使用主机默认编码且 `ensure_ascii=False`,因此在非 UTF-8 主机(CP1252、GBK)上, 非 ASCII 取值——例如配置的 Buzz 中继 URL——可能保存失败, 或写成存储自身无法读回的字节。临时写入现在显式指定 UTF-8; 原子替换、加锁与 chmod 行为不变,旧编码文件不做迁移。([#5803]) + - **setup:** `make setup-sandbox` 现在接受 Apple Container 的成功 镜像拉取。在安装了 Apple Container 但没有 Docker 的 macOS 上, 脚本此前会在镜像已经拉取成功之后,继续落入 Docker 可用性检查并以 @@ -4000,6 +5672,7 @@ Apple Container 独立完成的拉取现在计为成功并继续输出默认镜像 提示;Docker 的行为——包括 Apple Container 失败后的回退——保持 不变。([#5804]) + - **运行时:** 智能体流的拆除现在能在反复取消下保持存活。 `close_agent_stream()` 此前在调用方任务中 await `stream.aclose()`,拆除期间的第二次 `Task.cancel()` 会把关闭本身 @@ -4009,6 +5682,7 @@ 调用方的第一次取消被推迟到关闭完成之后, 来自流自身的关闭取消则上报为关闭失败,使 run 以失败终态收场。 ([#5806]) + - **工具:** DDG 图片搜索套用 #5792 的数字字符串强制转换。 `image_search_tool` 此前把环境变量替换出的 `max_results` 字符串直接交给 DDGS,后者在为 worker 定容时抛出 `TypeError`, @@ -4016,6 +5690,7 @@ `No images found`;`0` 会移除上限,负值则截断结果。 数字字符串现在可以工作,非法、零或负值会警告并回退到 5, 整数配置与默认值保持不变。([#5807]) + - **client:** 恢复的内嵌客户端流不再重放之前的回合。 在线程恢复后,`DeerFlowClient.stream()` 此前会把更早回合的消息 增量(包括工具结果)重新发进 `messages-tuple` 事件,并把它们的 @@ -4023,6 +5698,7 @@ `values` 全量状态快照中,但会被排除在当前回合的增量与用量之外, 以当前用户消息上的每 run `run_id` 为边界;Gateway SSE 不受影响。 ([#5811]) + - **持久化:** postgres schema 建立连接的退出现在跨取消排空。 `_ensure_postgres_schema_with_pool` 此前仍用裸 `async with` 获取连接,调用方的取消若在连接 `__aexit__` 阻塞时到达, @@ -4030,12 +5706,14 @@ 会带着一个仍被占用的连接关闭。该连接现在与其余 checkpointer provider 一样经过 `drained_async_context()`;正常路径行为不变。 ([#5812]) + - **MCP:** 裸文件名关联流程不再破坏反斜杠路径。stdio MCP 调用创建 `page.yml` 之后,结果中提到的 `C:\outside\page.yml` 的 basename 会被改写为指向新的工作区文件,因为该流程把反斜杠当作裸文件名的 边界。反斜杠现在在关联名的两侧都作为路径分隔符处理, 真正的裸文件名仍能解析,而未解析的 Windows、UNC 或相对路径中的 名称保持原样。([#5815]) + - **MCP:** MCP 文本结果中含空格的生成文件路径现在能被解析。 名为 `artifact with space.txt` 的文件此前在自由文本工具结果中 保留其绝对主机路径,后续的 `read_file` 以 `Permission denied` @@ -4043,15 +5721,18 @@ 含空白的绝对路径与工作区相对路径现在都会与该调用创建的文件关联, 并在当前线程的 user-data 目录树内校验;树外路径与有歧义的后缀 保持原样。([#5819]) + - **中间件:** `ThreadDataMiddleware` 现在保留 human 消息的元数据。 追加 run 元数据此前会从碎片重建最后一条 `HumanMessage`, 静默丢弃未复制的字段(如 `response_metadata`);中间件现在复制 既有消息,只更新其默认名称与 `additional_kwargs`。([#5823]) + - **上传:** 文档转换的 `stat()` 与 Markdown 回写不再占用事件循环。 `convert_file_to_markdown` 此前虽把转换本身卸载到了线程, 却仍在事件循环上执行大小检查的 `stat()` 与转换后 Markdown 的 多兆字节 `write_text`,阻塞大文档的上传摄取。两者现在都经由共享的 `run_file_io` 线程池;小文件内联转换的阈值不变。([#5830]) + - **MCP:** 初始化本身抛错时,惰性 MCP 工具发现现在只运行一次。 `get_cached_mcp_tools()` 里针对无事件循环的 `except RuntimeError` 回退把发现自身的错误也一并捕获:没有 @@ -4062,6 +5743,7 @@ "asyncio.run() cannot be called from a running event loop" 之下。现在初始化恰好驱动一次并记录真实失败;出错仍返回 `[]` 且缓存保持未初始化,留待下次重试。([#5836]) + - **配置:** `request_admission` 的整数字段现在支持 `$VAR` 替换。 `requests_per_minute` 与 `max_queue_size` 声明为 `strict=True`, `true` 与 `60.0` 仍会被拒绝,但像 `requests_per_minute: $RPM` @@ -4070,6 +5752,7 @@ 设定节奏。现在一个 `BeforeValidator` 会在严格检查之前把纯 十进制字符串转换为 `int`;布尔、浮点、`"60.0"`、空值与非数值 仍被拒绝,`gt=0` 保持不变。([#5838]) + - **工具:** 调用 SDK 之前先对 Exa 配置的结果上限做类型强制转换。 `$VAR` 替换解析出来是字符串,因此 `max_results: $EXA_MAX_RESULTS` 会让每次 `web_search` 调用失败 @@ -4079,6 +5762,7 @@ 的字符串);空白、非数值、带小数、布尔、零或负值会记录警告 并回退到默认值(5 与 1000),而不是让搜索失败,与 DDG、 SearXNG provider 保持一致。([#5839]) + - **网关:** 空白的 `GATEWAY_WORKERS` 与空白的 Lark broker 端口或超时现在视为未设置,扩展了 #5801 的空白环境变量规则。 微信扫码登录守卫中的 `int("")` 会把空白的 @@ -4089,6 +5773,7 @@ `GATEWAY_WORKERS or WEB_CONCURRENCY or "1"`,对多 worker 或非数值仍然 fail-closed;broker 则回退到自身默认值。 ([#5840]) + - **技能:** 技能评审的资源图现在会从 code-span 资源引用中 剥离章节锚点。Markdown 链接目标此前已在解析前丢弃 `#fragment`,但形如 `references/faq.md#pricing` 的 @@ -4097,6 +5782,7 @@ code-span token 现在以同样方式切分 fragment 并去掉尾部 句读标点;`references/v1.0.md#notes` 这类带点的文件名会 保留其扩展名中的点。([#5841]) + - **技能:** 技能评审的 Markdown 链接扫描在对抗性输入下保持线性。 提取器曾对每个候选 opener 重复扫描同一目标串,一长串未闭合的 `[` 或 `](` 密集的非法目标都会让评审退化为平方复杂度——#5714 在 256 KiB @@ -4105,6 +5791,7 @@ 范围内。扫描现在只遍历一次候选 opener,经与基准正则的差分 fuzz 验证, 与 `finditer` 完全一致;已匹配区间改按位置清除,不再使用 `str.replace`,避免误删后方相同的文本。([#5884]) + - **配置:** 配置缓存签名的应是它实际解析的字节,而不是 第二次读取。app-config 加载器此前打开 `config.yaml` 两次 ——先解析一次,再对一次全新的读取做哈希来记录 @@ -4114,16 +5801,19 @@ 器现在通过共享的 `file_signature.read_config_with_signature()` helper 只读取一次,并对返回的字节精确签名;竞态编辑最多 只多付出一次重载。([#5848]) + - **开发:** 当同级 worktree 的路径包含空格时,`make stop` / `make dev` 现在也能回收它占用的 开发端口。`serve.sh` 用 `awk '{print $2}'` 解析 `git worktree list --porcelain` 来构建 worktree 根目录列表,而该输出中的路径不加引号,因此 `.../deer flow two` 被记录成了 `.../deer`;从那个 worktree 启动的 Gateway 或前端永远不会被识别为 deer-flow 的进程, 启动会以“端口已被占用”中止。现在会保留整条路径。([#5856]) + - **上传:** 运行消息元数据中格式错误的 `files[*].size` 不再导致整个运行失败。 `UploadsMiddleware` 对客户端提供的文件条目的其他字段都做了容错校验,唯独把 `size` 直接交给 `int()`,因此 `"abc"` 或列表这样的值会在调用模型之前从 `before_agent` 抛出——而且由于该条目 会被原样带入,之后每次编辑或重新生成这条消息都会再次失败。该字段只用于 `<current_uploads>` 里的可读大小;现在无法使用的值会回退为 `0`,与缺失时一致,数字字符串仍然有效。([#5855]) + - **发布:** 版本升级不再把 `backend/uv.lock` 落下。`scripts/bump_version.sh` 会改写 `backend/pyproject.toml`、`frontend/package.json` 与 Helm chart,但 lockfile 同样记录了 根包自身的版本(uv 保留其 PEP 440 形式,因此 `2.1.0-rc0` 存为 `2.1.0rc0`),于是文档给出的 @@ -4131,6 +5821,7 @@ 也会拒绝该工作区;装了 pre-commit 时还会更早在 `uv-lock-check` 钩子上失败。现在该脚本会用 `uv lock` 刷新 lockfile,并在缺少 `uv` 时于修改任何文件之前退出,而不是留下一个只改一半的 工作区。实际改动仅涉及根包的那一行版本号。([#5859]) + - **沙箱:** `glob` 与 `grep` 不再在搜索根目录(或其任一祖先目录)命中忽略模式(如 `build`、 `dist`、`logs`、`node_modules`、`coverage`、`target`)时返回空结果。此前各远端沙箱把 `should_ignore_path` 用在**绝对路径**上,而该函数会检查路径的每一段,于是树上任意位置的忽略名 @@ -5285,6 +6976,7 @@ 条目在警告中丢弃,而配置了却得不到任何有效 ID 的值会拒绝所有公会并记录 错误;未设置、`null`、`[]` 或空白字符串仍允许所有公会。 `allowed_channels` 获得同样的标量处理。([#6338]) + - **发布:** `v*` 发布门禁现在会拦下过期的 `backend/uv.lock`。此前 `scripts/verify_versions.sh` 只比较 `Chart.yaml`、`pyproject.toml` 和 `package.json`,手动改这三处就能通过门禁,而 lock 里记录的根包版本仍是旧版本。 @@ -5316,12 +7008,14 @@ 回到默认工作区;工作区布局无法读取请求 URL,因此现在改由浏览器发起跳转,在 `next` 中保留完整地址。客户端会话检查(`AuthProvider.refreshUser`、 `useRequireAuth`)此前保留了路径但丢掉了查询参数,共享 fetcher 则丢掉了锚点;现在都保留完整地址。([#6681]) + - **沙箱:** AIO、E2B、OpenSandbox 提供者和 ACP agent 不再对 `sandbox.environment` / `acp_agents.<name>.env` 的值做第二次解析。加载 `config.yaml` 时已替换每个 `$VAR`,因此本身以 `$` 开头的已解析密钥(如 `$2b$12$...` 形式的 bcrypt 哈希或生成的密码)会再次被当作变量名查找,传入沙箱 或 agent 时变成空字符串;若同名主机变量存在,则变成该变量的值。现在这些值按 加载结果原样传递,与 BoxLite 和本地提供者一致。([#6673]) + - **前端:** 侧边栏和对话页加载更多对话时,不再重复请求已加载的对话。侧边对话 不在这些列表中显示,因此一页 50 条可见对话可能跨越更多服务端行;页面原先把这个 偏移量作为数组上的隐藏属性保存,TanStack Query 的结构共享和列表自身的缓存更新 @@ -5333,38 +7027,45 @@ 任务,现在显示为任务时区当天的零点,与服务端实际执行时间一致。此前界面把末尾的 `-07` 误认为 UTC 偏移量,显示成 UTC 零点:在 `Asia/Shanghai` 显示为 08:00,在 UTC 以西的时区则显示为前一天晚上;任务列表、详情、对话卡片和编辑表单的预填时间都受影响。([#6692]) + - **子代理:** 持久批处理工作器在不再监管仍在运行的子代理时,现在会取消它,并在它 停止后从注册表中移除对应条目。此前,在长期运行的进程中停止批处理服务(SDK 的 `SubagentRuntime` 停止)会让每个仍在运行的 `SubagentResult`(包括完整对话 记录)永久留在进程级注册表中;轮询时的存储库错误会把条目标记为失败或重新排队, 却让其子代理在无人监管的情况下继续运行,因此重试可能把同一条目执行两次。始终不 停止的条目会在其租约过期后被移除。([#6785]) + - **网关:** 子代理批次 JSONL 导出 (`GET /api/threads/{thread_id}/subagent-batches/{batch_id}/results.jsonl`) 现在每次读取 10 行已存储结果,而不是 500 行。每行最多可包含 `max_result_chars` 的报告和同等长度的证据 JSON,而导出会先把整页载入内存再开始输出,因此默认上限下 单个请求约占用 280 MB(上限 1,000,000 时约 1.7 GB),并且每页会阻塞事件循环 半秒以上。导出的行及其顺序不变。([#6706]) + - **网关:** 子代理批次条目列表 (`GET /api/threads/{thread_id}/subagent-batches/{batch_id}/items`,批次运行时工作区 每 2 秒轮询一次)不再为了只返回预览而载入每个条目的完整报告、证据 JSON 和提示词。 默认 `max_result_chars` 下,一页 100 个条目约占用 34 MB,并阻塞事件循环约 70 ms (`limit=500` 时为 170 MB 和 300 ms);现在不到 1 MB。响应内容不变。([#6781]) + - **渠道:** 在渠道设置页保存或断开机器人时,不再覆盖经由其他 Gateway worker 保存的机器人。每个 worker 都保留启动时读入的 `runtime-config.json` 内容,并用这份 副本重写整个文件,因此在两个 worker 下,一个 worker 上保存的 Telegram 机器人会在 另一个 worker 保存 Slack 机器人时丢失,已断开的机器人也会重新出现。现在每次保存 都会在跨进程共享的锁下重新读取文件,只修改自己的 provider。([#6844]) + - **持久化:** 同一用户同时对同一个运行首次评分(两个标签页、两台设备或客户端重试)时, 不再有一个请求返回 500。两个请求都会查不到已有记录并插入,第二个插入会触发 `(thread, run, user)` 唯一约束;在 Postgres 上,80 个这样的请求中有 26 个失败。 IM 会话绑定中的同类竞争(例如首条消息与 `/new` 同时到达)也不再导致消息处理失败。 落后的写入现在会更新另一方创建的记录。([#6852]) + - **MCP:** 在 SQLite 上取消后台 MCP 任务时,取消请求现在会在下一个轮询周期发往远程服务器, 而不是最多延迟 `mcp_tasks.lease_seconds`(默认 120 秒)。由于 SQLite 上 `FOR UPDATE` 不起作用,在轮询器认领任务期间提交的取消请求所清除的轮询租约会被写回;取消工作器 随后要等该租约过期,任务还会被多轮询一次,而结果会被丢弃。现在每次认领在写入租约时 都会重新确认任务仍可认领。Postgres 不受影响。([#6859]) + - **持久化:** 启动时的 schema 升级不再为已与模型一致的列记录虚假的 `safe_add_column ... drifts from the model definition` 警告。该检查拿模型的通用类型名 和默认值写法(`STRING`、`BIGINTEGER`、`DefaultClause('0')`)去比较数据库自己的写法 @@ -5372,6 +7073,7 @@ 数据库升级时都会记录一条,丢失 head stamp 的 schema 会记录 29 条(SQLite)或 36 条(Postgres),淹没了这个警告本应发现的真实类型不一致。现在两侧都按所连接 数据库的写法进行比较。([#6865]) + - **沙箱:** 启用 host bash 时,本地路径守卫现在会拒绝把 shell 变量用作路径前缀的 命令,例如 `cat $HOME/.ssh/id_rsa`。`~/`、`${HOME}/` 和 `"$HOME"/` 原本就会被拒绝, 但绝对路径扫描会跳过紧跟在单词字符之后的 `/`,因此不带花括号的 `$HOME/...` @@ -5382,6 +7084,7 @@ 因为它会把相对路径的 `cd` 移出工作区。`$PWD/...` 仍然允许,因为它与对应的相对路径 指向同一文件,除非命令自己重新绑定了 `PWD`。URL 中的变量不受影响。该守卫仍是尽力 而为的检查,并非隔离边界。([#6930]) + - **沙箱:** 启用 host bash 时,本地路径守卫重新接受 awk 字段运算,例如 `awk '{print $NF/2}' data.txt`、`awk '{print $sum/$n}' data.txt` 和 `($2+$3)/2`。 自 [#6930] 起,除非两侧都是数字,每个 `$NAME/` 都会被当作以变量为前缀的主机路径, @@ -5396,22 +7099,82 @@ (反斜杠换行、反引号、`$'...'`、未配对的 `)`),以及 `"` 可能位于 awk 正则字面量 中的程序都不享受豁免;awk 字符串字面量中的 `/` 和 `#` 按普通文本处理。 (issue #7021, [#7023]) + - **沙箱:** 在本地沙箱中,`ls` 现在接受 `/mnt/user-data` 根目录本身,并像 AIO 一样 列出 `workspace`、`uploads` 和 `outputs`。提供方有意映射了这个根目录,但工具层只允许 其下的路径,因此返回 `Permission denied`(带结尾斜杠时来自内部的 “path traversal detected” 检查)。列表只包含这三个目录,直接存放在根目录下的其他内容 仍然不可见。`glob`、`grep`、`read_file` 和写入仍然拒绝根目录,因为搜索会读取其下的 所有文件。([#6991]) - ### 安全 +- **制品:** 为 `_get_viewed_image_blob_response` 增加 + @require_permission("threads", "read", owner_check=True, require_existing=True) + 保护,共享的已查看镜像检查点 blob 只在调用方拥有仍然存活的 `threads_meta` 行时 + 才被返回;此前仅有路由级检查时,缺失的行被当作允许的未跟踪遗留线程处理,这 + 足以触达 blob 路径。该 helper 还会在 blob 存储禁用时提前返回,把 + `mimetypes.guess_type` 卸载到工作线程执行(冷 mimetypes 数据库在首次查询时会 + stat 系统文件),并把解析失败记录为携带异常类型的 warning。([#7604]) + +- **浏览器:** 对 unbrowse `web_fetch` 目标错误中报告的最终 URL 做有界与脱敏处理。 + 当目标页面返回 HTTP 400–599 时,失败消息会原样回显抓取服务的 `finalUrl`——既无 + 长度限制,嵌入其中的用户名和密码也原样保留。现在该消息会把 URL 送入 + `_strip_url_userinfo`:只保留带主机名的 http(s) URL(authority 解析失败的 URL + 不会产生任何最终 URL,因此原始凭据永远不会被回显),丢弃 netloc 中最后一个 + `@` 之前的所有内容,并把结果截断到 500 个字符。([#7399]) + +- **skillscan:** 对无法跟在兄弟节点之后的复合语句部分(`if`/`elif`/`else` 分支、 + 实际会执行的唯一 `match` case、`try` 与循环体),在外层 client handle 仍然存活 + 的状态下遍历,并让各部分按源顺序应用自己的重绑定;能跟在兄弟节点之后的部分 + (循环或 `try` 之后的 `else`、handler、`finally`、更靠后的 `except*`)仍然从 + 这些名字已被丢弃的状态开始。此前复合语句能绑定的所有名字在任何部分之前就被 + 丢弃,于是同一 body 中靠后的语句重绑定该变量时,真正先发生的一次出站网络调用 + 就会消失。仅由派发绑定的名字(循环目标、`except ... as` 名字、`match` 捕获) + 会在头部遍历完成之后才丢弃,因此头部自身的 sink(`for s in s.post(...)`)也会 + 被报告。([#7128]) + +- **技能:** 对 `skill_manage` 的 `write_file` 内容,改用共享包文件规则 + `is_code_file` 分类,而不是 `"scripts/" in path` 子串测试。 + `references/scripts/overview.md` 这类支持文件此前被送去可执行内容审核——一种 + 要求精确 “allow” 的扫描,在审核模型不可用时直接硬阻断——而顶层 `scripts/` + 目录之外的可运行文件(例如 `references/scripts/helper.py`)和无扩展名的 + shebang 文件却逃入静态审核,可以在 warn 裁决下或设置 + `security_fail_closed=false` 时写入磁盘,且该写入路径上没有后续扫描会恢复这道 + 闸门。可执行内容现在定义为顶层 `scripts/` 成员、代码后缀文件或 shebang 文件, + 与安装器和 SkillScan 保持一致。([#7088]) + - **上传:** 线程上传(`POST /api/threads/{thread_id}/uploads`)、删除上传文件 (`DELETE /api/threads/{thread_id}/uploads/{filename}`)以及把资料库文档附加到线程, 其 `500` 响应的 detail 不再包含原始 `OSError` 文本。此前遇到权限错误,或磁盘已满到 无法创建暂存文件时,响应会返回该线程上传目录的宿主机绝对路径(含用户 ID 和线程 ID)。 现在它们返回 `Failed to upload <name>.` / `Failed to delete <name>.`,完整错误只记录在 Gateway 日志中。([#7217]) - + +- **沙箱:** 在沙箱审计护栏中把 `.` 内置命令视为 `source` 执行上下文:此前只有 + `source` 这个词会命中,因此 `. $(curl ...)`、`. <(curl ...)` 和反引号形式以值 + 位置通过,而同一命令的 `source` 写法则被阻断。点号规则现在锚定在命令位置, + 位于既有的赋值与包装前缀之后,因此独立的点号路径参数(`du -sh . $(curl u)`) + 仍然通过,而对位置不敏感的兄弟规则恢复了对 `eval . $(curl u)`(包括包装写法) + 的阻断。还有一个 token 级检查会阻断把 process substitution 喂给命令位置解释器 + 的 stdin,但仅在没有其他途径提供程序时生效:脚本操作数、shell noexec 标志或 + 点号内置命令的文件名要求都会让抓取到的字节保持为数据。([#7247]) + +- **社区工具:** 在每个提供方中用 `safe_public_result_url` 过滤 `web_search` 结果 + URL——tavily、exa、youcom、tencent_wsa、serply(含 scholar 的 `pdf_url`)、 + groundroute、sofya、ddg_search、fastcrw、firecrawl、searxng、webz,以及 brave + 和 serper 的 organic links——使提供方载荷中的回环、链路本地或元数据服务地址 + 永远不会作为引用到达模型。非公开 URL 会被置空,同时保留该行的 title 与 + snippet;此前只有图片搜索链接被过滤,且 serper 的注释明确写明原样返回 organic + links。([#7037]) + +- **技能:** `executable_binary_magic` 现在会越过前导填充字节嗅探 ELF/PE/Mach-O + 魔数,底层是新的 `is_executable_binary_prefix`:匹配前最多跳过 63 字节的 ASCII + 空白/NUL 填充外加一个 UTF-8 BOM,并且压缩包预检、解包护栏、目录静态扫描与 + 嵌套 zip 检查中的扫描窗口从 8 字节扩大到 `EXEC_SNIFF_BYTES`。此前魔数前的一个 + 填充字节(带 `b'\n'` 前缀的 ELF 仍能经 `ld.so` 加载)就能同时绕过所有层,通过 + 预检与解包且不产生任何报告;64 字节及以上的填充序列保持在嗅探范围之外,因此 + 带缩进的正文不会误触护栏。([#7081]) + - **agents:** 自定义智能体与用户画像路由的 `500` detail 不再包含原始异常文本。 此前无法解析的智能体 `config.yaml` 或文件系统错误(如 `[Errno 13]`)会让响应 带出 `users/<user_id>/` 下的宿主机绝对路径;现在返回与智能体导入、导出一致的 @@ -5541,6 +7304,7 @@ `GET/PUT /api/user-profile` 此前还读写同一份全局 `USER.md`,任何 已认证用户都能覆盖或读取所有人的 prompt 上下文——该文件现在存放 在 `{base_dir}/users/{user_id}/USER.md`。([#4989]) + - **网关:** 元数据行缺失或 owner 为 NULL 的线程现在仍保持 owner 隔离。#5448 的内部调用方 run 范围限定会为每个受信任的内部调用方 返回空过滤器,而 @@ -5552,6 +7316,7 @@ 立 owner 的线程保持不过滤的读取,缺失/NULL-owner 的线程则回退到 代为执行的 owner 的原始 stamp,使跨用户的 run 保持隐藏。浏览器与 API 会话均不受影响。([#5484]) + - **前端:** 工具步骤链接改为经由共享的 `isSafeHref` scheme 白名单 把关。`web_fetch`、`web_search` 与 `image_search` 工具步骤此前把 URL 直接塞进 `<a href target="_blank">`,因此取自模型所写工具参 @@ -5562,6 +7327,7 @@ 同的不可点击“Unsafe link omitted”span;`web_fetch` 还不再对非 字符串的 `url` 参数抛异常,且所有工具分支中缺失的工具调用 `args` 都规范化为 `{}`。([#5526]) + - **上传:** 内嵌客户端的上传不再能穿过符号链接写入。 上传目录从沙箱内部即可写, 因此沙箱中的进程(例如被提示注入操控的进程)可以埋下 @@ -5571,6 +7337,7 @@ 上传现在通过一个禁止符号链接的复制辅助函数发布; 不安全的目标会被跳过,并按 Gateway 的契约经 `skipped_files` 与 `success: false` 上报。([#5578]) + - **上传:** 文档转换现在读取暂存字节的一份私有副本, 而不是它们落盘时使用的名字。上传目录可以从沙箱内部写入, 因此在文档转换前的窗口期内,沙箱中的进程可以把某个上传已提交的 @@ -5579,6 +7346,7 @@ 附件形式回到了会话中。Gateway 现在转换的是私有临时目录里由文件 描述符支撑的副本,客户端则转换调用方自己的源路径——被调换的名字 不再能重定向读取。([#5611]) + - **鉴权:** Live Browser WebSocket 连接现在强制执行 `threads:write`。WebSocket 此前绕过 HTTP 认证中间件, `browser_stream()` 会检查认证、Origin、thread 归属与浏览器 @@ -5586,6 +7354,7 @@ thread 属主仍能连接该 thread 的 Live Browser、 接收帧并派发输入事件。被拒绝的连接现在会在获取会话之前以关闭码 `4403` 拒绝;细粒度鉴权关闭时行为不变。([#5621]) + - **网关:** 现在会在写入 checkpoint、run 准入和智能体执行之前, 以 HTTP 400 拒绝外来的 `system` 与 `developer` 消息。 此前这类消息会被接受并可能被持久化, @@ -5593,6 +7362,7 @@ 消息表示现在会一次性完成规范化与校验;普通的 user、assistant、 tool 与附件消息以及受信任的内部系统消息均不受影响,既有 checkpoint 也不会被重写。([#5651]) + - **技能:** 用户作用域的 `.skill` 安装现在与继承的压缩包预检使用 同一份 app 配置来解析静态内容扫描。用户作用域的存储覆盖此前未传 入 `app_config`,内容扫描便回退到热更新的进程级全局值:当某存储 @@ -5600,6 +7370,7 @@ 将其改为 `false` 时,预检仍会执行但内容扫描被静默跳过,含 CRITICAL 内容的压缩包(如内嵌私钥材料)照样安装。按文件的 LLM 扫 描现在转发同一份配置,模型选择不再读取全局值。([#5703]) + - **沙箱:** 恢复沙箱的引用现在限定在已认证用户与线程范围内。已认 证调用方可以在自己拥有的线程上提交 `input.sandbox.sandbox_id`, 而 AIO 恢复路径会从进程级活跃沙箱缓存解析 body 提供的 id,却不检 @@ -5609,24 +7380,28 @@ HTTP 400 拒绝,checkpoint 复用要求作用域匹配,否则回退到以 `Overwrite` 持久化的规范 `acquire(user_id, thread_id)`,且只有服 务端创建的 fork 路径可以借用父沙箱。([#5736]) + - **日志:** urllib3 重试日志行中携带空格的请求目标现在会被脱敏。三个脱敏模式 (`Retry: …`、`Retrying (…) …` 与带引号的请求行)都在第一个空格处停止匹配,因此 RFC 9110 允许的、内部含空格的 origin-form 路径会带着完整签名查询串明文进入日志; `Retrying` 形态以 WARNING 级别输出,无需 DEBUG 就会落入生产日志。这些模式现在一直 匹配到目标的结束引号或行尾,与已加固的 `Redirecting` 与 increment-retry 槽位保持一致。 ([#5745]) + - **认证:** `POST /api/v1/auth/initialize` 现在原子地认领第一个管理员。处理逻辑此前在 一个会话中统计管理员数、在另一个会话中创建用户,两者之间没有任何锁或约束,因此两个并发 的首启请求(不同邮箱)都会成功且都成为管理员——而且毫无声息,因为 `setup-status` 随后 报告系统已初始化。计数与插入现在共用一个串行化事务(SQLite 上 `BEGIN IMMEDIATE`, PostgreSQL 上事务级 advisory lock,未知方言在启动时直接失败),落败方收到文档载明的 `409 system_already_initialized`;顺序请求的行为与响应结构不变。([#5776]) + - **沙箱:** `view_image` 读取现在要求先有 `sandbox:execute` 授权。 被允许调用 `view_image` 但被拒绝 `sandbox:execute` 的角色, 此前仍能读取当前线程允许目录中的图片,图片中间件还可能把这些字节 放入模型请求——这是授权缺口,而非跨用户读取。同步与异步读取现在 都会检查执行授权,中间件在读取已记录于线程状态的图片前也会复查, 角色变更之后同样如此。([#5799]) + - **鉴权:** suggestions 端点现在强制执行 `model:use`。在模型授权 启用的情况下,被拒绝某模型的用户此前仍能通过 `POST /api/threads/{thread_id}/suggestions` 调用它——显式指名, @@ -5634,6 +7409,7 @@ `GET /api/models/{model_name}` 会得到 403。共享的模型详情检查现在 在模型创建之前执行,拒绝时返回 403,并置于尽力而为的处理之外, 权限失败不会再变成返回空结果的 `200`。([#5816]) + - **沙箱:** 已查看图片的主机读取现在限定在当前线程。外部 run 输入 或线程状态更新此前可以提供带有自选 `actual_path` 的 `viewed_images` 元数据,在匹配到 `view_image` 历史之后, @@ -5747,6 +7523,7 @@ `SKILL.md` 与 `references/` 内容。该工具始终可用,技能工具策略也无法移除它。 现在本地目标仅限于配置的技能根目录和调用者自己的用户目录;`skill://` 与 `inline://` 目标不受影响。([#6580]) + - **沙箱:** BoxLite、OpenSandbox、Tenki 三个提供者的 `glob` 与 `grep` 用 `str.splitlines()` 切分记录,而该函数还会在裸回车、换页符、垂直制表符、 文件/组分/记录分隔符、下一行符以及 U+2028/U+2029 处断行——这些字符在 Linux @@ -5755,6 +7532,7 @@ 的匹配行会在该字符处被截断。现在这三个提供者只按 `" "` 切分,与共享解析器 既有的约定以及 LocalSandbox、AIO 后端、E2B 的行为一致。([#6595]) + - **安全:** 新增仅从环境变量读取的静态凭据加密密钥 `DEER_FLOW_CREDENTIALS_KEY`。 按连接存储的 IM 渠道凭据此前虽有加密路径,但没有任何生产代码接入,既无法写入 也无法读取,Slack 因而始终使用部署级 bot token。现在 Gateway 在启动时加载该密钥 @@ -5766,9 +7544,18 @@ `make up` 将其持久化到运行时目录,两个 compose 文件都会把它传给 Gateway。 `.jwt_secret`(以及托管模型密钥)现在以独占方式创建并回读,共享卷上同时冷启动的 副本不再各自保留不同的会话签名密钥。 ([#6611]) - ### 文档 +- **配置:** 在 `backend/docs/CONFIGURATION.md` 新增的“Skills and sandbox paths” + 一节中记录跨平台的技能与沙箱路径。该节区分 `skills.path`(网关读取的源根目录) + 与 `skills.container_path`(面向智能体的 `/mnt/skills` 根目录,即使在 Windows 上 + 也保持 POSIX 风格),固定解析顺序(先是 `skills.path`,然后 + `DEER_FLOW_SKILLS_PATH`,再是项目根目录下的 `skills/` 目录;相对路径按 + `DEER_FLOW_PROJECT_ROOT` 或进程 cwd 解析,而不是按存放 `config.yaml` 的目录), + 并给出 Linux、macOS、原生 Windows、WSL 和 Docker 网关的源路径。YAML 反斜杠引号、 + `DEER_FLOW_HOME` 下每用户自定义技能目录以及不得二次挂载技能目录的规则均有覆盖; + `config.example.yaml` 注释和 `README.md` 的沙箱指引也已同步更新。([#7304]) + - **扩展:** 新增示例编写与打包指南,以及 `scripts/check_extension.py PACKAGE`。 该工具构建临时 wheel,在安装前检查入口点、Python 语法、API 依赖兼容性与浏览器 资源。检查器运行可信构建后端,不导入扩展,也不改变部署配置。([#6755]) @@ -5777,6 +7564,7 @@ 链接与英文版一一对应,所有代码块和行内代码保持逐字节一致,只翻译正文。`README.md` 及其他译本的语言切换行和 `AGENTS.md` 的译本列表现已链接到该文件。 ([#6909]) + - **文档:** 配置检查与迁移命令现在在 backend 环境中运行。文档中的示例此前用 系统 Python 执行,无法导入已安装的 backend workspace 包;用 `uv run --directory backend` 选择环境会改变工作目录,导出的相对路径因此可能 @@ -5793,32 +7581,38 @@ `LocalContainerBackend`,说明针对受限网络既有的 Docker 选择, 并用两个现存的运行时选择 pytest 用例(它们会 mock 运行时命令) 作为验证。([#5605]) + - **文档:** 将 `config.example.yaml` 中的 `# Sandbox Configuration` 横幅移回它所介绍的 `sandbox:` 键正上方——此前它已漂移到二十行之外的 `uploads:` 块上方—— 并为 `uploads:` 增加专属横幅。仅改动注释;由示例生成的 `config.yaml` 文件不受影响。([#5652]) + - **文档:** 在 README 与运维排障指南中澄清既有本地和 Docker 部署的升级流程:`make config` 与 `make docker-init` 是首次安装步骤,而非常规的源码升级步骤;`make config-upgrade` 只在某个版本新增配置字段时才需要执行。([#5654]) + - **文档:** 新增连接本地 llama.cpp `llama-server` 的文档,关闭 #2931。`config.example.yaml` 与英文/中文配置页面新增 OpenAI 兼容 的 Qwen 示例(`use: langchain_openai:ChatOpenAI`,指向 `http://localhost:8080/v1`),并涵盖 `--alias` 模型名、Docker 可 达的 `base_url`、启用 `llama-server --api-key` 时的 API key,以 及 chat-template/工具调用要求。([#5688]) + - **文档:** 扩展手册(deerflow-extension-api 0.2.3,中英文)覆盖全栈插件与请求范围的 运行证据。新增 Full-Stack Plugins 章节,文档化 `registry.plugin()`、 `BrowserModule`/`BrowserAssets`、后端 action、模型工具与设置;运行证据章节覆盖 `resolve_run_evidence_reader`/`require_run_evidence_reader` 并附带按用户鉴权的路由 示例;Reference、Troubleshooting 与 Operating Extensions 也补充了插件相关小节。 ([#5775]) + - **文档:** 修正文档中的技能与 MCP 启用/禁用端点。文档站与 `backend/docs/API.md` 此前指向 `POST /api/extensions/skills/{name}/enable` 等网关中不存在的路由(404/405); 技能开关实为 `PUT /api/skills/{name}`(请求体 `{"enabled": bool}`),MCP 开关实为 `PATCH /api/mcp/config`(请求体 `{"server_name", "enabled"}`),两者均需管理员权限, 现已写入中英文技能与 MCP 页面及 API.md,并附可用的 cURL 示例。([#5778]) + - **文档:** 修正后端文档示例中引用了不存在 API 的地方。 `PATH_EXAMPLES.md` 的上传示例此前导入了一个任何模块都不导出的 `THREAD_DATA_BASE_DIR` 符号,还指向旧版上传桶;现在改为通过 @@ -5826,6 +7620,7 @@ 解析目录,它读取的正是 Gateway 实际写入的桶。`CONFIGURATION.md` 也删除了 `SandboxConfig` 从未声明、也无代码读取的 `sandbox.auto_start` 键。([#5798]) + - **文档:** 让自定义记忆存储指南变得可以照着做。harness 的 `memory.mdx` / `customization.mdx` 片段导入了一个并不存在的 模块,展示的存储类既无法被工厂构造、其调用也会被记忆更新器 @@ -5833,6 +7628,7 @@ 取值形式,并承诺了代码实际会刻意抛错之处的优雅回退。指南 (英文与中文)现在写明真实的模块、匹配真实的调用形态, 并陈述实际的错误行为。([#5844]) + - **文档:** 新增十章的 checkpoint 存储手册 (`harness/checkpoints`),中英双语——快速上手、渠道模式、 快照节奏、历史缓存、恢复与回滚、运维、可观测性、故障排查 @@ -5899,9 +7695,120 @@ 的标签列表固定为 `MIDDLEWARE_EVENT_TAGS`;`backend/docs/summarization.md` 补充该事件缺失的三个 `changes` 字段。扩展示例不再声称覆盖全部贡献类型: 它演示八种中的五种,README 为其余类型指向观察者指南和 bookmarks 插件示例。([#6582]) - ### 内部改进 +- **网关:** 删除未使用的代码且行为无变化:前端 `ProxyPolicy` 接口与 + `LANGGRAPH_COMPAT_POLICY` 常量(`frontend/src/core/auth/proxy-policy.ts`)、 + `app/gateway/auth/password.py` 中未使用的 `_CURRENT_VERSION` 密码哈希常量、 + skillscan 编排器中未使用的 `_PYTHON_CLIENT_SINK_METHODS` 集合(其测试现在从 + `_PYTHON_CLIENT_SPECS` 推导 sink 方法),以及 AIO 沙箱远端后端中未使用的 + Lark CLI 配置/数据路径常量——其 `_PROVISIONER_EXTRA_MOUNT_PATHS` 现在复用 + 共享的 `_LARK_CLI_RUNTIME_CONTAINER_PATH` 字面量。([#7554]) + +- **Provisioner:** 从 `docker/provisioner/app.py` 的 `_build_volumes`、 + `_build_volume_mounts` 与 `_build_pod` 中移除被忽略的 `include_legacy_skills` + 参数。请求字段保留在 `CreateSandboxRequest` 上,仍会被接受并记录日志,以便 + 旧版网关继续工作,但它不再门控任何挂载——public/custom/legacy 技能投影改为 + 无条件构建,精简后的 PVC 测试已在不再传递该标志的情况下断言这一点。([#7555]) + +- **网关:** 移除未使用的前端组件与一个未使用的辅助函数:没有任何导入引用的 + `components/ai-elements/` canvas-flow 组件集(`canvas`、`checkpoint`、 + `connection`、`context`、`controls`、`edge`、`image`、`node`、`open-in-chat`、 + `panel`、`plan`、`sources`、`toolbar`、`web-preview`),随之移除了 + `@xyflow/react` 与 `tokenlens` 依赖;以及 RAGFlow 社区工具中未使用的 + `_group_searchable_datasets` 函数。无面向用户的行为变化。([#7543]) + +- **依赖:** 将 `backend/uv.lock` 中传递依赖 `opentelemetry-api` 的锁条目从 + 1.41.1 升级到 1.45.1。新版本不再需要 `importlib-metadata`(已连同 `zipp` 一起 + 从锁中移除),刷新后的解析还新增了 + `opentelemetry-exporter-http-transport` 与 + `opentelemetry-exporter-otlp-common` 锁条目。([#7510]) + +- **沙箱:** 让 `test_bench_sandbox_prewarm` 因粗时钟导致的回归变得确定:用固定 + tick 的假时钟替换基准测试模块的时钟绑定(`bench.time`),不再量化真实的 + `time.monotonic` 并空转等待新桶开始。`Trace` 现在从受控时钟初始化,asyncio + 保留真实时钟使超时仍会触发,tick 恰好在 acquire/工具事件门控处推进;原先针对 + 同一 tick 的用例变为对 0.0 与 0.015 tick 偏移参数化的用例, + `backend/tests/AGENTS.md` 记录了该模式。([#7513]) + +- **依赖:** 将前端锁文件中的 `zod` 从 4.3.6 升级到 4.6.7。该依赖是间接依赖, + 因此只有 `frontend/pnpm-lock.yaml` 发生变化。([#7512]) + +- **依赖:** 将后端锁文件中的 `langgraph-checkpoint-postgres` 从 3.1.2 升级到 + 3.1.3。([#7511]) + +- **网关:** 对钉钉 stream 消息处理器任务保持强引用,并记录其失败。此前 stream + 循环用裸 `asyncio.create_task(...)` 把每一帧交给 + `client.background_task(...)`,结果被直接丢弃;asyncio 在任务被 await 之前只 + 持有弱引用,处理器可能在执行中途被垃圾回收并静默丢弃一条消息,其抛出的异常 + 也永远不会被观察到。任务现在进入 + `DingTalkChannel._background_tasks`,由 done 回调将其移出(集合保持有界), + `_log_task_error` 负责取出并记录异常,与子智能体执行器和 MCP 会话池使用的 + 跟踪方式一致。([#7429]) + +- **脚本:** 用新的测试套件固定 `scripts/test-config-version.sh` 包装脚本的 uv + 索引隔离。该包装脚本的存在意义是:主机上的 `uv` 指向屏蔽依赖的镜像时仍能运行 + `tests/test_config_version.py`,但此前没有任何东西固定这一契约:漏清 + `UV_INDEX`/`UV_EXTRA_INDEX_URL`/`UV_INDEX_URL` 中的任一名称,或漏掉 + `UV_DEFAULT_INDEX` 导出,都会悄悄恢复该包装脚本当初要解决的失败。新增的 + `test_test_config_version_script.py` 在 `PATH` 上用替身 `uv` 运行真实脚本, + 断言子进程实际收到的环境,并断言 `--deselect` 等参数会转发给 + `pytest -q tests/test_config_version.py` 调用。([#7453]) + +- **MCP:** 新增测试覆盖,表明其他事件循环上的等待者在刷新属主失败后会重试令牌 + 刷新——无论属主死于取消还是 `RuntimeError`。新的 `OAuthTokenManager` 用例先 + 挂起对令牌端点的首次 `post`,再让一个由独立 `asyncio.run` 调用方组成的 + `ThreadPoolExecutor` 并发发起,断言某个待决等待者会选出替代属主,且所有等待者 + 都带着刷新后的令牌恢复。动态上下文的 blocking-IO 测试现在把 + `_read_failures_are_fatal` 补丁为 false,并在 `finally` 块中释放被阻塞的 + inject,使场景在断言失败时也能清理。([#7458]) + +- **依赖:** 将 `tests/skills/requirements.txt` 中的 `pillow` 从 12.3.0 升级到 + 13.0.0。([#7443]) + +- **依赖:** 将前端的 `happy-dom` 从 20.11.1 升级到 21.0.4,并把装饰性的 + `FlipDisplay` 动画与 DOM 测试套件隔离。Happy DOM 21 的动画取消会 reject + `finished`,因此 `subtask-card` 与 `tool-call-details` 的内容/状态套件 + 现在换用 `tests/unit/helpers/flip-display.tsx` 替身,它通过 `uniqueKey` 保留 + 子元素、类名与键控重挂载,而不触发 Motion 的浏览器动画生命周期;真实动画行为 + 仍留在 Playwright,rejection 上报保持启用。([#7391]) + +- **lark:** 通过回归测试与文档固定 Lark CLI 下载中 `_publish_bytes_atomically` 的 + 失败契约。写入、fsync 或 `os.replace` 失败时原文件保持完好;移除暂存临时文件是 + 尽力而为的:即使 unlink 失败,甚至告警日志处理器本身抛出异常,最终抛出的仍是 + 原始发布错误(包括 `KeyboardInterrupt`)。测试还记录了 `mkstemp` 以 `0o600` 模式 + 发布、随后由调用方对最终路径执行 chmod。清理调用本身已在 #7200 中落地;本次变更 + 将其锁定以防回归。([#7329]) + +- **依赖:** 在 `frontend/package.json` 与 lockfile 中将 `next` 从 16.3.8 升级到 + 16.4.1。([#7384]) + +- **技能:** 在技能审查 CI 中独立于技能包审查运行根级公共技能脚本回归套件 + (`tests/skills/`)。新增的 `skill-tests` job 会同步 backend dev 依赖组并对 + `tests/skills` 运行 `pytest`,在缺少 `duckdb`、`openpyxl` 或 `requests` 时改为 + 失败而不是静默跳过数据分析测试,并设置 `PYTHONDONTWRITEBYTECODE=1`,使测试导入 + 不会在被审查的技能包里留下 `.pyc` 文件。对 `tests/skills/**` 的修改现在会触发 + 该 workflow;新的 `test_skill_script_ci.py` 固定该 job 会持续收集每个根级技能 + 测试模块,并继续受与技能包审查相同的 draft 过滤器约束。([#7350]) + +- **配置:** 新增 `make test-config-version` 目标,针对公共 PyPI 运行配置升级套件 + (`tests/test_config_version.py`)。此前在 `uv` 所指镜像会屏蔽依赖的主机上,该 + 套件会失败,且没有不动运维索引的解决办法;`scripts/test-config-version.sh` 会 + 取消设置 `UV_INDEX`、`UV_EXTRA_INDEX_URL` 和已弃用的 `UV_INDEX_URL`,导出 + `UV_DEFAULT_INDEX=https://pypi.org/simple`,并运行 + `uv run --locked pytest -q tests/test_config_version.py`,额外参数原样转发。 + 常规 `uv` 与 `config-upgrade` 调用仍保留主机配置的索引,`CONTRIBUTING.md` 已 + 记录这个可选启用的目标。([#7251]) + +- **依赖:** 在 backend lockfile 中将 `oauthlib` 从 3.3.1 升级到 4.0.0。该依赖是 + 间接依赖,因此只有 `backend/uv.lock` 发生变更。([#7240]) + +- **依赖:** 在 backend lockfile 中将 `certifi` 从 2026.4.22 升级到 2026.10.19, + 刷新其随包分发的 CA 证书列表。([#7239]) + +- **依赖:** 在 backend lockfile 中将 `pillow` 从 12.3.0 升级到 13.0.0。该依赖是 + 间接依赖,因此只有 `backend/uv.lock` 发生变更。([#7238]) + - **前端:** 移除已弃用且未使用的 `hast` 和 `@types/gsap` 包。现有 `@types/hast` 与 GSAP 自带类型继续提供所需声明。([#7108]) @@ -6001,18 +7908,22 @@ 谱,针对真实的启动 bootstrap、遗留行与线程删除预留/状态写入做演 练,并验证健康位置能在反复 bootstrap、降级与再升级后保持不变。仅 文档性质——修复本身已在 #5517 落地;无运行时行为变化。([#5518]) + - **重构:** 能力画廊与 MCP 连接校验的内部清理: 具名的状态/操作辅助函数配合提前返回,拆出独立的 `validate_mcp_connection()`(422 响应不变),共享 `installationQuery()`,并从 `catalogForServer()` 移除未使用的 display-name 参数。渲染出的 UI 与行为均无变化。([#5580]) + - **依赖:** 将 `/backend` 中的 `anyio` 从 4.13.0 升级到 4.14.2。 ([#5583]) + - **测试:** 新增由各沙箱 provider 共享的离线搜索契约测试。新的 112 用例参数化套件针对共享的 fixture 文件系统, 通过全部六个适配器执行相同的 `ls` / `glob` / `grep` 场景, 覆盖被忽略的后代、特殊路径、grep 范围、溢出、 传输失败与不可用的二进制。生产代码未变。([#5677]) + - **依赖:** `langgraph-checkpoint` 升级到 `>=4.2.0,<5.0`, postgres extra 的 `langgraph-checkpoint-postgres` 升级到 `>=3.1.2,<3.2`,使 `checkpoint_patches.py` 得以移除其 @@ -6020,6 +7931,7 @@ 弃”与普通值增量种子检测的修复。这些下限是关键的:缺少它们时,解 析器可能悄悄装上已不再打补丁的问题版本。行为不变——本地补丁此前只 是在掩盖该 bug。([#5734]) + - **测试:** 覆盖增量历史缓存的 full→delta 迁移,并钉住两个仍未修复 的上游 `DeltaChannel` 缺陷。迁移契约现在会在历史缓存关闭、冷启动 与热缓存三种状态下运行,断言物化后的历史与未缓存 saver 摘要一致; @@ -6102,6 +8014,7 @@ `make test-shard` 与 CI 的分片方式不变,live 与阻塞 I/O 测试仍被排除。 新增测试用离线 worker 替身固定了分片并行启动与“等待全部分片再报失败” 的行为。([#6324]) + - **集成:** Lark/Feishu CLI 的输出改为按 UTF-8 解码,不再使用宿主 locale。 `lark-cli`(通过 `@larksuite/cli` npm 包分发的原生二进制)与 npm 都会向管道 写入 UTF-8,但 `lark_cli.py` 中的每一处捕获都只传了 `text=True` 而未指定 @@ -9891,6 +11804,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#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 +[#5876]: https://github.com/bytedance/deer-flow/pull/5876 [#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5880]: https://github.com/bytedance/deer-flow/pull/5880 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 @@ -10129,6 +12043,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6324]: https://github.com/bytedance/deer-flow/pull/6324 [#6326]: https://github.com/bytedance/deer-flow/pull/6326 [#6328]: https://github.com/bytedance/deer-flow/pull/6328 +[#6329]: https://github.com/bytedance/deer-flow/pull/6329 [#6332]: https://github.com/bytedance/deer-flow/pull/6332 [#6333]: https://github.com/bytedance/deer-flow/pull/6333 [#6334]: https://github.com/bytedance/deer-flow/pull/6334 @@ -10230,6 +12145,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6501]: https://github.com/bytedance/deer-flow/pull/6501 [#6502]: https://github.com/bytedance/deer-flow/pull/6502 [#6503]: https://github.com/bytedance/deer-flow/pull/6503 +[#6504]: https://github.com/bytedance/deer-flow/pull/6504 [#6505]: https://github.com/bytedance/deer-flow/pull/6505 [#6506]: https://github.com/bytedance/deer-flow/pull/6506 [#6508]: https://github.com/bytedance/deer-flow/pull/6508 @@ -10243,6 +12159,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6519]: https://github.com/bytedance/deer-flow/pull/6519 [#6520]: https://github.com/bytedance/deer-flow/pull/6520 [#6521]: https://github.com/bytedance/deer-flow/pull/6521 +[#6522]: https://github.com/bytedance/deer-flow/pull/6522 [#6523]: https://github.com/bytedance/deer-flow/pull/6523 [#6524]: https://github.com/bytedance/deer-flow/pull/6524 [#6527]: https://github.com/bytedance/deer-flow/pull/6527 @@ -10318,9 +12235,11 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6673]: https://github.com/bytedance/deer-flow/pull/6673 [#6674]: https://github.com/bytedance/deer-flow/pull/6674 [#6676]: https://github.com/bytedance/deer-flow/pull/6676 +[#6680]: https://github.com/bytedance/deer-flow/pull/6680 [#6681]: https://github.com/bytedance/deer-flow/pull/6681 [#6683]: https://github.com/bytedance/deer-flow/pull/6683 [#6685]: https://github.com/bytedance/deer-flow/pull/6685 +[#6688]: https://github.com/bytedance/deer-flow/pull/6688 [#6689]: https://github.com/bytedance/deer-flow/pull/6689 [#6690]: https://github.com/bytedance/deer-flow/pull/6690 [#6691]: https://github.com/bytedance/deer-flow/pull/6691 @@ -10333,6 +12252,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6706]: https://github.com/bytedance/deer-flow/pull/6706 [#6709]: https://github.com/bytedance/deer-flow/pull/6709 [#6714]: https://github.com/bytedance/deer-flow/pull/6714 +[#6715]: https://github.com/bytedance/deer-flow/pull/6715 [#6716]: https://github.com/bytedance/deer-flow/pull/6716 [#6717]: https://github.com/bytedance/deer-flow/pull/6717 [#6719]: https://github.com/bytedance/deer-flow/pull/6719 @@ -10358,10 +12278,12 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6762]: https://github.com/bytedance/deer-flow/pull/6762 [#6764]: https://github.com/bytedance/deer-flow/pull/6764 [#6768]: https://github.com/bytedance/deer-flow/pull/6768 +[#6770]: https://github.com/bytedance/deer-flow/pull/6770 [#6772]: https://github.com/bytedance/deer-flow/pull/6772 [#6774]: https://github.com/bytedance/deer-flow/pull/6774 [#6776]: https://github.com/bytedance/deer-flow/pull/6776 [#6779]: https://github.com/bytedance/deer-flow/pull/6779 +[#6780]: https://github.com/bytedance/deer-flow/pull/6780 [#6781]: https://github.com/bytedance/deer-flow/pull/6781 [#6782]: https://github.com/bytedance/deer-flow/pull/6782 [#6783]: https://github.com/bytedance/deer-flow/pull/6783 @@ -10421,6 +12343,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6882]: https://github.com/bytedance/deer-flow/pull/6882 [#6885]: https://github.com/bytedance/deer-flow/pull/6885 [#6886]: https://github.com/bytedance/deer-flow/pull/6886 +[#6888]: https://github.com/bytedance/deer-flow/pull/6888 [#6891]: https://github.com/bytedance/deer-flow/pull/6891 [#6893]: https://github.com/bytedance/deer-flow/pull/6893 [#6894]: https://github.com/bytedance/deer-flow/pull/6894 @@ -10460,6 +12383,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6957]: https://github.com/bytedance/deer-flow/pull/6957 [#6959]: https://github.com/bytedance/deer-flow/pull/6959 [#6961]: https://github.com/bytedance/deer-flow/pull/6961 +[#6965]: https://github.com/bytedance/deer-flow/pull/6965 [#6967]: https://github.com/bytedance/deer-flow/pull/6967 [#6969]: https://github.com/bytedance/deer-flow/pull/6969 [#6971]: https://github.com/bytedance/deer-flow/pull/6971 @@ -10478,7 +12402,9 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7001]: https://github.com/bytedance/deer-flow/pull/7001 [#7002]: https://github.com/bytedance/deer-flow/pull/7002 [#7004]: https://github.com/bytedance/deer-flow/pull/7004 +[#7006]: https://github.com/bytedance/deer-flow/pull/7006 [#7014]: https://github.com/bytedance/deer-flow/pull/7014 +[#7016]: https://github.com/bytedance/deer-flow/pull/7016 [#7017]: https://github.com/bytedance/deer-flow/pull/7017 [#7018]: https://github.com/bytedance/deer-flow/pull/7018 [#7020]: https://github.com/bytedance/deer-flow/pull/7020 @@ -10487,33 +12413,50 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7031]: https://github.com/bytedance/deer-flow/pull/7031 [#7033]: https://github.com/bytedance/deer-flow/pull/7033 [#7035]: https://github.com/bytedance/deer-flow/pull/7035 +[#7037]: https://github.com/bytedance/deer-flow/pull/7037 [#7039]: https://github.com/bytedance/deer-flow/pull/7039 [#7040]: https://github.com/bytedance/deer-flow/pull/7040 -[#7048]: https://github.com/bytedance/deer-flow/pull/7048 [#7042]: https://github.com/bytedance/deer-flow/pull/7042 +[#7043]: https://github.com/bytedance/deer-flow/pull/7043 +[#7045]: https://github.com/bytedance/deer-flow/pull/7045 +[#7048]: https://github.com/bytedance/deer-flow/pull/7048 +[#7049]: https://github.com/bytedance/deer-flow/pull/7049 [#7051]: https://github.com/bytedance/deer-flow/pull/7051 [#7053]: https://github.com/bytedance/deer-flow/pull/7053 +[#7054]: https://github.com/bytedance/deer-flow/pull/7054 [#7057]: https://github.com/bytedance/deer-flow/pull/7057 [#7058]: https://github.com/bytedance/deer-flow/pull/7058 [#7059]: https://github.com/bytedance/deer-flow/pull/7059 [#7061]: https://github.com/bytedance/deer-flow/pull/7061 [#7065]: https://github.com/bytedance/deer-flow/pull/7065 +[#7071]: https://github.com/bytedance/deer-flow/pull/7071 [#7074]: https://github.com/bytedance/deer-flow/pull/7074 [#7075]: https://github.com/bytedance/deer-flow/pull/7075 [#7076]: https://github.com/bytedance/deer-flow/pull/7076 [#7078]: https://github.com/bytedance/deer-flow/pull/7078 +[#7081]: https://github.com/bytedance/deer-flow/pull/7081 [#7084]: https://github.com/bytedance/deer-flow/pull/7084 +[#7088]: https://github.com/bytedance/deer-flow/pull/7088 +[#7092]: https://github.com/bytedance/deer-flow/pull/7092 [#7093]: https://github.com/bytedance/deer-flow/pull/7093 [#7099]: https://github.com/bytedance/deer-flow/pull/7099 +[#7102]: https://github.com/bytedance/deer-flow/pull/7102 [#7104]: https://github.com/bytedance/deer-flow/pull/7104 [#7106]: https://github.com/bytedance/deer-flow/pull/7106 [#7108]: https://github.com/bytedance/deer-flow/pull/7108 +[#7110]: https://github.com/bytedance/deer-flow/pull/7110 [#7112]: https://github.com/bytedance/deer-flow/pull/7112 +[#7115]: https://github.com/bytedance/deer-flow/pull/7115 [#7116]: https://github.com/bytedance/deer-flow/pull/7116 [#7117]: https://github.com/bytedance/deer-flow/pull/7117 [#7122]: https://github.com/bytedance/deer-flow/pull/7122 [#7123]: https://github.com/bytedance/deer-flow/pull/7123 [#7127]: https://github.com/bytedance/deer-flow/pull/7127 +[#7128]: https://github.com/bytedance/deer-flow/pull/7128 +[#7132]: https://github.com/bytedance/deer-flow/pull/7132 +[#7134]: https://github.com/bytedance/deer-flow/pull/7134 +[#7136]: https://github.com/bytedance/deer-flow/pull/7136 +[#7138]: https://github.com/bytedance/deer-flow/pull/7138 [#7140]: https://github.com/bytedance/deer-flow/pull/7140 [#7141]: https://github.com/bytedance/deer-flow/pull/7141 [#7144]: https://github.com/bytedance/deer-flow/pull/7144 @@ -10521,38 +12464,219 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7150]: https://github.com/bytedance/deer-flow/pull/7150 [#7151]: https://github.com/bytedance/deer-flow/pull/7151 [#7152]: https://github.com/bytedance/deer-flow/pull/7152 +[#7153]: https://github.com/bytedance/deer-flow/pull/7153 [#7154]: https://github.com/bytedance/deer-flow/pull/7154 [#7155]: https://github.com/bytedance/deer-flow/pull/7155 +[#7159]: https://github.com/bytedance/deer-flow/pull/7159 +[#7161]: https://github.com/bytedance/deer-flow/pull/7161 [#7163]: https://github.com/bytedance/deer-flow/pull/7163 +[#7164]: https://github.com/bytedance/deer-flow/pull/7164 +[#7165]: https://github.com/bytedance/deer-flow/pull/7165 [#7167]: https://github.com/bytedance/deer-flow/pull/7167 +[#7169]: https://github.com/bytedance/deer-flow/pull/7169 [#7170]: https://github.com/bytedance/deer-flow/pull/7170 +[#7172]: https://github.com/bytedance/deer-flow/pull/7172 +[#7175]: https://github.com/bytedance/deer-flow/pull/7175 +[#7180]: https://github.com/bytedance/deer-flow/pull/7180 +[#7182]: https://github.com/bytedance/deer-flow/pull/7182 +[#7183]: https://github.com/bytedance/deer-flow/pull/7183 +[#7186]: https://github.com/bytedance/deer-flow/pull/7186 +[#7189]: https://github.com/bytedance/deer-flow/pull/7189 +[#7193]: https://github.com/bytedance/deer-flow/pull/7193 +[#7196]: https://github.com/bytedance/deer-flow/pull/7196 +[#7198]: https://github.com/bytedance/deer-flow/pull/7198 +[#7200]: https://github.com/bytedance/deer-flow/pull/7200 +[#7202]: https://github.com/bytedance/deer-flow/pull/7202 +[#7203]: https://github.com/bytedance/deer-flow/pull/7203 [#7205]: https://github.com/bytedance/deer-flow/pull/7205 [#7207]: https://github.com/bytedance/deer-flow/pull/7207 +[#7209]: https://github.com/bytedance/deer-flow/pull/7209 +[#7213]: https://github.com/bytedance/deer-flow/pull/7213 +[#7214]: https://github.com/bytedance/deer-flow/pull/7214 [#7217]: https://github.com/bytedance/deer-flow/pull/7217 +[#7221]: https://github.com/bytedance/deer-flow/pull/7221 +[#7228]: https://github.com/bytedance/deer-flow/pull/7228 +[#7229]: https://github.com/bytedance/deer-flow/pull/7229 +[#7231]: https://github.com/bytedance/deer-flow/pull/7231 +[#7233]: https://github.com/bytedance/deer-flow/pull/7233 +[#7234]: https://github.com/bytedance/deer-flow/pull/7234 +[#7236]: https://github.com/bytedance/deer-flow/pull/7236 +[#7238]: https://github.com/bytedance/deer-flow/pull/7238 +[#7239]: https://github.com/bytedance/deer-flow/pull/7239 +[#7240]: https://github.com/bytedance/deer-flow/pull/7240 +[#7245]: https://github.com/bytedance/deer-flow/pull/7245 +[#7247]: https://github.com/bytedance/deer-flow/pull/7247 +[#7249]: https://github.com/bytedance/deer-flow/pull/7249 +[#7251]: https://github.com/bytedance/deer-flow/pull/7251 +[#7253]: https://github.com/bytedance/deer-flow/pull/7253 +[#7254]: https://github.com/bytedance/deer-flow/pull/7254 [#7255]: https://github.com/bytedance/deer-flow/pull/7255 +[#7256]: https://github.com/bytedance/deer-flow/pull/7256 +[#7257]: https://github.com/bytedance/deer-flow/pull/7257 [#7258]: https://github.com/bytedance/deer-flow/pull/7258 +[#7259]: https://github.com/bytedance/deer-flow/pull/7259 +[#7261]: https://github.com/bytedance/deer-flow/pull/7261 +[#7264]: https://github.com/bytedance/deer-flow/pull/7264 +[#7266]: https://github.com/bytedance/deer-flow/pull/7266 +[#7269]: https://github.com/bytedance/deer-flow/pull/7269 +[#7271]: https://github.com/bytedance/deer-flow/pull/7271 +[#7273]: https://github.com/bytedance/deer-flow/pull/7273 +[#7274]: https://github.com/bytedance/deer-flow/pull/7274 [#7277]: https://github.com/bytedance/deer-flow/pull/7277 +[#7279]: https://github.com/bytedance/deer-flow/pull/7279 +[#7281]: https://github.com/bytedance/deer-flow/pull/7281 +[#7284]: https://github.com/bytedance/deer-flow/pull/7284 +[#7286]: https://github.com/bytedance/deer-flow/pull/7286 +[#7289]: https://github.com/bytedance/deer-flow/pull/7289 +[#7290]: https://github.com/bytedance/deer-flow/pull/7290 +[#7291]: https://github.com/bytedance/deer-flow/pull/7291 +[#7293]: https://github.com/bytedance/deer-flow/pull/7293 +[#7294]: https://github.com/bytedance/deer-flow/pull/7294 +[#7295]: https://github.com/bytedance/deer-flow/pull/7295 +[#7297]: https://github.com/bytedance/deer-flow/pull/7297 +[#7299]: https://github.com/bytedance/deer-flow/pull/7299 +[#7301]: https://github.com/bytedance/deer-flow/pull/7301 +[#7304]: https://github.com/bytedance/deer-flow/pull/7304 +[#7305]: https://github.com/bytedance/deer-flow/pull/7305 +[#7306]: https://github.com/bytedance/deer-flow/pull/7306 +[#7307]: https://github.com/bytedance/deer-flow/pull/7307 +[#7308]: https://github.com/bytedance/deer-flow/pull/7308 +[#7311]: https://github.com/bytedance/deer-flow/pull/7311 +[#7312]: https://github.com/bytedance/deer-flow/pull/7312 +[#7313]: https://github.com/bytedance/deer-flow/pull/7313 [#7325]: https://github.com/bytedance/deer-flow/pull/7325 [#7326]: https://github.com/bytedance/deer-flow/pull/7326 +[#7328]: https://github.com/bytedance/deer-flow/pull/7328 +[#7329]: https://github.com/bytedance/deer-flow/pull/7329 +[#7333]: https://github.com/bytedance/deer-flow/pull/7333 +[#7338]: https://github.com/bytedance/deer-flow/pull/7338 +[#7340]: https://github.com/bytedance/deer-flow/pull/7340 +[#7342]: https://github.com/bytedance/deer-flow/pull/7342 +[#7345]: https://github.com/bytedance/deer-flow/pull/7345 +[#7347]: https://github.com/bytedance/deer-flow/pull/7347 +[#7348]: https://github.com/bytedance/deer-flow/pull/7348 +[#7349]: https://github.com/bytedance/deer-flow/pull/7349 +[#7350]: https://github.com/bytedance/deer-flow/pull/7350 +[#7351]: https://github.com/bytedance/deer-flow/pull/7351 +[#7352]: https://github.com/bytedance/deer-flow/pull/7352 +[#7353]: https://github.com/bytedance/deer-flow/pull/7353 +[#7354]: https://github.com/bytedance/deer-flow/pull/7354 +[#7355]: https://github.com/bytedance/deer-flow/pull/7355 +[#7356]: https://github.com/bytedance/deer-flow/pull/7356 +[#7357]: https://github.com/bytedance/deer-flow/pull/7357 +[#7358]: https://github.com/bytedance/deer-flow/pull/7358 +[#7360]: https://github.com/bytedance/deer-flow/pull/7360 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 +[#7363]: https://github.com/bytedance/deer-flow/pull/7363 [#7364]: https://github.com/bytedance/deer-flow/pull/7364 +[#7369]: https://github.com/bytedance/deer-flow/pull/7369 +[#7370]: https://github.com/bytedance/deer-flow/pull/7370 +[#7371]: https://github.com/bytedance/deer-flow/pull/7371 +[#7372]: https://github.com/bytedance/deer-flow/pull/7372 +[#7374]: https://github.com/bytedance/deer-flow/pull/7374 +[#7377]: https://github.com/bytedance/deer-flow/pull/7377 +[#7378]: https://github.com/bytedance/deer-flow/pull/7378 +[#7383]: https://github.com/bytedance/deer-flow/pull/7383 +[#7384]: https://github.com/bytedance/deer-flow/pull/7384 +[#7388]: https://github.com/bytedance/deer-flow/pull/7388 +[#7390]: https://github.com/bytedance/deer-flow/pull/7390 +[#7391]: https://github.com/bytedance/deer-flow/pull/7391 +[#7393]: https://github.com/bytedance/deer-flow/pull/7393 +[#7394]: https://github.com/bytedance/deer-flow/pull/7394 +[#7395]: https://github.com/bytedance/deer-flow/pull/7395 +[#7397]: https://github.com/bytedance/deer-flow/pull/7397 +[#7399]: https://github.com/bytedance/deer-flow/pull/7399 [#7400]: https://github.com/bytedance/deer-flow/pull/7400 +[#7404]: https://github.com/bytedance/deer-flow/pull/7404 +[#7407]: https://github.com/bytedance/deer-flow/pull/7407 +[#7409]: https://github.com/bytedance/deer-flow/pull/7409 [#7410]: https://github.com/bytedance/deer-flow/pull/7410 +[#7411]: https://github.com/bytedance/deer-flow/pull/7411 +[#7412]: https://github.com/bytedance/deer-flow/pull/7412 +[#7417]: https://github.com/bytedance/deer-flow/pull/7417 +[#7418]: https://github.com/bytedance/deer-flow/pull/7418 +[#7423]: https://github.com/bytedance/deer-flow/pull/7423 +[#7428]: https://github.com/bytedance/deer-flow/pull/7428 +[#7429]: https://github.com/bytedance/deer-flow/pull/7429 +[#7430]: https://github.com/bytedance/deer-flow/pull/7430 +[#7432]: https://github.com/bytedance/deer-flow/pull/7432 +[#7441]: https://github.com/bytedance/deer-flow/pull/7441 +[#7442]: https://github.com/bytedance/deer-flow/pull/7442 +[#7443]: https://github.com/bytedance/deer-flow/pull/7443 [#7445]: https://github.com/bytedance/deer-flow/pull/7445 +[#7446]: https://github.com/bytedance/deer-flow/pull/7446 [#7449]: https://github.com/bytedance/deer-flow/pull/7449 +[#7450]: https://github.com/bytedance/deer-flow/pull/7450 +[#7452]: https://github.com/bytedance/deer-flow/pull/7452 +[#7453]: https://github.com/bytedance/deer-flow/pull/7453 +[#7455]: https://github.com/bytedance/deer-flow/pull/7455 +[#7456]: https://github.com/bytedance/deer-flow/pull/7456 +[#7458]: https://github.com/bytedance/deer-flow/pull/7458 +[#7462]: https://github.com/bytedance/deer-flow/pull/7462 +[#7465]: https://github.com/bytedance/deer-flow/pull/7465 [#7467]: https://github.com/bytedance/deer-flow/pull/7467 +[#7473]: https://github.com/bytedance/deer-flow/pull/7473 +[#7474]: https://github.com/bytedance/deer-flow/pull/7474 +[#7483]: https://github.com/bytedance/deer-flow/pull/7483 +[#7484]: https://github.com/bytedance/deer-flow/pull/7484 +[#7485]: https://github.com/bytedance/deer-flow/pull/7485 +[#7489]: https://github.com/bytedance/deer-flow/pull/7489 +[#7492]: https://github.com/bytedance/deer-flow/pull/7492 +[#7496]: https://github.com/bytedance/deer-flow/pull/7496 +[#7498]: https://github.com/bytedance/deer-flow/pull/7498 +[#7499]: https://github.com/bytedance/deer-flow/pull/7499 +[#7500]: https://github.com/bytedance/deer-flow/pull/7500 +[#7501]: https://github.com/bytedance/deer-flow/pull/7501 +[#7503]: https://github.com/bytedance/deer-flow/pull/7503 +[#7504]: https://github.com/bytedance/deer-flow/pull/7504 +[#7505]: https://github.com/bytedance/deer-flow/pull/7505 [#7507]: https://github.com/bytedance/deer-flow/pull/7507 +[#7510]: https://github.com/bytedance/deer-flow/pull/7510 +[#7511]: https://github.com/bytedance/deer-flow/pull/7511 +[#7512]: https://github.com/bytedance/deer-flow/pull/7512 +[#7513]: https://github.com/bytedance/deer-flow/pull/7513 +[#7516]: https://github.com/bytedance/deer-flow/pull/7516 +[#7517]: https://github.com/bytedance/deer-flow/pull/7517 [#7521]: https://github.com/bytedance/deer-flow/pull/7521 [#7530]: https://github.com/bytedance/deer-flow/pull/7530 +[#7532]: https://github.com/bytedance/deer-flow/pull/7532 +[#7542]: https://github.com/bytedance/deer-flow/pull/7542 +[#7543]: https://github.com/bytedance/deer-flow/pull/7543 +[#7546]: https://github.com/bytedance/deer-flow/pull/7546 +[#7548]: https://github.com/bytedance/deer-flow/pull/7548 +[#7553]: https://github.com/bytedance/deer-flow/pull/7553 +[#7554]: https://github.com/bytedance/deer-flow/pull/7554 +[#7555]: https://github.com/bytedance/deer-flow/pull/7555 +[#7556]: https://github.com/bytedance/deer-flow/pull/7556 [#7557]: https://github.com/bytedance/deer-flow/pull/7557 [#7560]: https://github.com/bytedance/deer-flow/pull/7560 +[#7561]: https://github.com/bytedance/deer-flow/pull/7561 +[#7564]: https://github.com/bytedance/deer-flow/pull/7564 +[#7566]: https://github.com/bytedance/deer-flow/pull/7566 +[#7567]: https://github.com/bytedance/deer-flow/pull/7567 +[#7569]: https://github.com/bytedance/deer-flow/pull/7569 +[#7572]: https://github.com/bytedance/deer-flow/pull/7572 [#7576]: https://github.com/bytedance/deer-flow/pull/7576 +[#7579]: https://github.com/bytedance/deer-flow/pull/7579 [#7580]: https://github.com/bytedance/deer-flow/pull/7580 +[#7582]: https://github.com/bytedance/deer-flow/pull/7582 +[#7585]: https://github.com/bytedance/deer-flow/pull/7585 +[#7586]: https://github.com/bytedance/deer-flow/pull/7586 [#7588]: https://github.com/bytedance/deer-flow/pull/7588 +[#7589]: https://github.com/bytedance/deer-flow/pull/7589 [#7591]: https://github.com/bytedance/deer-flow/pull/7591 [#7593]: https://github.com/bytedance/deer-flow/pull/7593 [#7594]: https://github.com/bytedance/deer-flow/pull/7594 [#7597]: https://github.com/bytedance/deer-flow/pull/7597 +[#7604]: https://github.com/bytedance/deer-flow/pull/7604 [#7606]: https://github.com/bytedance/deer-flow/pull/7606 +[#7609]: https://github.com/bytedance/deer-flow/pull/7609 +[#7611]: https://github.com/bytedance/deer-flow/pull/7611 +[#7612]: https://github.com/bytedance/deer-flow/pull/7612 +[#7617]: https://github.com/bytedance/deer-flow/pull/7617 +[#7622]: https://github.com/bytedance/deer-flow/pull/7622 +[#7628]: https://github.com/bytedance/deer-flow/pull/7628 [#7631]: https://github.com/bytedance/deer-flow/pull/7631 +[#7643]: https://github.com/bytedance/deer-flow/pull/7643 [#7651]: https://github.com/bytedance/deer-flow/pull/7651 +[#7659]: https://github.com/bytedance/deer-flow/pull/7659