bump_version.sh now also rewrites backend/packages/harness/pyproject.toml
(deerflow-harness is a workspace member, so `uv lock` moves its
backend/uv.lock entry too), and verify_versions.sh gates on it in both
tag and mutual-equality modes. RELEASING.md and AGENTS.md describe the
five version sources.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* chore(changelog): Sync the change log information of 2.2.0
* chore(changelog): update the change log information of 2.2.0
* chore(changelog): sync the change log information of 2.2.0
* Add entry for pull request #7585 in CHANGELOG_zh.md
* fix(tui): show picker labels as written instead of parsing markup
SelectScreen passed thread titles and model display names to Textual as
plain str, which Textual parses as content markup. A thread title such as
'Fix [/bold] in parser' raised MarkupError and exited the app whenever
/threads opened, and '[a]' vanished from the list. Wrap the title and
option labels in Rich Text so they render literally.
* docs(changelog): reference #7631 for the TUI picker markup fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Both shipped compose files used the floating nginx:alpine tag. When the
alias moved to the 1.31.3 alpine build (musl 1.2.6), nginx's pid-file
pwrite() started issuing pwritev2(RWF_NOAPPEND), which the default
seccomp profile of older Docker releases (and EOL kernels like CentOS 7)
does not allowlist. The master then dies with
pwrite() "/run/nginx.pid" failed (1: Operation not permitted)
and the container restart-loops, so make docker-start / make up never
become healthy. The image cannot fix this (nginx/docker-nginx#1059);
1.31.2 is the last alpine build on musl 1.2.5, whose pwrite() still uses
pwrite64.
Pins both compose files to nginx:1.31.2-alpine in lockstep and adds a
regression test that rejects any floating nginx tag in either file.
* fix(channels): give DingTalk AI cards the channel token, not the SDK fetch
AICardReplier's async card calls read dingtalk_client.get_access_token()
inline, and dingtalk-stream refreshes it with requests.post and no
timeout. Card creation and card streaming both run on the Gateway loop,
so the first card and every SDK refresh after it (about every two hours)
blocked the loop for the whole token request, without bound if DingTalk
stalled.
Hand the replier a _CardTokenClient that serves the channel's own
httpx token (10s timeout), set before card creation and before every
streamed update. A token failure while streaming now raises into send's
existing sampleMarkdown fallback instead of being skipped silently.
Pin it with a strict blocking-I/O anchor over a real DingTalkStreamClient
and AICardReplier, a gate meta-check, and unit tests for token rotation
and failure. LocalHttpServer also records each request's method, path
and headers.
* docs(changelog): reference #7597 for the DingTalk AI card token fetch
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
`ArtifactFileDetail` and `loadArtifactContentFromToolCall` re-parsed
`write-file:` URLs with `new URL` + `decodeURIComponent` after
`parseWriteFileArtifact` had already rejected malformed values. The panel
restores its selection from session storage and only type-checks it as a
string, so a stale or hand-edited `write-file:%E0%A4%A` (or
`write-file://[`) threw `URIError`/`TypeError` during render.
Export the guarded parser and read `path` / `tool_call_id` from one parsed
result in both places; a rejected URL degrades to the raw string or
`undefined` instead of taking the panel down.
* fix(scripts): read the checkout .env as data in serve.sh
serve.sh sourced the root .env under set -a, so bash ran it as a script:
$$ became the shell's PID, backticks and $(...) ran commands, and an
unquoted value with a space ran its second word as a command (exit 127
under set -e, stopping make dev). The exports also beat the Gateway's
own load_dotenv(override=False), so e.g. a $$ in AUTH_JWT_SECRET changed
the signing secret on every launch.
Parse .env with a stdlib-only scripts/dotenv_exports.py on the host
Python and eval its shell-quoted export lines. It tokenizes like
python-dotenv, so values match what the Gateway reads, and keeps the
launcher's precedence over inherited variables and its $NAME /
${NAME:-x} / leading-~ expansion that support_bundle.py mirrors. $$ is
now one token in both. Unparseable lines and invalid names are skipped
with warnings that never echo values; a non-UTF-8 file stops the
launcher.
Tests run serve.sh's real .env block: no execution, parity with
python-dotenv, precedence, warnings, the non-UTF-8 stop and a stdlib-only
run; the support-bundle launcher cases now exercise the parser.
* docs(changelog): reference #7606 for serve.sh reading .env as data
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(extensions): add durable team public state
* test(channels): wait for each artifact reply
* revert: remove unrelated channel artifact test changes
Revert 213addd3c72be08246287445d9817c8856f36e40 so the channel test fix is maintained separately on main. Keep the public-state feature and its tests unchanged.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Translated zh pages link to the English docs routes (/docs/...) but kept
localized Chinese anchor slugs, which do not exist on the English pages.
Replace each Chinese anchor with the matching English heading slug
(verified present in the target file): a-run-fails-after-a-config-edit,
forks-are-accepted-not-refused, placements, failure-isolation, runaway-guards.
Co-authored-by: leom1919 <leom1919@users.noreply.github.com>
Declare required thinking, native reasoning effort and image support for both
K3.1 Pro IDs. Normalize fixed parameters at the final Moonshot request boundary
and cover real SDK serialization with offline regression tests.
Fixes#7425
* fix(channels): resolve reply artifacts off the event loop
_handle_chat_on_thread and _handle_streaming_chat called
_prepare_artifact_delivery inline. For every presented file it resolves
the virtual path (realpath), resolves the outputs root, and checks the
file with is_file and stat, all on the Gateway loop that also serves
every other channel and request.
Run it through asyncio.to_thread on both reply paths, and anchor both
through the real manager under the strict blocking-I/O gate.
* docs(changelog): reference #7594 for the off-loop reply artifact resolution
* docs(changelog): drop the duplicate #7580 link definition in the zh changelog
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(channels): write each Feishu thread alias once per streamed reply
Every outbound to a Feishu running card re-sent set_thread_id for all of
the reply's aliases (source message, card, topic, root, parent and Feishu
thread ids), although only updated_at changed after the first chunk.
That was about five store writes per streamed chunk: a full store.json
rewrite under the store lock each, or a database upsert.
Remember the aliases written for each in-flight reply and skip them on
non-final chunks. A failed write is not remembered, so the next chunk
retries it, and the final outbound still writes every alias, so the
store ends up as before.
* docs(changelog): reference #7593 for the Feishu streamed thread alias writes
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(wizard): replace retired Claude Sonnet 4 default with claude-sonnet-5-5
The setup wizard's Anthropic default model is now `claude-sonnet-5-5` instead of the retired `claude-sonnet-4-20250514`. A new per-model `model_omitted_config` field makes the wizard leave out the extended-thinking (`budget_tokens`) and thinking-disabled templates for that model only, since Sonnet 5.5 rejects them; the other Claude models are unchanged. The Anthropic examples in `config.example.yaml` and `configuration.mdx` (which used `claude-3-5-sonnet-20241022`) now use `claude-sonnet-5-5` without those settings, and the change also adds a regression test and a CHANGELOG entry.
Notice anthropic-retired-models-2026-10-27 about api.anthropic.com's API describes the change. It was read from a file and is not signed by the provider.
* Update CHANGELOG with PR reference #7585
Added a reference to pull request #7585 in the CHANGELOG.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(runtime): present outputs before a loop wrap-up and explain step-limit stops
Loop-detection warnings now ask the agent to present the files it already
created in /mnt/user-data/outputs before its final answer, when
present_files is available.
A run that exhausts recursion_limit now ends with stop_reason
recursion_capped and a plain message that lists outputs it created but
did not present. An active goal stands down without an evaluator call.
The /wait error envelope keeps the GraphRecursionError name, and IM
channels reply with the step-limit message instead of a generic failure.
* docs(changelog): note run-end delivery and step-limit stops (#7588)
* test(runtime): stop pinning LangGraph's step count in the step-limit test
The produced paths and the message already follow the observed model
calls, so the exact count only pinned LangGraph's per-step accounting.
Also note on the step-limit goal branch that its caller passes no
evaluator factory, so it has to stay ahead of the evaluator section.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(api): survive a malformed CSRF cookie
`readCsrfCookie` percent-decodes a cookie the gateway sets verbatim.
Cookies are shared by every app on the same host (ports included), so a
stray or truncated `%` escape from another local app, a proxy, or a stale
value makes `decodeURIComponent` throw `URIError` inside `fetch` and
`getCsrfHeaders` — every state-changing call fails with an exception
before the gateway can answer 403.
Fall back to the raw cookie value when decoding fails, matching
`getLocaleFromCookie` (#7553).
* test(api): exercise sibling CSRF cookies in the DOM
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(channels): keep IM binding codes on screen and report goal state in replies
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 disappears.
The bots' /connect replies and the IM /goal replies now use the owner's
web UI language (else channel_connections.notification_locale), and /goal
reports the goal's state as the web goal bar does: in progress, why it
stopped, or that it was met.
* docs(changelog): note the IM binding dialog and localized IM replies (#7591)
* test(channels): check IM reply tables against every notification locale
The connect-reply check compared a fixed en-US/zh-CN pair, and the /goal
reply table had none. Both now match NOTIFICATION_LOCALES with the same
keys in each, like the existing _GOAL_REASON_TEXT check, so a new locale
fails here instead of raising KeyError at reply time.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
agentconnectprotocol.org no longer resolves (NXDOMAIN). The integration
depends on agent-client-protocol (Agent Client Protocol by Zed
Industries), so point both en and zh tool guides at its canonical
site https://agentclientprotocol.com and use the protocol's actual
name.
* fix(sandbox): require a path start before translating virtual and host roots
Path translation only checked the right-hand segment boundary (#4035,
#4053), so a root embedded in a longer path or URL was rewritten too.
DeerFlow's own artifact URLs contain /mnt/user-data, so
curl .../artifacts/mnt/user-data/outputs/x.html had the thread's host
directory spliced into the URL, and a mount at /data turned an
unrelated /var/data/app.log into /var/mnt/data/app.log in model-visible
output.
Add a shared left-hand PATH_START_BOUNDARY: a root must not follow a
word character, '.', '~' or '-'. '/' is still allowed so
file:///mnt/user-data keeps mapping. It applies to the bash command
rewrite, LocalSandbox command/content resolution, and both reverse
masking paths.
* refactor(sandbox): derive the path-start lookbehind from one class
Build PATH_START_BOUNDARY from _PATH_START_BLOCKERS so the regex and
scanner paths cannot drift, and document the attached short-option
value (-o/data/x) as a known false negative of the start rule.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(checkpoints): reject malformed Redis history write records
Validate pending-write tuples before optional Redis history entries reach
delta materialization. Treat the whole malformed entry as a miss so the
authoritative saver rebuilds it without dropping or inventing state.
Cover actual checkpoint reads, whole-entry replacement, valid tuple/list
records, neighbor preservation and warm-cache rereads.
* fix(checkpoints): bind cached history writes to their channel
* fix(checkpoints): keep cache warning failures best-effort
Guard warning handlers across decode, ingestion, serialization, Redis outages and purge. Preserve authoritative rebuilds and valid neighboring writes, with native graph regression coverage.
* docs(gateway): keep guidance below the hard size budget
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(streaming): make Redis stream END publication idempotent
* fix(streaming): align Redis test fake with Lua END checks
Remove the unreachable owned missing-stream branch, mirror tail-only data checks in both fake script paths, and pin legacy END and owned missing-buffer behavior against real Redis.
* fix(streaming): gate legacy END scans after writer rollout
* fix(helm): synchronize embedded config version with schema
* test(runtime): use real stream bridge config in close coverage
Keep new configuration defaults in lifecycle fixtures and surface owner-task failures instead of masking them as event timeouts. Drain every helper task and cover context entry/close failures.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(community): resolve async tool config off the event loop
Agent runs execute on the Gateway event loop, and the async community
tools resolved their tool config through get_app_config() inline. That
call stats and hashes config.yaml and extensions_config.json on every
call and reparses them after an edit, so each tool call held the loop
for it, up to three times per call: Tavily, SearXNG and Firecrawl read
the config again to build their client, InfoQuest read it three times,
and the browser tools read the launch config for every action.
Resolve it through asyncio.to_thread once per call, as image_search and
groundroute already do, and build the client from that same read. This
covers jina_ai, crawl4ai, browserless, tavily, searxng, firecrawl,
infoquest, ragflow, lightrag and browser_automation.
* docs(changelog): reference #7580 for the off-loop community tool config reads
* docs(changelog): use the existing community-tools label in the zh entry for #7580
* fix(community): resolve the jsonify dataset_rows config off the event loop
dataset_rows_tool is async and called _load_settings() inline, so every
call ran get_app_config()'s stat + sha256 revalidation on the loop.
Read it through asyncio.to_thread like the other async community tools,
and add the tool to the off-loop anchors.
* docs(changelog): include Jsonify in #7580 and drop trailing whitespace
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(i18n): survive a malformed locale cookie
getLocaleFromCookie decoded the raw cookie value without a guard, so a
malformed percent-encoding threw URIError during locale bootstrap and in
formatTimeAgo. It also split the cookie on every "=", truncating values
that contain one. Decode defensively and split on the first "=" only.
* fix(i18n): normalize the cookie locale in formatTimeAgo
The second caller of getLocaleFromCookie cast the raw value to Locale, so a
malformed-but-known-prefixed cookie skipped normalization. Normalize it like
useI18n does, and cover the malformed and valid cookie paths.
---------
Co-authored-by: 一片一片 <192845873+gdut4140@users.noreply.github.com>
* fix(setup-agent): keep a failed discard from replacing the publish outcome
* test(setup-agent): pin the post-publish discard and assert the real temp name
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(subagents): drain final cleanup across caller cancellation
* fix(subagents): preserve stop outcomes during cancellation
Follow up on PR #7546 discussion_r4374235193 and Issue #7535.
Capture the unwinding cancellation before the drained cleanup tail and use it
only while the result remains non-terminal. Preserve completed, failed and
timed-out decisions. Keep cancellation propagation and cleanup ownership.
Share the synchronous result holder and publish its existing FAILED outcome
before requesting cancellation. This necessary timeout boundary overlaps #7483;
no active-cancel terminal write is added to the cleanup tail.
Validation: 12 targeted regressions pass on Windows and Linux; make test
37876 passed / 808 skipped, make test-blocking-io 292 passed / 1 skipped.
Format, lint, diff checks and four-file incremental OCR delegated review pass.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>