2447 Commits

Author SHA1 Message Date
mosifu
51cc489c8f
fix(wecom): raise the image upload cap to the platform's 10 MB (#8174)
* fix(wecom): raise the image upload cap to the platform's 10 MB

`send_file` capped images at 2 MB and files at 20 MB. 2 MB is the *voice*
limit in WeCom's aibot media-upload protocol; images are allowed up to 10 MB
(developer.work.weixin.qq.com/document/path/101917). Every 2-10 MB image was
therefore dropped before upload, and the turn arrived without its chart.

Raise the image cap to 10 MB. The 512 KiB chunk size keeps a 10 MB upload at
20 chunks, well under the protocol's 100-chunk limit.

* docs(changelog): record the WeCom image-cap fix

Adds the Unreleased -> Fixed -> channels entry for #8174 to CHANGELOG.md and
CHANGELOG_zh.md.
2026-11-12 17:16:42 +08:00
hataa
416a0647a4
fix(subagents): resolve the inherit-path app config off the event loop (#8134)
* fix(subagents): resolve the inherit-path app config off the event loop

task_tool's inherit-model fallback called get_app_config() synchronously
inside the async tool body. get_app_config() reads config and extension
files on every cache miss (and may fully reload them), so the fallback
path stalls the calling streaming event loop for the duration of the
disk IO - the same class of loop stall removed for tool assembly in
issue #5172. Every other config resolution in this tool already
dispatches through run_assembly; route this one through it too.

* chore: drop the CHANGELOG entry per maintainer request

Changelogs for merged PRs are created in batch upstream; keeping the
entry here only risks merge conflicts.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 16:59:02 +08:00
Bas Cancrinus
7ae36b469d
fix(helm): make the gateway init containers' image and resources conf… (#8161)
* fix(helm): make the gateway init containers' image and resources configurable

init-home and init-extensions hard-coded busybox:1.36 and set no resources.
Clusters that pull only from a mirror could not start the gateway, and a
namespace ResourceQuota on limits.* rejected the pod, although every main
container already has a configurable image and requests+limits.

Add gateway.initContainers.image (repository/tag, default busybox:1.36, same
convention as nginx.image) and gateway.initContainers.resources (modest
requests+limits by default, matching the chart README's resource defaults).
Add a helm-template test for the defaults and the overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(helm): explain mirroring gateway init images

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 16:44:42 +08:00
paer
2dc68b242f
fix(subagents): prevent concurrent file-backed creates from overwriting definitions (#8132)
* docs(subagents): describe create-only file publication

* fix(subagents): prevent concurrent create overwrites

* test(subagents): cover independent create races

* docs(persistence): record create-only publication

* docs(config): explain managed subagent file claims

* docs(persistence): retain create-only publication rule

* docs(subagents): retain shared file create semantics

* fix(gateway): probe managed subagent hard links on startup

* test(gateway): cover managed subagent volume probes

* test(gateway): keep volume probes off the event loop

* docs(config): describe startup hard-link warnings

* docs(changelog): reference managed subagent fix #8132

* docs(changelog): keep the fix entry clear of append conflicts
2026-11-12 14:39:25 +08:00
ywy
98e8598adb
fix(config): delete thread dir through extended-length path to avoid WinError 145 (#8144)
Co-authored-by: Nan Gao <thunderggnn@gmail.com>
2026-11-12 14:24:24 +08:00
ZephyrWang
b80191fad3
fix(memory): reject duplicate fact IDs during import (#8151)
* fix(memory): reject duplicate fact IDs during import

* fix(memory): explain duplicate IDs in import response

* docs(changelog): link duplicate import fix PR

* fix(memory): keep duplicate validation portable

---------

Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 11:42:55 +08:00
Willem Jiang
b883deaab2
ci: split backend unit tests into six shards (#8164)
* ci: split backend unit tests into six shards

* test(runtime): synchronize rollback lease renewal
2026-11-12 11:08:31 +08:00
Bas Cancrinus
13e6fc29e5
feat(opensandbox): add opt-in secure_access sandbox option (#8162)
* feat(opensandbox): add opt-in secure_access sandbox option

Forward a new `sandbox.secure_access` option (default false) to
SandboxSync.create so OpenSandbox can require a per-sandbox endpoint
token at the ingress gateway and server proxy. The server returns the
token as an endpoint header and SDK 0.1.15 already attaches endpoint
headers to execd and file requests, so DeerFlow needs no token handling.

Non-boolean values are rejected at provider startup so a quoted "false"
cannot silently enable it. Documented in config.example.yaml,
CONFIGURATION.md, the SandboxConfig docstring and the provider README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(opensandbox): validate server proxy boolean

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 09:49:33 +08:00
2SinceJuly
6eb4833598
fix(client): scope memory management to the named agent's bucket (#8146)
* fix(client): scope memory management to the named agent's bucket

A named DeerFlowClient stored and read its memory in that agent's bucket:
MemoryMiddleware and the memory tools both pass agent_name. The management
methods on the client did not, so create_memory_fact wrote the fact into the
user's default bucket while the agent's injected context came from its own
bucket — the fact was persisted but never recalled by the same client.

Pass the client's agent_name through get_memory, export_memory,
import_memory, reload_memory, clear_memory and the single-fact CRUD calls.
An unnamed client keeps the default bucket, and a backend that ignores the
scoping keyword keeps raising its own unsupported-operation error.

Fixes #8142

* fix(client): refuse named memory calls a backend cannot scope

DeerFlowClient forwarded the agent name to every memory management call, but
a backend that declares no agent-scoped storage ignores the keyword and
answers normally. A named client then reported success while the call read,
cleared or exported the user's default bucket, the same silent
wrong-bucket behaviour #8142 is about. Check the capability flag before
forwarding the scope and raise an unsupported-operation error, as the
Gateway already does with 501.

Resolve the scope in reload_memory before the get_memory fallback so an
unsupported scope is not mistaken for a backend without reload support and
retried as a user-wide read.

* chore: drop the CHANGELOG entry

The changelog is generated at release time, and the hand-written entry points
at #8146 without a link definition, which breaks
tests/test_changelog_reference_links.py. Removing it keeps the file released-
generated, per the reviewer's request.

* docs(memory): explain scoped noop client behavior

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 09:21:28 +08:00
Wenchao An
9699f04cdc
feat(projects): add visible project memory with extraction controls (#8117)
* feat(projects): add visible project memory with extraction controls

* fix(projects): avoid replaying committed memory extractions

* chore(ci): retrigger project memory checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 09:09:44 +08:00
mosifu
041d9e375e
fix(wecom): treat "@bot /new" in group chats as a command (#8118)
WeCom leaves the bot mention in group message text, so `_publish_ws_inbound`
handed "@RobotA /new" to `is_known_channel_command`, which requires a bare
leading slash. The turn was classified as chat and the command never ran.

Strip leading mentions for the command lookup only, mirroring Feishu (#4229)
and DingTalk (#4724): ordinary chat keeps its mentions, and the stripped form
is what reaches ChannelManager so it parses `/new`. The comment spells out that
this drops any leading mention, not only the bot's -- WeCom text mentions carry
display names, so `aibotid` cannot be matched against them.

Recorded in CHANGELOG.md / CHANGELOG_zh.md under Unreleased -> Fixed.

Fixes #8099

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-12 08:01:39 +08:00
NanHan Qing
2baadff446
fix(scripts): make check.py and setup_wizard.py --help print usage (#8152)
Both entry points ignored their arguments entirely, so the first thing a
contributor tries ran the real work: `python scripts/check.py --help` ran the
full dependency probe (and, on a host with a missing tool, exited 1), and
`python scripts/setup_wizard.py --help` entered the wizard -- which on an
existing config.yaml asks "Do you want to reconfigure?" and then rewrites
config.yaml and .env. Unknown flags were silently ignored the same way.

Parse arguments in the __main__ block, following scripts/doctor.py: main()
stays directly callable for the in-process unit tests, --help prints usage and
exits 0 before any probe or prompt, and an unknown flag exits 2 with
"unrecognized arguments". The no-argument invocations behind `make check` and
`make setup` are unchanged.

Adds subprocess CLI-contract cases in a disposable checkout next to the
existing script tests, modelled on backend/tests/test_doctor_script.py.
2026-11-12 06:15:52 +08:00
Wenchao An
3d826c6d95
feat(approvals): add Ask, Auto and Full tool permission modes (#8033)
* feat(hitl): add human-in-the-loop approval for high-risk tool calls

* fix(hitl): address approval review findings

* docs(hitl): keep agent guidance under size limit

* fix(hitl): guard approval pause recovery

* docs(hitl): shrink inherited guidance

* fix(hitl): harden approval admission and recovery

* fix(approvals): address review safety and reconciliation gaps

* ci: retry approval checks after runner timeout

---------

Co-authored-by: wangzeren <1004695029@qq.com>
2026-11-11 20:59:05 +08:00
zuiqiangxijiejin
c7bc8bab67
fix(docker): handle inline YAML comments in sandbox mode detection (#8103)
* fix(docker): honor inline comments on the sandbox section header

* fix(docker): match the sandbox provider value exactly

* test(docker): ignore nested provisioner URLs

* docs(docker): explain exact sandbox provider matching
2026-11-11 20:11:53 +08:00
Heo Hao
80cb6404b6
fix(browserless): reject timeouts httpx cannot use (#8025)
* fix(browserless): reject timeouts httpx cannot use

`_coerce_timeout` forwarded any int, float or numeric string to
`httpx.AsyncClient(timeout=...)` unchanged, so a configured `timeout_s: 0`
or a negative value failed every fetch and capture with `ConnectTimeout`
against a healthy backend, `nan` raised `ValueError: deadline must not be
NaN`, and `inf` left the request without a deadline so it never completed.
Each of those silently disabled the provider.

Fall back to the documented 30s default with a warning when the number is
not a usable deadline. This matches `crawl4ai._coerce_timeout` after #7481;
`jina_ai._coerce_timeout` still forwards non-positive values.

* fix(browserless): address timeout review and format tests

Explain how HTTPX handles NaN and positive infinity. Document that
Browserless accepts positive finite timeouts without an upper limit,
and pin that behavior with a 1e308 test case.

Crawl4AI and Jina timeout validation are tracked in #7481 and #7823.
Remove the Browserless docstring's claim that Crawl4AI shares its
coercion behavior. Format the test file for the backend lint check.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-11 19:43:53 +08:00
Wistom Zhang
218c64180e
feat(summarization): align built-in compaction prompt to preserve user requirements and approval conditions (#8114) (#8114)
In long-running agent workflows with repeated context compaction, empirical gateway ablations across 132 real sessions revealed that standard boundary compliance remains robust, while semantic drift can occur when model-generated summaries rephrase literal approval phrases and action conditions.

This change converges PR #8114 into a clean built-in summary prompt enhancement:
- Replaces regex extraction, tag elevation, and heuristic revocation with an extended built-in DeerFlow summary prompt (_DEERFLOW_SUMMARY_PROMPT).
- Instructs the summarizer to faithfully record active user requirements, literal approval phrases, permitted actions, and later explicit revisions without paraphrasing.
- Treats summary text strictly as historical data, fully preserving the security boundary and control/data plane separation of DurableContextMiddleware.
- Unifies automatic and manual compaction factory paths to select the built-in template when unconfigured, while preserving explicit summary_prompt precedence.
- Adds comprehensive sync/async unit tests covering built-in and custom templates.

Co-authored-by: Totoro <Totoro-qaq@users.noreply.github.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
2026-11-11 19:36:32 +08:00
fuduoduo
4d95bff550
test(utils): cover goal-objective and bash-exit input boundaries (#8048)
* test(utils): cover goal-objective and bash-exit input boundaries

* test(utils): make the string-comparison boundary case actually pin the contract

Review follow-up: 400 digits is below CPython's default int-max-str-digits cap (4300), so an implementation regressed to int(match.group(2)) would still pass. Derive the digit count from sys.get_int_max_str_digits() + 1 instead. Also document how this file differs from test_tool_result_meta.py (direct return contract vs message normalization). No production code changes.

---------

Co-authored-by: jack12178 <jack12178@users.noreply.github.com>
2026-11-11 19:04:09 +08:00
ZephyrWang
d9b2cb4bcb
fix(memory): require facts in replacement imports (#8131)
* fix(memory): require facts in replacement imports

* docs(memory): document import facts requirement

* docs(changelog): link memory import fix
2026-11-11 17:35:17 +08:00
Xinmin Zeng
2a97ec3e58
docs: turn README into an index and move reference detail to docs/guide (#8113)
* docs: turn README into an index and move reference detail to docs/guide

The README had grown to 393 KB because every change appended its notes there. The text moves verbatim into 18 topic guides under docs/guide/; only headings are re-leveled and relative links re-pointed. The README keeps the introduction, a quick start and a guide index. Contributor test notes move to CONTRIBUTING.md.

* docs: send user docs to topic guides and budget the root README

The rule 'update README.md for user-facing changes' is what grew the README. Guidance now points to docs/guide/, and check_agent_guidance.py fails when the root README exceeds 16 KiB (AG003).

* chore: remove unreferenced screenshots and finished plan notes

pr-build/ holds two PR screenshots (3 MB) from #1986. plans/ holds the plan for the merged #4049. docs/SKILL_NAME_CONFLICT_FIX.md is a February change note. Nothing references them.

* docs: route always-loaded AGENTS.md files to scoped guides

* docs(agents): cut scoped guides to scope, verify, traps, decisions

Each AGENTS.md keeps four kinds of content: the scope of the directory, the
verification commands, traps a local test run does not show, and decisions the
code cannot state. Values, field lists, function inventories, test names, issue
numbers and history are deleted, because the code, the tests and git state them.

Search and fetch providers share one community guide. Four guides with no
remaining content are removed. The size check now allows 6 KiB per file (hard
limit 8 KiB) and 24 KiB per chain (hard limit 32 KiB).

* style: format frontend guide table and guidance test imports

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-11 13:26:17 +08:00
Bas Cancrinus
4a27b133f8
fix(helm): apply image.pullSecrets to the nginx Deployment (#8123)
The frontend, gateway, provisioner, Postgres and Redis pods include
deer-flow.imagePullSecrets, but the nginx Deployment did not. With
nginx.image mirrored to a private registry, the nginx pod stayed in
ImagePullBackOff while every other pod started.

Add a helm-template test that renders every workload and checks that
each one uses the configured pull secrets and none sets them by default.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:10:31 +08:00
Totoro
3df3b3c630
feat(sandbox): show source line numbers for short read_file ranges (#8109)
* feat(sandbox): show source line numbers for short read_file ranges

* fix(sandbox): address numbered read review feedback

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-11-11 11:08:39 +08:00
ZephyrWang
bf4aa82f09
fix(telegram): deliver rejected photos as documents (#8056)
* fix(telegram): deliver rejected photos as documents

* fix(telegram): log rejected-photo document fallback
2026-11-11 08:03:40 +08:00
Hyeonsang Cho
8850f8eb7e
fix(runtime): keep assembly evidence from reviving a run a peer reclaimed (#8046)
* fix(runtime): keep assembly evidence from reviving a run a peer reclaimed

In heartbeat mode every run-row write is conditional on this worker's
live lease except assembly evidence, which went through RunStore.put().
That upsert rewrote status, owner, lease, error and stop_reason from the
local record. When a peer had already reclaimed the run, it put the row
back to running and erased the takeover error. If the owner's local lease
was still live, its renewal then succeeded, the peer never reclaimed it
again, and the run finished as success after the peer had published its
terminal recovery event. If the write landed late, the row came back with
an expired lease and was reclaimed a second time.

Add RunStore.merge_owned_metadata(), which merges metadata keys only for
the owner holding a live lease on a pending/running row and writes no
other field. SQL follows the existing clock -> row lock -> conditional
UPDATE order; MemoryRunStore mirrors it. With heartbeat enabled,
record_assembly_evidence() uses it, refuses a locally fenced run before
any write, fences the run when the store rejects the merge, and fails
closed on stores without the method instead of falling back to put().
Single-worker mode keeps the full snapshot.

* docs(changelog): reference #8046 for the assembly evidence takeover fix

* test(runtime): scope the simulated lease expiry to the run's thread

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-11 07:56:53 +08:00
PeaceMaker-best
cf8c29c6b6
fix(agents): fence late progress assessments across graph entries (#8083)
* fix(agents): fence late progress assessments across graph entries

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

* docs(agents): keep combined progress guidance within budget

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: Totoro <msy626836554@gmail.com>
2026-11-10 22:04:04 +08:00
一片一片
989b20b1dc
fix(hitl): never pause on an approval call without a usable id (#8070)
* fix(hitl): never pause on an approval call without a usable id

A provider can emit a tool call with no id (langchain_core keeps `id=None`),
an empty id, or the same id twice in one response. `HitlApprovalMiddleware`
copied that id into the interrupt payload, but the Gateway resume validator
requires unique non-empty strings: every resume answered 422 and every new run
stayed 409 `approval_pending`, so the thread was wedged permanently — the
all-reject escape is validated after that check. Denial paths also built
`ToolMessage(tool_call_id=None)`, which raises inside `after_model`, so a
non-interactive run failed instead of denying the call.

Treat such calls as unaddressable: strip them from the model response with a
visible rejection note instead of pausing, and deny by removal whenever a
`ToolMessage` could not pair. The tool never executes and the thread stays
usable.

Fixes #8069

* docs(hitl): state the empty-id rule accurately and track legacy recovery

Review follow-up on the unaddressable-approval guard:

- An empty tool-call id was technically resumable end to end (langchain-core
  accepts `ToolMessage("")`, and the Gateway validator accepts `""`), so only a
  null or duplicated id can wedge a pause. The rule still denies empty ids
  because strict providers reject them when the conversation is replayed; the
  docstring, CHANGELOG and interaction policy now say exactly that, and
  `_process_responses` uses the same `_addressable` predicate as the pause
  guard so the two checks cannot drift.
- Add an operator note for pauses stored before this fix: they are not cleared
  here, so continue in a new thread; in-place all-reject recovery is tracked in
  #8071.

* fix(hitl): drop a colliding call id from every serialized surface

Review follow-up: `_strip_calls` removed the unaddressable approval calls from
`tool_calls`, but `clone_ai_message_with_tool_calls` filters the raw
`additional_kwargs["tool_calls"]` payload and content tool-call blocks by id.
A non-target call sharing a removed id kept that id in `kept_ids`, so the
removed calls stayed in the raw provider payload and the next request would
advertise calls that have no result.

Drop the whole colliding id group: an id shared with a denied approval call
cannot be answered unambiguously either. The regression covers a mixed response
(duplicate approval ids plus an ordinary call with the same id) and asserts the
raw payload, content blocks, execution and visible note.

* fix(hitl): reject an id shared with an ordinary call before pausing

Review follow-up: `_addressable(targets)` only asked whether a `ToolMessage`
can answer the targeted calls. A single approval-required call whose id is
shared by an ordinary call in the same response still paused, even though
approving or rejecting it would also answer the ordinary call by id.

Add `_unambiguous_ids`: every targeted id must also be unique across the whole
response. A collision denies the colliding group (strip + visible note) before
the pause, and the resume path applies the same check to a legacy pause. New
regression: one approval-required call and one ordinary call sharing an id.

* fix(hitl): strip a deny target whose id an ordinary call shares

`_deny_all` routed only on `_addressable`, which checks uniqueness among the
targeted calls. A target whose id an ordinary non-target call also carried was
therefore answered with a deny `ToolMessage` while the ordinary call stayed
serialized, so the next request advertised the id twice with two results.

`_deny_all` now requires `_unambiguous_ids` and strips the whole colliding id
group, matching the pause path. The tool still never executes.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 21:30:43 +08:00
Wenchao An
6fe87b65fa
feat(extensions): show package-owned usage in extension details (#8031)
* feat(extensions): show package-owned usage in extension details

* test(extensions): align bookmark checks with detail UI

* fix(extensions): reconcile package documentation and locale fallbacks

* fix(extensions): align discovery locale fallback

* fix(extensions): isolate malformed optional documentation

* docs: keep backend guidance within inherited budget
2026-11-10 18:41:27 +08:00
ZephyrWang
42da64a25c
fix(dingtalk): verify recipient status for file sends (#8089) 2026-11-10 17:50:40 +08:00
liyinlong
99e395aa9c
fix(runtime): recover completed thread reservations on admission (#8068)
* fix(runtime): recover completed thread reservations on admission

* Address thread reservation recovery review

* Place reservation recovery note below agent fixes

---------

Co-authored-by: liyinlong <173714821+lilong555@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 15:40:34 +08:00
yetuge
d49e0f3d7c
fix(gateway): reject unusable skill_context entries at the input boundary (#8091)
* fix(gateway): reject unusable skill_context entries at the input boundary

merge_skill_context indexes entry["path"] and calls entry.get(), so a
caller-supplied non-object entry or one without a usable path raised during
the state merge and failed the run with a 500. Validate the list shape at the
run-input and thread-state boundaries, mirroring the todos and delegations
channels.

* Normalize entry by ensuring 'name' is a string

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-10 15:36:01 +08:00
PeaceMaker-best
91f4fb4570
fix(memory): preserve retrieval indexes on operational startup failures (#8084)
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-11-10 15:14:52 +08:00
Evan
226d1bc5ce
fix(runtime): persist a rejected HITL call in the resume run's history (#8073)
* fix(runtime): persist a rejected HITL call in the resume run's history

An approval pause and its resolution are two Runs. The paused Run dispatches
the gated call; the resume Run produces its result. An approved call executes
and reaches on_tool_end, but a rejection is written by HITL after_model with no
tool callback. Journal reconciliation only persists such middleware-written
results for calls dispatched by the current Run, so the rejection stayed in
checkpoint state and never reached the run-event history: /messages and the
resume run's messages had no llm.tool.result for it.

The worker already reads the durable interrupt payload to recover the gated
tool names. It now also returns each paused tool_call_id with its name, and the
resume journal adopts those calls, so the existing reconciliation persists the
rejection under the resume Run. Approved results keep their tool-callback event
and are deduplicated by message id; results from other earlier Runs are still
not re-attributed.

* fix(runtime): keep HITL replay names independent of call adoption

Derive the replay-gate tool names and the adopted call ids separately
from the matched pause's actions, so an action without a usable
tool_call_id still gates replay. Pin that the all-reject escape paths
(approval disabled, gated tools changed or cleared while paused) reach
the history feed under the resume run, check resume-run attribution in
the mixed-decision test, and add a CHANGELOG entry.

* refactor(runtime): drop unused _pending_hitl_resume_calls wrapper

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 15:11:09 +08:00
SuKoYa07
6d1b12f650
fix(summarization): count replayed vLLM reasoning (#8077)
* fix(summarization): count replayed vLLM reasoning

* fix(summarization): unwrap nested runnable bindings

* test(summarization): cover replayed reasoning in content blocks

---------

Co-authored-by: lovesky514514-cmd <273698517+lovesky514514-cmd@users.noreply.github.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
2026-11-10 15:08:00 +08:00
qkycir
d03f1234f5
fix(sandbox): recover BoxLite VMs on the provider event loop (#8072)
* fix: recover BoxLite sandboxes on the async runtime loop

* test: register the BoxLite scoped guidance

* test(sandbox): pin BoxLite recovery lifecycle signatures

* docs(changelog): link BoxLite recovery PR

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 15:05:33 +08:00
Wistom Zhang
1fbc06279a
feat(benchmark): add offline memory vs project-context comparison suite (part of #7884) (#8058)
* feat(benchmark): add offline memory vs project-context comparison suite (part of #7884)

* fix(benchmark): route prompts through DynamicContextMiddleware, flatten rows, and share test run

* refactor(benchmark): run through local Gateway and simplify first-pass suite (#7884)

* docs(agents): keep context memory benchmark guidance within chain budget

* fix(benchmark): verify DeerMem scope rejection

---------

Co-authored-by: Totoro <msy626836554@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 14:45:56 +08:00
xuilen
10bbcc92de
fix(serply): delegate max_results coercion to shared owner (#7869) (#7872)
* fix(serply): delegate max_results coercion to shared owner (#7869)

* refactor(serply): simplify count helper and consolidate tests

---------

Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
2026-11-10 14:09:40 +08:00
hdu_zhou
bfa2794c76
fix(uploads): keep dedupe counter before compound archive suffix (#8042)
Re-uploading a file whose name collides with a compound archive suffix
(e.g. .tar.gz, .tar.bz2) previously inserted the dedupe counter inside
the suffix: a second report.tar.gz became report.tar_1.gz, which broke
file-type detection. Treat the compound suffix as a single extension so
the counter lands before it (report_1.tar.gz), matching single-suffix
behavior (report_1.tar, report_1.txt).

- Add _COMPOUND_SUFFIXES and split stem/suffix as one unit, sorted
  longest-first with a closed, non-overlapping allowlist (.tar.br added).
- Expand tests to all 13 suffixes plus counter-escalation and plain .tar.
- Add a CHANGELOG [Unreleased] Fixed entry.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 09:52:01 +08:00
shunxinyi
54753636b1
fix(memory): recall CJK sub-phrases when jieba is not installed (#8022)
* fix(memory): recall CJK sub-phrases when jieba is not installed

Without the memory-zh extra, _tokenize fell back to whitespace splitting, so
the FTS5 index kept a whole CJK run as a single unicode61 token and every CJK
sub-phrase query returned no hits without any warning. The relevance ranker
already emitted CJK character bigrams for the same text, so the two retrieval
strategies disagreed on the same data.

Reuse relevance.cjk_token for CJK runs when jieba is unavailable and route
_preprocess_content through _tokenize, so the index and the query side agree.
Installs with jieba keep the existing jieba path.

Existing index rows are rebuilt on the first search or warm-up after the
upgrade; no schema or version change is involved.

* style(tests): format the CJK fallback regression test

`ruff format --check .` in the backend lint job reformats the new assertion in tests/test_memory_retrieval_adapter.py: the implicitly concatenated message is joined into one line. Apply the formatter output, no behavior change.

* fix(deermem): recall single CJK characters through an FTS5 prefix phrase

The fallback index stores CJK runs as character bigrams, so a one-character
query had no equal token and came back empty; `"重"*` is an FTS5 prefix phrase
that matches the bigram `重试` and still matches a standalone `重` token, while
multi-character tokens keep the plain quoted phrase. Also force the no-jieba
branch with a monkeypatch so the regression test runs on installs that have
the optional `memory-zh` extra, and cover the single-character and absent-
character cases.

* docs(deermem): scope the single-CJK prefix note to leading matches

The prefix phrase only reaches indexed tokens that start with the queried
character, so a character occurring only later inside a bigram (务 in 任务)
is not matched. Say that in the docstring and mark the residual gap as
accepted instead of widening the prefix to the run's trailing character.
2026-11-10 09:50:46 +08:00
zuiqiangxijiejin
ea7bb64f02
fix(nginx): use checkout-local temp paths for local development (#8075)
* fix(nginx): use checkout-local temp paths for local development

* docs(nginx): clarify local request buffering comment

* docs: keep nginx guidance within the inherited byte budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 09:47:37 +08:00
2SinceJuly
c612531213
fix(agents): drop empty typed text blocks from user content (#8066)
* fix(agents): drop empty text items from sanitized list content

InputSanitizationMiddleware merges list-form user content into one typed
{"type": "text"} block, but only replaces the items it collected. Empty
bare strings and empty text blocks are skipped by the extractor (matching
message_content_to_text) and then re-emitted as-is, so a request whose
content was ["", {"type": "text", ...}] still carried a bare "" content
part. The Chat Completions schema only allows typed content-part objects
in a content list, so endpoints that validate against it rejected the
whole turn — and kept rejecting every later call that resent the message.

Empty text items are now recognised during extraction and dropped when the
content list is rebuilt: leading, interleaved and trailing positions. A
list made only of empty items collapses to one empty typed block, and an
image-only message still passes through untouched. Nothing else changes:
non-text blocks keep their positions, and the transform stays
request-scoped.

* fix(agents): drop empty text items when rebuilding sanitized content

InputSanitizationMiddleware merges list-form user content into one typed
{"type": "text"} block, but it only replaced the items it had collected.
Empty bare strings and empty text blocks were skipped during extraction
(matching message_content_to_text) and then re-emitted unchanged, so a
message whose content was ["", {"type": "text", ...}] still carried a bare
"" content part. The Chat Completions schema only allows typed content-part
objects inside a content list, so endpoints that validate against it
rejected the whole turn, and kept rejecting every later model call that
resent the message.

Empty text items are now collected during extraction (they are text-shaped,
so the rebuild is what has to drop them) and therefore disappear in all
positions: leading, interleaved and trailing. A list made only of empty
items collapses to one empty typed block instead of leaking the bare
string, and an image-only message still passes through untouched.

Non-text blocks keep their existing placement rule, and the transform stays
request-scoped.

* docs(middleware): state that empty typed text blocks are dropped

The #8038 scope note in middlewares/AGENTS.md still said empty typed text
blocks remain unchanged and that providers can still reject them. This
change drops those blocks too, so the note described the opposite of what
the code does. Rewrite the summary and that bullet to match, and keep the
remaining limits explicit: user content only, and an empty user turn can
still be rejected.

* test(middleware): keep the legacy cases out of the new class and pin keep-alive

Inserting TestEmptyTextBlockItemsAreDropped before the existing
already-wrapped, graph-bubble-up and fail-open cases silently moved those
three tests into the new class, changing their node IDs and putting them
under a docstring about empty typed text blocks. Place the new class after
test_fail_open_on_processing_error so the legacy tests keep their home.

Also pin the boundary the sweep must not cross: a whitespace-only part,
typed or bare, carries no words but still keeps the turn alive. Assert
both shapes survive next to an image so tightening not block to not
block.strip() fails here rather than turning an empty user turn into a
rejected one.

* docs(middleware): keep the empty-text note inside the guidance size limit

The added paragraph pushed the effective AGENTS.md chain 55 bytes past its hard limit. Condense the same facts instead of adding a new line to the budget.

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
2026-11-10 08:04:43 +08:00
kongxin
f17e0504d0
fix(feedback): return 409 instead of 500 when feedback is POSTed twice (#8064)
* fix(feedback): return 409 instead of 500 when feedback is POSTed twice

POST /api/threads/{id}/runs/{rid}/feedback did a plain INSERT. A second
POST by the same user for the same run (a retry, or feedback on another
message of that run) hit uq_feedback_thread_run_user and the uncaught
IntegrityError surfaced as HTTP 500.

FeedbackRepository.create now rolls back and raises
FeedbackAlreadyExistsError on that collision, and the route maps it to
409 pointing callers at PUT, which already upserts.

* fix(feedback): only map the run/user unique key to 409; cover concurrent POSTs

Address review on #8064:
- create() now re-reads the (thread, run, user) row after an
  IntegrityError and raises FeedbackAlreadyExistsError only when it
  exists; any other violation (e.g. a feedback_id collision) propagates.
- Add a commit-barrier test where two first POSTs race: one 200, one
  409, and the winner's row is kept.
- Add CHANGELOG.md / CHANGELOG_zh.md Fixed entries.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 07:47:01 +08:00
ZephyrWang
f746da3a4c
fix(agents): count model calls independently of reused message IDs (#8050)
* fix(agents): count model calls independently of reused message IDs

* docs(agents): clarify budget accounting and changelog

* docs: avoid concurrent changelog insertion conflicts

* docs(agents): condense TokenBudgetMiddleware guidance to fit chain budget
2026-11-10 07:31:47 +08:00
inchang-ing
1647320ad1
fix(gateway): return 404 on cross-owner thread_id instead of 500 (#8057)
* fix(gateway): return 404 on cross-owner thread_id instead of 500

When a caller-supplied thread_id already belongs to another user, the SQL-backed store rejects the duplicate primary key with IntegrityError. The route's recovery path resolved the surviving row through the requesting owner's scope, so a foreign row could not be read back and the route surfaced a 500 instead of the documented 404 ownership-conflict response ("Thread not found").

Read the row back bypassing the owner scope before collapsing to a 500 so the cross-owner case mirrors ThreadOwnershipConflictError.

Fix #8052

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>

* test(gateway): pin thread-ownership 404 and real-failure 500

Address review feedback on the cross-owner 404 recovery:
- Update test_insert_race_concurrent_trusted_claims_have_one_winner: the
  losing request now resolves the surviving unscoped row as a 404 ownership
  conflict instead of a 500, while the single-winner assertions and the
  non-disclosing detail are preserved.
- Add test_create_thread_keeps_real_integrity_failure_as_500 to pin the
  fall-through branch where a duplicate-key IntegrityError coincides with no
  surviving row, which must still surface as a 500 rather than collapse into
  the ownership-conflict 404.
- Document the user-facing error-surface change in the Unreleased Fixed
  section.

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>

---------

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
Co-authored-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 06:22:24 +08:00
yetuge
0dc423605a
fix(browserless): resolve the token through the shared credential rule (#8043)
* fix(browserless): resolve the token through the shared credential rule

Browserless read BROWSERLESS_TOKEN and the configured token raw, so a value
from a file kept its trailing newline and became the auth query parameter;
every request then failed authentication. A whitespace-only value was sent as
a token instead of counting as unset. Use provider_credentials.resolve_api_key
like the other search and fetch providers, with the blank-config fallback to
the environment.

* fix(browserless): keep an explicit blank token opt-out

An explicitly configured token ("" or null) is authoritative: a self-hosted
capture endpoint uses it to opt out of authentication, and falling back to
BROWSERLESS_TOKEN sent the cloud credential to that endpoint. Only an omitted
token key falls back to the environment. Regression tests cover the client
token and the outgoing capture query.

* docs(changelog): record the browserless token normalization

Match the sibling #7981 entry in both locales; the fix closes out the
credential-rule family for the bundled search and fetch providers.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 05:59:16 +08:00
lihongyuan99
b0b3a8aa22
fix(typesafe): build the async HTTPX client off the event loop (#7931)
* fix(typesafe): build the per-evaluation HTTPX client off the event loop

TypeSafeClient.aask constructs httpx.AsyncClient on the running loop, and
that constructor loads the certificate store synchronously
(create_ssl_context -> ssl.create_default_context(cafile=certifi.where())).
aask is the path GuardrailMiddleware.awrap_tool_call awaits, plus both
memory paths, so every guarded tool call stalls the Gateway loop for the
duration of the trust-store load.

Move the construction into asyncio.to_thread, the shape backend/AGENTS.md
records for the other HTTPX clients and the one the merged Crawl4AI anchor
tests. Requests and cleanup stay on-loop, so the deadline scope, retry
loop, bounded body read and the error taxonomy are untouched.

* fix(typesafe): close clients built during cancellation

Co-authored-by: Niranjan Bajgai <2053809+bajgai@users.noreply.github.com>

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Niranjan Bajgai <2053809+bajgai@users.noreply.github.com>
2026-11-09 21:53:26 +08:00
Hyacinth-of-Security
5c5cf24ff2
fix(skills): read undecodable skill state as missing (#7963)
* fix(skills): read undecodable skill state as missing

`_read_manifest` and `_read_skill_states` document that an unreadable file
means "no manifest" / "no overrides", and both catch OSError and
JSONDecodeError. A file that is not valid UTF-8 raises UnicodeDecodeError,
which is a ValueError and matches neither, so the projection and the skill
state read raise instead of falling back.

Catch it alongside the existing exceptions.

* fix(skills): keep the reason when profile state cannot be decoded

Review feedback on #7963: the state reader dropped the exception, so a decode
failure that silently discards the user's disabled set left nothing to diagnose,
and the next toggle overwrites those bytes. Log the exception, and log a debug
line when an unreadable projection manifest forces a rebuild.

* docs(skills): state that undecodable skill state fails open

_read_skill_states already degraded a malformed file to {} before this change;
making undecodable bytes take the same path made that explicit, so say so next to
the sandbox check that documents the opposite verdict for a single named skill.

---------

Co-authored-by: lxcxjxhx <lxcxjxhx@users.noreply.github.com>
2026-11-09 21:49:46 +08:00
yu1128-L
7a8051c0d9
fix(artifacts): bind ranged reads to validated generation (#7978)
* fix(artifacts): bind ranged reads to validated generation

* fix(artifacts): drain pinned reads without Windows hard links

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 21:18:57 +08:00
lilei
31d97fc85b
fix(memory): select FTS5 matches by composite score (#8005)
* fix(memory): select FTS5 matches by composite score

Score every SQL-filtered match before selecting the final composite top-k.
Keep candidate retention bounded with a streaming heap and preserve the
existing scoring formula and adapter-level filtering behavior.

Add regressions for confidence and time-decay reversals, category filters,
result prefixes, ties, and the public DeerMem storage path.

Fixes #8003

* fix(memory): report matched and retained FTS5 rows

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 21:16:18 +08:00
SpecStream
fa8e015023
feat(community): add SpecStream web search and fetch provider (#7968)
* feat(community): add SpecStream web search and fetch provider

* fix(specstream): keep a direct answer when sources are empty, add changelog entry

Signed-off-by: Nataliia <nati@macbook-pro-nataliia.home>

---------

Signed-off-by: Nataliia <nati@macbook-pro-nataliia.home>
Co-authored-by: Specstreamai <333137486+Specstreamai@users.noreply.github.com>
2026-11-09 20:32:22 +08:00
Severus
7adb2062eb
fix(gateway): keep retryable thread inputs ahead of terminal receipts (#7972) (#8008)
* fix(gateway): keep retryable thread inputs ahead of terminal receipts (#7972)

* fix(gateway): document thread-input ranking policy (#7972)

* test(gateway): cover thread-input ranking and receipt starvation (#7972)

* test(gateway): format thread-input regression tests with repo ruff config (#7972)
2026-11-09 20:28:09 +08:00
一片一片
3ebd4de993
fix(scheduler): isolate a malformed stored schedule from the poll batch (#7999)
remain from its review:

- `stored_next_run_at` caught the ValueErrors a dropped zone or a newer cron
  parser raises, but a stored `schedule_spec` that is JSON null, a list or a
  string fails on `.get` with AttributeError. The launch-failure handler
  recomputes the schedule, so that escaped `run_once`'s claimed-task loop and
  skipped the rest of the batch. Catch it too; the task is paused with the
  reason like the other unusable schedules.
- `run_once` dispatched its claimed batch with a bare loop, so any per-record
  raise in `dispatch_task` re-aborted the batch. Isolate each task's dispatch
  and keep claim-level store failures loud above the loop.

Tests: `test_scheduler_poll_batch_isolation.py` (non-object specs, end-to-end
pause, per-task isolation). Red on `91f20a7`, green at head.

Refs #7998
2026-11-09 20:25:32 +08:00