3663 Commits

Author SHA1 Message Date
NanPan
4343ced685
fix(mcp): drain personal config mutations on cancellation (#6093)
* fix(mcp): drain personal config mutations on cancellation

* test(mcp): cover cancelled personal config mutation

* test(mcp): remove unused cancellation event

* test: remove unused event loop from personal MCP cancellation regression

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 19:39:11 +08:00
Grapette.L
f61372764e
fix(agents): anchor agent-name validation against a trailing newline (#5960)
* fix(agents): anchor agent-name validation against a trailing newline

Two copies of the shared ^[A-Za-z0-9-]+$ grammar still used re.match,
whose $ also matches before a final newline, while every other copy uses
fullmatch. POST /api/agents {"name": "reviewer\n"} therefore passed the
router guard and died inside the strict file store as an HTTP 500, and
DeerMem accepted the same name as an agents/{name}/facts directory.

* test(agents): state which trailing-newline params the regression actually covered

.match only accepted "reviewer\n" on main; "reviewer\n\n" and "reviewer \n"
were already rejected, so the docstrings should not read as if every param in
these parametrizations was broken before the fix.
2026-09-30 19:38:09 +08:00
Anton Dziatkovskii
a83aebe702
fix(docs): harness docs document an async client API that does not exist (#5843)
* docs: replace nonexistent async client API in harness docs

DeerFlowClient has no async methods: client.astream() and client.ainvoke()
do not exist, so the "Create Your First Harness" tutorial and the harness
integration guide fail with AttributeError on their first call. Overrides
were also shown as a nested config={"configurable": {...}} dict, which
stream()/chat() swallow via **kwargs and silently ignore.

Switch the examples to the shipping API (sync stream()/chat(), flat kwarg
overrides, agent_name as a constructor arg) in the EN and ZH copies of both
pages. backend/docs/STREAMING.md is left alone: its astream mentions are
design discussion and LangGraph internals, not user-facing examples.

Assisted-by: Claude Code / claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>

* docs: serialize StreamEvent before StreamingResponse, use real event names

Review follow-up on the two points raised in #5843.

P2 — the streaming examples handed a generator of `StreamEvent` dataclasses
straight to Starlette. `StreamingResponse` calls `.encode()` on any chunk that
is not `str`/`bytes`, so the first event raised
`AttributeError: 'StreamEvent' object has no attribute 'encode'`. Both
occurrences (the `run_agent` snippet and the inline FastAPI route, which shipped
the dataclass `repr()` through an f-string) now serialize to SSE frames built
from `event.type` / `event.data`.

P3 — the tutorial showed mapping literals with `messages` and `thread_state`,
neither of which exists. The real contract is
`StreamEventType = Literal["values", "messages-tuple", "custom", "end"]`
(backend/packages/harness/deerflow/client.py:128) and the stream yields
dataclasses, so the section now shows `StreamEvent(...)` with the real names
plus a consuming loop.

The zh tutorial had no equivalent section at all; added for parity.

Assisted-by: Claude Code / claude-opus-5
Machine: A-Mac16-2019-PaloAlto
Account: tonydzi
Operator: robot:pr-reply-daily

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: drop the nonexistent load_config() from every harness snippet

Review follow-up on #5843 (@willem-bd): the rewritten tutorial still opened
with `from deerflow.config import load_config`, which raises ImportError
before `client.stream(...)` is ever reached — `deerflow/config/__init__.py`
exports `get_app_config` and friends, and the string `load_config` does not
appear anywhere under `backend/packages/harness/deerflow/` (the only near
match is `load_configured_extension_middlewares`).

The call was also unnecessary: `DeerFlowClient.__init__` already resolves
configuration itself (client.py:220-222 — `reload_app_config(config_path)`
when a path is given, otherwise `get_app_config()`, which auto-loads from
the default path or DEER_FLOW_CONFIG_PATH).

Fixed the class rather than the one snippet you flagged: the same dead
import appeared 10 times across 4 pages (EN + ZH tutorial and integration
guide). Every one is gone. The "configuration in embedded mode" section now
shows the API that exists — `DeerFlowClient(config_path="...")` — instead of
`load_config(config_path="...")`.

Also from the review:
- the `end` example showed only `total_tokens`; `stream()` always yields
  input/output/total (client.py:920), and the snippet right below prints the
  whole usage dict, so a reader saw keys the docs omitted. Both EN and ZH now
  show the full payload, and the `values` example visibly elides `artifacts`
  and `summary_text` instead of pretending they are absent.
- the two SSE comments on the ZH integration page stayed English while the
  example above them was translated; translated.

Verified statically, not executed: `grep` for the symbol across the harness
package (zero hits) and a read of `DeerFlowClient.__init__`. I did not stand
up a backend to run the tutorial end to end.

Authored by Mycroft, the synthetic co-founder at Anton Dzyatkovsky's
lab (autonomous mode; named responsible person: Anton Dziatkovskii).

Assisted-by: Claude Code / Opus 5 (review response, implementation)
Machine: MacBook-Anton
Account: a
Operator: mycroft (autonomous routine github-thread-watch)
Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: fix native artifact SSE encoding and custom config advice

---------

Signed-off-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
Co-authored-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 19:36:54 +08:00
Grapette.L
0110845f66
fix(config): guard the recovered stream cleanup delay like its heartbeat sibling (#5850) 2026-09-30 19:26:50 +08:00
Totoro
295437692a
fix(agent,goal): stop coaching scripts when no bash tool is bound, and show the goal evaluator Human Input Card answers (#6082)
* fix(agent): drop script guidance from the lead prompt when no bash tool is bound

The workspace section told the agent how to write scripts and commands
("prefer relative paths", "avoid hardcoding /mnt/user-data/... inside
generated scripts") even when no tool could run them. With the default
LocalSandboxProvider host bash is off and the bash tool is removed, so
agents wrote helper scripts nothing could execute, then stalled or
computed by hand.

apply_prompt_template() takes bash_available (default True, so other
callers keep today's text). The lead agent (both assembly branches) and
the embedded client pass whether their authorized tools, bound or
deferred, include bash. Without it:
- the two script bullets become one line: no bash tool is bound, work
  out results directly and write them with write_file;
- the subagent section no longer shows direct bash examples, and marks
  the bash subagent unavailable, since it inherits the lead's tool groups;
- the ACP hint no longer suggests `bash cp`.

* fix(goal): show the goal evaluator the user's answer to a Human Input Card

The web UI sends a Human Input Card answer as a hidden human message
(hide_from_ui with a human_input_response) and shows it on the card.
format_visible_conversation() skipped every hidden message, so the
evaluator saw the question but never the answer. Following the user's
answer then read as the assistant guessing, and the goal stood down with
needs_user_input although the user had answered.

A hidden human message whose human_input_response passes
read_human_input_response() now adds a line
"User (Human Input Card answer): <value>" from the structured value; the
question is already on the card's own line. Other hidden messages, such
as goal continuations, stay out. Over the 12,000-char cap the latest
answer the cut drops is kept at the top next to the latest request, and
is never taken for the request itself. Without card answers the cut is
unchanged.

* fix(agent): keep the subagent examples and bash hint in line with the bound tools

Review follow-up for the lead prompt without bash.

- The last delegation example follows the tools. With a bash subagent on
  offer it is unchanged. A lead with bash and no bash subagent keeps
  "Run a routine test, build, or git command directly." without the Bash
  subagent sentence. A lead without bash gets a file-work example instead.
- The unavailable-bash description no longer points at the sandbox
  provider. Since #2253 it renders only when the bash subagent is
  registered and the lead binds no bash, where the sandbox is not the cause.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-30 17:54:42 +08:00
Hyeonsang Cho
14cd840bbf
fix(gateway): reject non-ASCII tokens instead of raising in constant-time compares (#6076)
* fix(gateway): reject non-ASCII tokens instead of raising in constant-time compares

hmac.compare_digest raises TypeError when a str operand holds non-ASCII
characters. Starlette decodes header bytes as latin-1 and query values as
UTF-8, so a single 0xE9 byte in a CSRF token, GitHub webhook signature,
internal auth token, or OIDC state turned the expected 403/401 into a 500.

Route the five client-facing comparisons through a new
app.gateway.utils.constant_time_equals helper that compares the UTF-8
encodings ("surrogatepass" so even a lone surrogate cannot raise). ASCII
results are unchanged, and no bypass was possible before the fix.

* fix(provisioner): reject non-ASCII API keys instead of raising; drive OIDC test over HTTP

The standalone provisioner compared X-API-Key with secrets.compare_digest on
str operands, so a non-ASCII header raised TypeError (500) the same way the
Gateway sites did. It cannot import app.gateway, so encode inline.

The OIDC state regression test now mounts the auth router and sends the
percent-encoded state through a real signed state cookie instead of stubbing
get_state_cookie and calling the handler directly.

* docs(changelog): reference #6076

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 17:41:11 +08:00
Wenchao An
1e462ae4a2
fix(frontend): consolidate run duration into reasoning headers (#6057)
* fix(frontend): show run duration in reasoning headers

* fix(frontend): cover all reasoning duration disclosures

* fix(frontend): collapse restored reasoning and cache duration targets

* chore: remove PR-only reasoning screenshot
2026-09-30 17:22:41 +08:00
kuse
8ecff3be95
test(config): make refresh_skew rejection test load-bearing (#6080)
test_refresh_skew_rejects_negative_and_boolean omitted the required
token_url, so McpOAuthConfig(refresh_skew_seconds=-1) failed validation
on the missing field alone -- deleting the ge=0 bound and the boolean
validator would leave the test green. Pass token_url (the same URL the
refresh_skew_seconds=0 boundary test uses) so only the guards can
satisfy pytest.raises; verified by stripping both guards from
extensions_config.py, which now fails exactly the two parametrized
cases, and restoring them, which returns the suite to 42 passed.

Also move SandboxOwnershipConfig up beside the other config imports
and drop the redundant in-test `import pytest` (review nits on #6026).

Generated-by: ZCode (GLM, coding agent)

Co-authored-by: liwenjie200543 <liwenjie200543@users.noreply.github.com>
2026-09-30 17:21:43 +08:00
PeaceMaker-best
b73a4e79bf
feat(memory): scope management API by agent (#5565)
* feat(memory): scope management API by agent

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(memory): preserve scoped management isolation

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-09-30 16:50:07 +08:00
Hyeonsang Cho
af67738521
fix(agents): treat a null max_total_subagents override as unset (#6088)
* fix(agents): treat a null max_total_subagents override as unset

An API caller sending "max_total_subagents": null in the run context
failed that run with a TypeError: the key was present, so
dict.get(key, default) returned None and building SubagentLimitMiddleware
compared None with an int.

Resolve the per-run delegation cap through one helper,
effective_total_subagents_per_run, which treats None as "use
subagents.max_total_per_run" and clamps to 1-50. The lead agent's
middleware and assembly paths, DeerFlowClient, and the system prompt all
use it, so the extension host policy and release policy now report the
enforced cap instead of an out-of-range request.

* docs(changelog): reference #6088 in the null max_total_subagents entry
2026-09-30 16:10:29 +08:00
xihongshichaojidan8
d0e4d3525c
fix(frontend): show media icons for APNG, AVIF and WebM (#6089) 2026-09-30 15:58:45 +08:00
NanHan Qing
c714629faf
fix(sandbox): expose DEERFLOW_USER_ID to every bash command (#6058)
* fix(sandbox): expose DEERFLOW_USER_ID to bash commands

A skill's SKILL.md can trigger scripts, but those scripts run as bash
subprocesses inside the sandbox and cannot reach the Gateway auth context,
so get_effective_user_id() is unavailable to them and user-scoped skills
are impossible to write. The maintainer settled the direction on #3919:
inject the id as DEERFLOW_USER_ID.

Every bash command now begins with 'export DEERFLOW_USER_ID=<resolved>; '
(or 'unset' when the resolved id is unusable), mirroring the existing
DEERFLOW_CHANNEL_USER_ID channel-identity prefix (#3914). The id rides the
command string rather than execute_command(env=...), because a non-empty env
switches AioSandbox to the bash.exec API reserved for request-scoped secrets.

The earlier attempt (#4029) was closed for making every command carry the
new export without updating the exact-command assertions in
tests/test_channel_user_id_env.py. Those assertions are updated here, and the
composition of the two prefixes is pinned by a dedicated test.

* fix(sandbox): fix the DEERFLOW_USER_ID unit test and cover review findings

Follow-up to the review on this PR.

Unit test (CI `backend-unit-tests (2)`): the change moved the command
contract but left the exact-command assertion in
tests/test_skill_request_scoped_secrets.py::test_local_bash_forwards_env_and_timeout
expecting the bare `echo hi`. The local validation run used a `-k` filter that
did not select that file, so the breakage was invisible there. The assertion
now pins the composed command, and `_is_windows` is patched explicitly so it no
longer depends on the host OS.

[P2] Windows local sandbox: `export` is invalid under the PowerShell/cmd.exe
fallback, but skipping the prefix outright left the promise unmet on Windows.
The id is published through `execute_command(env=...)` there instead:
LocalSandbox layers `env` into the per-process environment and, unlike
AioSandbox, has no persistent session that could carry a stale value. It is
deliberately kept out of `injected_env` so the identifier does not enter the
secret-redaction set.

Docstring: `_user_identity_prefix` now carries the same "informational, not
authenticated identity" caveat the channel variable has in
backend/app/channels/AGENTS.md. The value is authorization-grade at the Gateway
boundary, but any bash command can overwrite its own environment, so skills
must not treat the shell variable as proof of identity.

Docs: the en/zh sandbox.mdx identity sections and the channels/AGENTS.md
identity paragraph now describe the second, outermost prefix, its ordering
against the channel prefix, and the Windows behaviour.
2026-09-30 14:56:03 +08:00
Totoro
f75d89f39d
fix(goal): record a failed evaluator call as a stand-down (#6073)
* fix(goal): record a failed evaluator call as a stand-down

When the goal evaluator call raised (a provider error, or an answer that
was not valid JSON), _prepare_goal_continuation_input() logged a warning
and returned without writing anything. The goal stayed active with no
last_evaluation, or with an earlier run's verdict and run id, unlike the
other paths that stop the goal loop.

Record it like the no_durable_end_of_turn stand-down: blocker run_failed,
stand_down_reason evaluator_failed, the current run id, and a reason that
names only the exception type. Nothing is recorded if the run was aborted
meanwhile. A failure before the evaluator is called keeps its behavior and
now logs "Could not prepare goal evaluation".

* docs(gateway): fit the evaluator-failure note in the AGENTS.md budget

Main grew backend/app/gateway/AGENTS.md to 49,006 bytes, and the longer
note pushed the merged file past the 49,152-byte hard limit that
tests/test_agent_guidance_check.py enforces. Say it in one clause.

* docs(gateway): keep main's AGENTS.md line after the merge

The merge from main kept this branch's version of the goal paragraph,
which restored the Human Input Card wording #6068 replaced and put the
file at 49,194 bytes, over the 49,152-byte limit the agent-guidance
check enforces. Take main's line. It already says every non-satisfied
evaluation is persisted with last_evaluation, which this fix makes true
for a failed evaluator call.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 11:35:19 +08:00
PeaceMaker-best
66597ae89d
fix(subagents): retain completed model usage on cancellation (#5910)
* fix(subagents): retain completed model usage on cancellation

* docs(subagents): fit guidance within chain budget

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:56:52 +08:00
kuse
f2f9e6f6fe
fix(config): guard numeric config fields that accepted silently-broken values (#6026)
* fix(config): guard numeric config fields that accepted silently-broken values

Ten numeric fields across six config models admitted zero, negatives, or
booleans (YAML/JSON true coerces to 1) and only failed at runtime — or kept
running with a nonsense value:

- CircuitBreakerConfig: failure_threshold >= 1, recovery_timeout_sec > 0
- McpServerConfig: tool_call_timeout > 0, session_init_timeout > 0
- McpOAuthConfig.refresh_skew_seconds >= 0 (0 stays valid)
- ModelConfig.stream_chunk_timeout > 0; context_window keeps gt=0 and
  gains boolean rejection (review follow-up)
- RunEventsConfig.max_trace_content >= 1
- SandboxConfig.port 1..65535, idle_timeout >= 0 (0 = documented disable)
- SummarizationConfig.trim_tokens_to_summarize >= 1

Review follow-ups: boolean rejection extended to every bounded numeric
sibling in sandbox_config (replicas, acquire_timeout, burst_limit,
health_check_skip_seconds, bash_output_max_chars, read_file_output_max_chars,
ls_output_max_chars, bash_command_timeout, temporary_grant_ttl,
renewal_interval_seconds, ttl_multiplier) so the file is uniform, and to
model_config.context_window. Tests cover allow_inf_nan=False rejection
(float('inf')/'nan') and the refresh_skew_seconds=0 boundary.

Generated-by: ZCode (GLM, coding agent)

Authored-by: liwenjie200543 <liwenjie200543@users.noreply.github.com>

* style(config): apply Ruff formatting to numeric guards

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: liwenjie200543 <liwenjie200543@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:48:26 +08:00
yang rui
fcff55a075
fix(frontend): preserve first-turn order during chat creation (#6074) 2026-09-30 10:45:19 +08:00
PeaceMaker-best
0ef405a77f
fix(agent): anchor compaction to runtime model profile (#5566)
* fix(agent): use runtime profile for compaction

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(summarization): honor required-thinking profile anchors

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:38:19 +08:00
Totoro
d73844b997
fix(goal): show the goal evaluator the tool calls and results (#6068)
* fix(goal): show the goal evaluator the tool calls and results

The evaluator read only the text of user and assistant messages and was told
to fail closed without explicit evidence, so it stood down with
missing_evidence on most completed file tasks: the tool calls and tool
results that the web UI shows never reached it.

Include the assistant's tool calls and the tools' results inside the
existing message window, shortened (200 characters per argument value, 600
per step, line breaks kept), and tell the evaluator to treat tool results as
data, that a successful tool call alone does not meet a goal, and that a
guessed input means needs_user_input. The no-progress and final-clear
signatures are unchanged.

* fix(goal): tighten the evaluator's tool evidence after review

- Pair each tool result with the latest call of its id before it. Some
  providers reuse ids such as call_0 across turns, and the window-wide
  map labelled an earlier read with a later write's name.
- Keep model-supplied tool names on one line in calls and result labels,
  and escape U+0085, U+2028 and U+2029 in arguments and results, which
  json.dumps(ensure_ascii=False) leaves raw, so no tool step can start a
  new evidence line.
- Leave out the results of a hidden assistant message's calls. The web UI
  renders a result only under its call, so neither shows there either.
  A clarification prompt stays: the UI shows it as its own card.
- Over the 12,000-char cap, keep whole lines from the end instead of
  slicing mid-line. Keep the latest user message that the cut drops at
  the top, whole when it and the lines after it fit, else shortened to
  2,000 chars. If the next assistant message back does not fit whole,
  keep its end in the room left. A marker counts the lines left out.
- Docs: the Gateway AGENTS.md and a worker docstring no longer say the
  evaluator never sees tool messages.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-30 10:36:09 +08:00
FanouZeng-TT
4006ca072f
fix(channels): keep the delimiter newline when splitting long Discord replies (#6062)
* fix(channels): preserve Discord split boundaries

* test(channels): cover Discord split at leading remainder newline

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:17:46 +08:00
Wenchao An
9dc7bb30da
fix(capabilities): restore administrator management of shared MCP plugins (#6018)
* fix(capabilities): restore administrator management of shared MCP plugins

* fix(mcp): clarify control ownership and require mutation scope
2026-09-30 10:14:42 +08:00
lihongyuan99
f94dbf21f3
docs(backend): point the stream-mode translation anchor at stream_modes.py (#6021)
* docs(backend): point the stream-mode translation anchor at stream_modes.py

* docs(backend): use one anchor spelling within the stream-mode table row

The row named the translator by its backend-relative path and its call
site by the harness-relative short form, so the same symbol was written
two ways two rows apart.
2026-09-30 10:13:22 +08:00
Hyeonsang Cho
777961f84e
fix(events): read a non-filename run ID as an unknown run on JSONL (#6070)
* fix(events): read a non-filename run ID as an unknown run on JSONL

The run-scoped routes (events, messages, workspace-changes) pass the URL's
run_id straight to the event store. With run_events.backend: jsonl, an ID
that cannot be a filename (run.1, a%20b, %2e%2e) raised ValueError and
surfaced as a 500, while the memory and database stores return an empty
result.

JSONL reads and deletes now treat such an ID as an unknown run through one
helper, _existing_run_file(), which returns before building any path.
Writes still reject it.

* docs(changelog): reference #6070 for the JSONL run ID fix

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:07:58 +08:00
NanPan
48aec0bc84
fix(gateway): drain managed model saves on cancellation (#6024)
* fix(gateway): drain managed model saves on cancellation

* test(gateway): pin managed model save cancellation drain

* fix(gateway): log failed drained managed model saves

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

* fix(gateway): keep guidance within hard size limit

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 10:06:53 +08:00
Shxiao
73fdbcfdf4
fix(scheduler): prevent fixed-hour cron tasks from firing twice on DST fall-back (#6066)
* fix(scheduler): prevent fixed-hour cron tasks from firing twice on DST fall-back

* fix(scheduler): restrict fixed-time DST skip to single-time cron and add edge tests

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 09:50:39 +08:00
Hyeonsang Cho
b37f769b67
fix(persistence): reject SQLite file: URIs instead of opening them as filenames (#6069)
* fix(persistence): reject SQLite file: URIs instead of opening them as filenames

resolve_sqlite_conn_str passed a file: connection string through
unchanged, but LangGraph's SQLite checkpointer and Store factories
(SqliteSaver, SqliteStore, AsyncSqliteSaver, AsyncSqliteStore) connect
without uri=True, so SQLite treated the whole URI as a literal filename:
file:checkpoints.db?mode=rwc created a file with that exact name in the
working directory, file::memory:?cache=shared persisted to disk, and a
file:/// URI failed to open. The readiness probe did open URIs with
uri=True, so it checked a different file than the runtime used and
reported in-memory URIs as not_configured.

Reject file: URIs in resolve_sqlite_conn_str, the one helper all four
factories go through, with an error that names
checkpointer.connection_string. The probe drops its URI parsing and
reports a rejected connection string as unreachable.

* docs(changelog): reference #6069

* test(persistence): pin the case-sensitive file: URI check
2026-09-30 09:40:02 +08:00
Bob
4394c23b66
fix(channels): decode Slack entity escapes in inbound message text (#6045)
* fix(channels): decode Slack entity escapes in inbound message text

Slack sends a user-typed &, < and > as &amp;, &lt; and &gt; in event text, so raw <...> always marks a control sequence. The Slack channel passed that text to the agent as-is, so code, comparisons and names like R&D reached the agent as 'a &lt; b', 'R&amp;D'. Decode the three entities after bot-mention stripping (amp last, so an escaped entity is decoded once) and keep mentions and links untouched.

* test(channels): cover Slack entities inside link labels

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 09:38:05 +08:00
xiaodu55
81fa17c15a
fix(gateway): render the bundled interpreter repair path without backslash escapes (#6040)
* fix(gateway): render the bundled interpreter repair path without backslash escapes

The repair hint used {sys.executable!r}, whose repr() escapes Windows
backslashes - users saw 'D:\college\...\python.exe' in the
copy-paste guidance, and
test_stale_bundled_interpreter_can_be_repaired_without_reinstall fails
3/3 on Windows because the raw path never appears in the detail. The
!r was written for the surrounding quotes; on POSIX paths that is its
only effect, which is why CI never saw it.

Quote the raw path explicitly instead. On Windows the three test params
now pass (22/22 in the file); POSIX output is unchanged apart from the
quoting coming from the literal instead of repr().

* fix(gateway): json-escape the interpreter command in the repair hint

Review follow-up (P2): the raw Windows path in the hint cannot be
pasted into the server's JSON ('\U' is not a JSON escape), so the
prescribed repair could not be saved. Show the command value via
json.dumps(sys.executable) - pasteable JSON on every platform - and
keep the raw path in a parenthetical for terminal use, per review.
2026-09-30 09:19:39 +08:00
Storm
fbf34e9cfc
fix(helm): plumb lark-cli runtime images into the provisioner (#5835)
* fix(helm): plumb lark-cli runtime images into the provisioner

The provisioner, docker-compose, and the root README all support
`LARK_CLI_INIT_IMAGE` (Pattern A) and `LARK_CLI_BROKER_IMAGE` (Pattern B),
but the chart never exposed them, so a Helm operator following the README
had no value to set and silently got a sandbox without a `lark-cli`
runtime (`lark-cli: command not found` at chat time).

- values.yaml: `provisioner.larkCliInitImage` / `larkCliBrokerImage`, both
  empty by default, so the rendered Pod spec is unchanged for existing
  releases.
- provisioner-deployment.yaml: render each env var only when non-empty.
  The provisioner keys off `bool(env)`, so omitting the variable keeps
  "feature off" identical to the pre-change chart.
- chart README: new "Sandbox lark-cli runtime (optional)" section; root
  README points Helm users at the two keys.
- backend/tests/test_helm_provisioner_lark_cli_images.py: render
  regression test (red before the template change).

* docs(helm): drop duplicated lark-cli callout in chart README

Review on #5835 caught the "An image here does not authenticate anyone
by itself" blockquote pasted twice verbatim in the new chart README
section. Remove the second copy and keep the first as the section's
closing note. Docs-only; the rendered chart is unchanged.
2026-09-30 09:16:00 +08:00
RongJie G
d642dfdd2f
fix(mcp): reserve notification launches atomically (#5965)
* fix(mcp): reserve notification launches atomically

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>

* fix(mcp): classify busy notification conflicts by type

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 09:12:11 +08:00
illusion
f9f6a36243
docs(zh): add the missing External Chat Message Roles section to README_zh (#6059)
* docs(zh): add the missing External Chat Message Roles section to README_zh

Adds the 'External Chat Message Roles' section to the Chinese README,
mirroring the section present in the English README under the Security
Notice. Same position (between Gateway Admin Is Equivalent to Code
Execution and Deployment Defaults), same content semantics, translated
rather than paraphrased.

Covers the HTTP 400 rejection of client-supplied system/developer
messages, the no-rewrite-of-existing-checkpoints note, and the full
local-verification PoC instructions (poc_external_system_message_injection.py,
--expect vulnerable / --expect blocked). The referenced script exists at
backend/tests/poc_external_system_message_injection.py.

The English TOC does not list this subsection either, so the zh TOC is
left unchanged to stay consistent (same approach as #5409).

* Fix minor language inconsistencies in README_zh.md

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 08:36:31 +08:00
huyengiang101086-spec
f119cacbc3
fix(frontend): bind Windows dev server to loopback by default (#6034)
Co-authored-by: huyengiang101086-spec <270684046+huyengiang101086-spec@users.noreply.github.com>
2026-09-30 08:12:31 +08:00
Hyeonsang Cho
ed09efdb7f
fix(config): upgrade the config.yaml the Gateway loads (#5991)
* fix(config): upgrade the config.yaml the Gateway loads

scripts/config-upgrade.sh resolved DEER_FLOW_CONFIG_PATH, then
backend/config.yaml, then <checkout>/config.yaml. make dev exports
DEER_FLOW_PROJECT_ROOT=<checkout>, so the Gateway reads the checkout copy
first: with both files present the script upgraded a file nothing loads
and left the live config outdated while reporting success. It also
ignored DEER_FLOW_PROJECT_ROOT and a DEER_FLOW_CONFIG_PATH set in .env,
and fell back to another file when DEER_FLOW_CONFIG_PATH named a
missing one.

Ask the harness resolver (AppConfig.resolve_config_path) instead of
copying its order, defaulting DEER_FLOW_PROJECT_ROOT to the checkout as
serve.sh does. A missing DEER_FLOW_CONFIG_PATH or invalid project root
now fails with the Gateway's error; no config anywhere still creates
<checkout>/config.yaml from the example.

* docs(changelog): reference #5991

* test(config): pin config-upgrade seeding a missing config from the example

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 22:10:29 +08:00
Bob
9b95f64e45
fix(sandbox): make glob patterns root-relative with recursive ** (#6046)
path_matches backs the glob tool and grep's glob= filter for every sandbox provider. It relied on PurePosixPath.match, which matches from the right and treats ** as a single-segment * on Python 3.12+, so only a leading **/ was recursive: src/**/*.py missed src/top.py and anything two or more levels deep, and docs/*.md also matched vendor/docs/b.md.

Match patterns that contain a / segment by segment from the search root, with ** spanning zero or more directories. Patterns without / keep matching the basename at any depth.
2026-09-29 21:50:59 +08:00
Zhengxu (Joshua) Jin
f64cc16385
docs(helm): fix the external PostgreSQL example in the chart README (#6038)
* docs(helm): fix the external PostgreSQL example in the chart README

The example set postgresql.external.host/port/database/username/password,
but the chart has never read those keys; it only reads
postgresql.external.databaseUrl or existingSecret. With the documented
values the chart creates no postgres Secret and the gateway gets no
DATABASE_URL, while config.yaml still references $DATABASE_URL, so the
config fails to load. Show databaseUrl, with existingSecret as the
alternative.

* docs(helm): clarify external database URL encoding

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 21:32:42 +08:00
xiaodu55
b30be67e00
fix(tests): write file_signature fixtures as bytes so CRLF never leaks in (#6042)
On Windows, Path.write_text and the text-mode append translate the
fixtures' newlines to CRLF on disk, so read_config_with_signature
correctly returns b'...info\r\n' while the tests assert the byte-exact
b'...info\n' - three tests fail on Windows (CI's Linux never sees the
translation). The product behavior is correct; the fixtures must write
the exact bytes they assert.

write_bytes / binary append for the three byte-sensitive tests: the
file now passes 10/10 on Windows.
2026-09-29 21:01:18 +08:00
yetuge
5312271f2b
fix(community): close the per-call Firecrawl client's async HTTP pool (#6013)
* fix(community): close the per-call Firecrawl client's async HTTP pool

The firecrawl tools build a fresh AsyncFirecrawlApp for every web_search
and web_fetch call. The migration to the async client (#5786) kept the
per-call construction, but AsyncFirecrawlApp eagerly creates an
httpx.AsyncClient-backed pool in its constructor and exposes no public
teardown, so each tool call abandons one unclosed pool. The sibling
providers either scope their httpx clients with 'async with' or close
them in a finally (tavily); firecrawl now does the same via a
best-effort teardown that tolerates SDK shapes without the v2 client.

Signed-off-by: yetuge <2219677952@qq.com>

* fix(firecrawl): make per-call pool teardown resilient

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

* test(firecrawl): skip the real-SDK teardown test when the private layout differs

The real-SDK test hard-asserted the locked firecrawl-py 4.23.0 private
layout (_v2_client.async_http_client._client.is_closed), so an
environment resolving another version in the declared >=1.15.0 range
died with AttributeError instead of skipping — while the helper (and
test_teardown_survives_a_client_without_v2_attributes) deliberately
treat a missing or reshaped layout as a no-op. Guard on the same
attributes and skip when they are absent.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 21:00:36 +08:00
Stella-xixi
65ec5a475d
fix(uploads): preserve human message metadata (#5972) 2026-09-29 17:59:25 +08:00
Shxiao
f87a04e581
fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares (#6015)
* fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares

* fix(changelog): restore database entry and add #4380 reference link, simplify task_tool unwrap

* fix(subagents): preserve Overwrite wrapper when task_tool forwards sandbox state to executor

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 17:24:29 +08:00
Storm
1b691689da
fix(integrations): keep Lark runtime status consistent (#5994)
* fix(integrations): keep Lark runtime status consistent

Probe sandbox runtime readiness for every Lark mutation response outside the credential lock. Expose explicit sandbox_runtime_probed state so the frontend can distinguish conservative fallbacks without inferring semantics from runtime detail text, while cancelling stale reads before updating the cache.

* style(frontend): format Lark cache test

* fix(integrations): snapshot Lark credential status under the lock

Why this change is needed:
PR #5994 moved the post-mutation status read outside the per-user
credential lock so the new sandbox runtime probe would not extend the
critical section. As flagged in review, that lets a concurrent app
switch or auth flow land between the committed mutation and the status
read; in remote-provisioner mode the runtime probe can spend seconds
before probe_lark_auth runs, so the raced switch can clear the new
tokens and make a completed login report success=false, or mix an old
app_id with a newer auth state.

What changed:
- add _LarkCredentialSnapshot (app config + auth probe) and capture it
  in the same _lark_credential_lock section that commits the mutation
- thread the snapshot through _get_lark_mutation_status into
  get_lark_integration_status(credential_snapshot=...); only the
  credential-independent runtime probe still runs after the lock
- apply to complete_lark_auth, complete_lark_config, and
  set_lark_app_credentials; install keeps the one-shot status build

Problem solved:
Mutation responses stay tied to the just-committed credential
generation: a raced app switch can no longer flip a completed
device-code login to success=false or blend a stale app_id with newer
auth state, while the runtime probe stays off the lock.

* fix(integrations): address Lark status review follow-ups

- install_lark_integration: snapshot credential-derived status fields
  under _lark_credential_lock so the install response cannot mix newer
  credential state read across the unlocked runtime probe
- useInstallLarkIntegration: route the cache write through
  cacheLarkMutationStatus so install cancels in-flight reads and keeps
  probed runtime fields under frontend/backend version skew
- README: soften the UI claim - the status API exposes
  sandbox_runtime_probed and the mutation cache honors it

* test(integrations): tighten Lark status fixtures and guidance docs

- cache.test: use a distinct mutation runtime mode in the unprobed-merge
  case so the assertions prove the cached runtime fields win
- e2e mock: include broker in the sandbox_runtime_mode union cast so the
  fixture cannot drift from LarkSandboxRuntimeMode
- AGENTS.md (gateway + skills): mention sandbox_runtime_probed alongside
  the existing runtime readiness fields, keeping the gateway guidance
  file under its byte budget

* fix(integrations): harden Lark mutation cache against skew and races

- cacheLarkMutationStatus: await cancelQueries before writing so an
  in-flight GET whose fetch already resolved cannot clobber the fresher
  mutation status, matching the existing threads/archive pattern
- self-heal version skew: an unprobed (pre-field backend) mutation
  response now triggers a status refetch instead of risking the
  conservative runtime fallback lingering until the next focus/remount
- api: normalize a missing sandbox_runtime_probed to false at the
  boundary so the declared-non-optional type matches skewed runtimes
- tests: pin the skew contract (missing-field cases for cache and api)
  and add a useInstallLarkIntegration regression test proving the
  install cache write goes through cacheLarkMutationStatus
- README: describe the probed flag as cross-version semantics instead of
  claiming the same-version API surfaces both states
- lark_cli: note that install's credential snapshot runs the auth probe
  subprocess while holding the credential lock

* refactor(integrations): derive sandbox_runtime_probed inside the resolver

_resolve_sandbox_runtime_readiness now returns a fourth probed element
next to the branch that produces the unevaluated remote fallback, so the
'was readiness actually evaluated' invariant can no longer drift from the
fallback logic it mirrors. Callers and resolver-level tests updated to
pin probed per branch.
2026-09-29 17:23:36 +08:00
NanPan
907910957b
fix(gateway): drain managed subagent mutations on cancellation (#6023)
* fix(gateway): drain managed subagent mutations on cancellation

* test(gateway): pin managed subagent mutation drain

* fix(gateway): strengthen managed subagent mutation drain typing

* test(gateway): cover managed subagent cancellation drain
2026-09-29 17:11:07 +08:00
Hyeonsang Cho
1cd42cf42e
fix(doctor): check the config file the Gateway actually loads (#5987)
* fix(doctor): check the config file the Gateway actually loads

make doctor always inspected <checkout>/config.yaml and ignored
DEER_FLOW_CONFIG_PATH and DEER_FLOW_PROJECT_ROOT. A missing override
that stops the Gateway from starting still reported "config.yaml found"
and "config.yaml loadable", and a valid override pointing elsewhere got
the wrong file checked.

Doctor now resolves the path through the harness's own resolver,
AppConfig.resolve_config_path, after defaulting DEER_FLOW_PROJECT_ROOT
to the checkout as scripts/serve.sh does for make dev. An override the
Gateway would reject fails "config.yaml found" with the Gateway's error
and the config checks skip; an unimportable harness is reported rather
than raised.

* docs(changelog): reference #5987

* fix(doctor): hand the config-location variables over like serve.sh

Review follow-up. serve.sh sources the checkout .env over the shell and
then replaces an unset or empty DEER_FLOW_PROJECT_ROOT with the
checkout. Doctor loaded .env shell-first and only defaulted an unset
root, so a shell DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT that
differed from .env, or an empty root, made it check a different file
than the Gateway loads.

For those two variables the .env value now overrides the shell (other
keys keep doctor's shell-first loading), and an empty root becomes the
checkout, before the harness resolver runs.

* fix(doctor): report any harness import failure and expand ~ like source

Review follow-up.

- The harness-import guard caught only ImportError, so a harness that is
  present but broken differently (TypeError/SyntaxError while the module
  body runs) still crashed doctor. Catch Exception, as
  check_config_loadable does, and name the exception type in the report.
- serve.sh's `source .env` expands an unquoted leading `~` in
  DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT and keeps a quoted one
  literal; dotenv_values expands neither. Expand the unquoted form so a
  `~/...` path in .env no longer fails a setup that starts, and keep the
  quoted form literal so doctor does not pass one the Gateway rejects.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 16:55:21 +08:00
NanHan Qing
f8b69bd84b
fix(mcp): close thread-scoped MCP sessions on thread deletion (#5956)
* fix(mcp): close thread-scoped MCP sessions on thread deletion

Deleting a thread removed its filesystem data, checkpoints, runs, events,
feedback, metadata and browser session, but never closed its persistent MCP
sessions. MCPSessionPool.close_scope() had no production caller at all, so a
recreated thread id under the same user could reuse stale stateful MCP server
state, and the leaked owner task plus subprocess kept holding memory and file
descriptors.

Add MCPSessionPool.close_thread_scope(user_id, thread_id) and call it from the
reserved deletion path under the same best-effort policy as the neighbouring
cleanups. It matches every incarnation of the thread -- the legacy user:thread
scope and any v2:[user, thread, incarnation] scope -- because a deletion cannot
reliably read the current incarnation: the record may predate incarnation
tracking, and a new incarnation can be minted concurrently. close_scope() is
unchanged for callers that already hold one exact key.

Add mcp_scope_belongs_to_thread() to own the matching rule, and hoist the v2:
prefix into a shared constant so the encoder and matcher cannot drift.

Refs #5188

* test(mcp): bind owner identity in thread cleanup checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 16:53:18 +08:00
xiaodu55
8a3a309d1c
fix(tests): make the mcp task service suite deterministic on Windows (#5995)
Follow-up to #5940/#5941: with the hang gone, 12 tests further down the
file fail on native Windows. One disease, several signatures: the tests
patch _CANCELLATION_DRAIN_TIMEOUT_SECONDS to 0.01, but on Windows +
CPython 3.12 a 10ms wait_for over an already-created task times out
immediately (Windows timer granularity, ~15.6ms default tick), so
instant claims are abandoned to the background handoff and every
claim-completes-in-the-foreground premise breaks - abandoned fresh
claims (single_flight), cancellation landing after run_once already
returned (repeated_outer), releases that never happen in the
foreground (poll_retry / notification_failure).

- raise the patched drain 0.01 -> 0.05 in the five affected tests:
  clears the granularity tick with headroom, keeps every existing 0.2s
  bound valid, and the gate-blocked-claim abandonment the single-flight
  tests exercise still drains-timeout as designed
- loosen test_run_once_releases_claim_when_driver_is_missing_or_fails'
  strict next_poll_at > now + 5s to >=: Windows datetime.now() can
  return the same tick for the whole run_once, making the strict
  comparison a clock artifact; the backoff floor is the invariant

On Windows the file now passes 126/126 in ~20s (was 12 failed). Linux
semantics unchanged: claims complete in microseconds inside a 50ms
drain there.

Fixes #5993
2026-09-28 22:47:29 +08:00
Shxiao
b87ebd9402
fix(database): validate pool settings and command_timeout against booleans and non-finite values (#6009) 2026-09-28 22:46:39 +08:00
kuse
857eac45cd
fix(agents): treat non-finite read_file line bounds as unusable in loop detection (#5990)
_coerce_line_number() caught only (TypeError, ValueError), but a
model-emitted JSON number such as 1e999 parses to float('inf') and
int(inf) raises OverflowError, which escaped _track_and_check() through
after_model() and crashed the run. Treat the bound as unusable (None),
matching the OverflowError guards already merged for the search-tool
coercers.
2026-09-28 22:37:30 +08:00
yetuge
160fd50a28
fix(sandbox): reject non-finite health_check_skip_seconds in config (#5997)
`health_check_skip_seconds` was the only float field in SandboxConfig
without `allow_inf_nan=False`, and `ge=0` alone does not stop inf
(`inf >= 0` is true). A config.yaml value of `1e999` loads as the string
"1e999" and pydantic's float coercion turns it into inf, so the field
accepted it and the BoxLite warm-reuse site then skipped the health
check permanently: `skip_seconds > 0 and (time.time() - released_at) <
skip_seconds` holds forever for inf, so every box this instance
recently released is promoted without validation instead of failing
fast at config load.

Add `allow_inf_nan=False`, matching the sibling sandbox float fields
(`bash_command_timeout`, `renewal_interval_seconds`, `ttl_multiplier`)
and the ownership lease-TTL model validator.

Red/green: `test_sandbox_config_rejects_non_finite_health_check_skip_seconds`
parametrizes inf and "1e999"; both were accepted before the fix (RED)
and are rejected after. Full boxlite suite: 54 passed / 15 skipped.

Regression: the sandbox/config/ownership subset fails 17 tests on this
Windows host both with and without the change, with a byte-identical
failure-name set (environment-type: process-spawn and file-stat
behavior), so no regressions are introduced.

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-28 22:36:19 +08:00
Daoyuan Li
5850b0da9f
fix(tenki): parse sticky environment values as booleans (#5849)
* fix(tenki): parse sticky environment values as booleans

* docs(tenki): keep agent guidance within chain limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 22:22:43 +08:00
lau0708
8712d495e2
fix(channels): grant sandbox read bits to Feishu/DingTalk inbound uploads (#5998)
The channel manager persists inbound attachments with
`write_upload_file_no_symlink` (root-owned, mode 0o600) and then relaxes them
to group/other readable via `_make_inbound_file_sandbox_readable`. In
AIO/Docker sandbox mode the sandbox runs as a non-root user on the bind-mounted
thread directory, so without those bits the file is unreadable and the agent
sees an empty attachment. The HTTP upload route applies the same permit.

`FeishuChannel._receive_single_file` and `DingTalkChannel._receive_single_file`
persist their own bytes and hand back a virtual path instead of routing content
through the manager, so neither reached that permit:

- Feishu leaves `msg.files` populated with `file_key` descriptors, but the
  manager's `_read_http_inbound_file` requires a `url` key, so its ingest pass
  reads no data and skips the upload.
- DingTalk clears `msg.files`, so the ingest pass returns immediately.

Telegram is unaffected because it hands raw bytes to the manager through
`INBOUND_FILE_CONTENT_KEY`, which is why it was covered when the permit was
added. Apply `apply_upload_sandbox_permits(..., S_IRGRP | S_IROTH)` right after
each channel's own write, off the event loop, mirroring the manager and HTTP
paths.

Tests: `test_channel_file_attachments.py` gains a mounted-provider regression
per channel asserting the persisted upload is group/other readable.
2026-09-28 22:19:32 +08:00
yc2bgr8
712a11c0cd
feat(community): migrate firecrawl tools to AsyncFirecrawlApp (#5786)
Replace the synchronous FirecrawlApp with AsyncFirecrawlApp in the
firecrawl community tools, continuing the TODO.md planned feature
('Replace sync requests with httpx.AsyncClient in community tools').

web_search_tool and web_fetch_tool become async functions and await
client.search()/client.scrape(). Return values (SearchData.web,
Document.markdown/metadata) are identical between the sync and async
clients, so callers see no change; error handling and the api_key /
api_url construction path are unchanged.

Tests now invoke the async tools via ainvoke under @pytest.mark.anyio,
and the client search/scrape methods are mocked with AsyncMock.
2026-09-28 18:08:46 +08:00
jiangjiang248
50d2d68556
fix(llm): avoid quadratic think-block stripping on unclosed tags (#5854)
* fix(llm): avoid quadratic think-block stripping on unclosed tags

* test(llm): pin think tag parsing edge cases

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 16:38:12 +08:00