4571 Commits

Author SHA1 Message Date
Totoro
2464566942
fix(summarization): cover all selected history before compaction (#6918)
* fix(summarization): cover all selected history before compaction

* fix(summarization): bound compaction work and recover oversized carries

* docs: leave headroom in middleware guidance chain

* fix(summarization): cool down repeated chunk-limit failures

* test(runtime): complete callback cleanup graph fixture

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-16 09:41:20 +08:00
RongJie G
b0a545ec90
fix(harness): stop length-capped Responses tool calls (#6946)
Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
2026-10-16 09:37:18 +08:00
Dragon ze
fd61c48fa2
fix(acceptance): reject all-skipped go test records as evidence (#6957)
A `go test -v` run whose every case is skipped exits 0 and prints the same
`ok` package banner a passing run does, so the banner alone supplied positive
evidence for `tests_passed:` while no test executed. unittest (#6916) and Cargo
(#6891) already enforce the non-skipped bar; Go's per-case `--- SKIP` lines are
the only record content that separates the two here.

Non-verbose `go test` output is byte-identical for an all-skipped and a passing
package, so those records stay judged by the banner alone.
2026-10-16 09:10:35 +08:00
xuilen
5633320a56
test(backend): preserve LF endings in read_file fixtures on Windows (#6438) (#6949) 2026-10-16 09:01:28 +08:00
CSXizhang
3fd03ec2eb
fix(compaction): reject manual compaction of threads with pending execution (#6929)
* fix(compaction): reject manual compaction of threads with pending execution

A thread paused at a static breakpoint (interrupt_before=["tools"]) keeps its
pending node send in the checkpoint's __pregel_tasks channel. The compaction
write goes through the state-mutation graph, which schedules no tasks, so
LangGraph drops the pending send ('Ignoring unknown node name tools in pending
sends') and a resumed run finishes success without ever executing the tool.

Fail closed: detect a non-empty __pregel_tasks in the raw checkpoint tuple
inside compact_thread_context and raise ContextCompactionPendingWork, mapped
to HTTP 409 by the compact route. The reservation (reserve_checkpoint_write)
already serializes this check against runs and other checkpoint writers, and
the write still forks from the exact head that was read.

Adds dual-mode (full/delta) regression tests: the rejected compaction leaves
checkpoint id, next and pending task ids untouched, and resuming afterwards
executes the pending tool exactly once; idle threads still compact.

* fix(compaction): reject unavailable checkpoints before summarizing

* docs: keep compaction guidance within merged size budget
2026-10-16 08:59:44 +08:00
D41910
0f2c2f788a
refactor(community): fold serper onto the shared max_results owner (#6966)
Serper keeps a private _coerce_max_results in the pre-#5852 shape: a bare
int() silently turns max_results: true into 1 and 3.5 into 3, and an invalid
value falls back without a warning (#5865).

Delegate to deerflow.community.search_max_results.coerce_max_results, which
owns what invalid means for the folded providers, and keep Serper's
10-result cap at the call site the way You.com does, because clamping policy
is deliberately per-provider.

Invalid configured counts now warn under the Serper label and fall back to
the default instead of being silently coerced.
2026-10-16 08:57:16 +08:00
theater
c3ac4b981f
fix(e2b): grep literal patterns verbatim instead of re-escaped (#6952)
E2BSandbox.grep() applied re.escape() to the caller's pattern and then
handed the escaped text to grep -F. Fixed-string grep treats the added
backslashes as part of the text to find, so an exact literal such as
'alpha.beta' never matched. Align with the boxlite/tenki/opensandbox
seams: grep receives the RAW pattern (-F matches it literally, -E as a
regex), and only a non-literal pattern is compile-validated locally.

A shared-contract regression covers dot/space/bracket/backslash/plus
literals through every shell-backed provider.
2026-10-16 08:31:55 +08:00
万家良
4f1c848744
fix(runtime): defer host cancellation through the whole terminal finalization (#6914)
* fix(runtime): defer host cancellation through the whole terminal finalization

Cancelling run_agent() during an early terminal-finalization await (e.g.
the delivery-receipt write) escaped the surrounding `except Exception`
guards, because asyncio.CancelledError derives from BaseException. The
cancellation then skipped the remaining terminal tail: terminal status
persistence, the completion hook, task-stop notification, and the stream
END marker.

Add `_await_despite_host_cancellation`, which shields each terminal step
so the started awaitable keeps running under host cancellation, defers the
interruption (uncancelling the task), and re-awaits the shield until the
step completes. Apply it to the durable finalization stages; the original
interruption is still re-raised after publish_end.

* fix(runtime): bound terminal deferral and shield abort-path cancellation

* fix(runtime): retain owned finalization through cancellation

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-16 07:25:19 +08:00
Dragon ze
33a3970c6b
chore(ci): refresh the shard duration baseline (#6963)
* chore(ci): refresh the shard duration baseline

backend/.test_durations was last regenerated on 2026-09-22 (61ae228b0, #5656)
and now covers 40.1% of the collected suite, so `least_duration` plans 19,415
tests from the imputed average instead of measured cost. Replaying CI's 4-way
split with the committed file yields shard loads of 203.5/217.4/234.3/222.9s
(1.15x heaviest-to-lightest); with this refresh the same replay is 219.6s each.

Regenerated with the documented target: `make test-shard-durations`, i.e.
`pytest -m "not live" --ignore=tests/blocking_io --store-durations
--clean-durations --durations-path=.test_durations tests/ -q` on main at
8d994cc1e. Data-only diff; the shard jobs keep reading the file unchanged.

* chore(ci): cover the four tests added by 6b60903d7

The checkpointer/postgres-schema helper suites gained 4 cases while this
refresh was in flight, which would have left the baseline 4 entries short of
the base tip. Re-ran just those two files through the same store path, so the
file now covers 32,396/32,396 collected tests with no gap. Re-measuring those
files also refreshed 113 neighbouring entries.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-16 07:10:06 +08:00
Chaoqi
6b60903d73
fix(runtime): strip the SQLAlchemy driver suffix from sync and store DSNs (#6917)
* test(runtime): cover the driver suffix in sync and store DSNs

* fix(runtime): strip the SQLAlchemy driver suffix from sync and store DSNs

With `postgres_url: postgresql+asyncpg://...` and the default empty
`postgres_schema`, the sync checkpointer, the sync store and the async
store passed the DSN to psycopg's `from_conn_string` unchanged, and
libpq cannot parse the `+asyncpg` scheme. `dsn_with_search_path` only
drops the suffix when it adds a schema, so a non-empty schema hid the
bug. The async checkpointer already calls `normalize_libpq_dsn` first.

Call `normalize_libpq_dsn` at these three call sites as well.

* refactor(persistence): drop the driver suffix inside dsn_with_search_path

dsn_with_search_path dropped a SQLAlchemy `+driver` suffix only when it
added a search_path, so every libpq caller had to wrap the DSN in
normalize_libpq_dsn to cover an empty schema, and flattening that
nesting later would bring the bug back.

Normalize at the top of dsn_with_search_path so both paths drop the
suffix and check the scheme in one place, and remove the outer call
from its five callers. The sync checkpointer and both stores are back
to their code on main.
2026-10-15 23:45:09 +08:00
Guo-Yixin
8d994cc1e7
fix(acceptance): reject nonpassing unittest summaries (#6916)
* fix(acceptance): require passing unittest counts

* test(acceptance): retain unittest output attribution guard

* test(acceptance): cover unittest errors and recorded failures

* test(acceptance): honor unittest empty-selection exit status

* docs(subagents): keep unittest guidance within inherited budget
2026-10-15 15:26:47 +08:00
王一帆
6bc1020d34
feat(knowledge): decouple scope and read-only catalogs (phase 2) (#6904)
* feat(knowledge): decouple scope and read-only catalogs

* docs(knowledge): keep frontend guidance within repository budget

* fix(knowledge): align execution identity and harden provider boundaries

* docs: keep knowledge guidance within CI size budgets

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 15:10:39 +08:00
Jason Cat
edcb78fb87
fix(search): surface DuckDuckGo search failures instead of reporting an empty result (#6648)
* fix(search): surface DuckDuckGo failures instead of reporting an empty result

DDGS execution failures (rate limits, timeouts, blocked HTML/vqd extraction,
SDK errors, and the missing ddgs dependency) were caught in _search_text and
converted into an empty list, so web_search reported "No results found" for
every failure. The agent then read an outage as a query that matched nothing
and rewrote the query instead of trying an alternative.

_search_text now raises DDGSearchError carrying the exception type and message
for those failures, and web_search_tool answers them with a structured
"DuckDuckGo search failed: ..." error. A genuinely empty result set (the SDK's
exact DDGSException("No results found.") sentinel) still returns the existing
"No results found" response, and tool_result_meta classifies the two cases
differently (transient/config vs no_results).

Tests: tests/test_ddg_search_tools.py (three new failure tests red on main),
tests/test_agent_guidance_check.py, tests/test_tool_result_meta.py; ruff check
and ruff format --check clean.

* fix(search): keep DDGS client construction inside the error boundary

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 15:06:51 +08:00
Chaoqi
d06fc14be9
fix(image-search): keep the source page URL in DuckDuckGo and Serper results (#6915)
* test(image-search): expect source_url in DuckDuckGo and Serper results

* fix(image-search): keep the source page URL in DuckDuckGo and Serper results

The DuckDuckGo and Serper image_search tools returned only title,
image_url and thumbnail_url and dropped the page each image comes from
(DDGS `url`, Serper `link`). Brave already returns it as `source_url`,
and the workspace UI links every image thumbnail through `source_url`,
so DuckDuckGo (the default provider) and Serper results rendered as
"Unsafe link omitted" and the agent lost the image's source page.

Return `source_url` from both providers, screened with
`safe_public_result_url` like the image and thumbnail URLs.
2026-10-15 14:56:46 +08:00
Hyeonsang Cho
2dc92a2e4a
docs: add a Korean README (README_ko.md) (#6909)
* docs: add a Korean README translation

* docs(changelog): reference #6909 for the Korean README

* docs(readme-ko): restore blank lines before headings and sync with README.md

Six headings sat directly under the preceding paragraph or code fence where
README.md keeps a blank line. Also translate the README.md additions from
#6898 (Codex tool_choice), #6886 (mixed-provider citation cleanup), and #6701
(authenticated HTML preview resources).
2026-10-15 14:45:29 +08:00
Willem Jiang
3d02cd4607
fix(tests): keep subagent guidance within chain budget (#6926) 2026-10-15 14:36:47 +08:00
Dragon ze
2768b86fff
fix(community): reserve Browserless capture names before publishing (#6901)
* fix(community): reserve Browserless capture names before publishing

Name selection and publication were two separate operations:
`_dedupe_output_name` probed with `Path.exists()` and then
`publish_bytes_atomically` ran `os.replace`, so two `web_capture` calls in the
same thread outputs directory could both settle on `report.png` before either
wrote, and the second publication replaced the first capture while both reported
success for the same artifact path (#6883). `web_capture_tool` runs the writer
through `asyncio.to_thread`, and same-second default names collide the same way
because the generated stem's timestamp has second resolution.

Claim the name instead of checking for it: an `O_CREAT | O_EXCL` open either
creates an empty placeholder or reports the name held, so the probe walk
(-1, -2, ... then the microsecond timestamp) is race-free. `_write_capture_output`
releases the placeholder when publication fails, catching BaseException so an
interruption also frees the name, and a failing unlink is logged rather than
allowed to mask the original error - the same error-masking family #6871 is
addressing in the shared publisher, which this change deliberately does not
touch.

When even the timestamped names are held, the function now raises instead of
returning a name it cannot own; returning one would reintroduce the silent
overwrite. `tests/test_browser_outputs_atomic.py` pins the publisher call as
source text, so its expectation moved to the new call shape in the same commit.

Refs #6883

* fix(community): open the reserved capture name with new-file permissions

An `os.open` without an explicit mode creates the file with `0o777 & ~umask`, and
`publish_bytes_atomically` preserves an existing destination's mode, so the
reservation added in the previous commit made every capture executable (`0o755`
under the usual umask) where main produced `0o666 & ~umask` (`0o644`) through the
publisher's new-file branch. Pass `0o666` explicitly and pin the mode against a
`Path.write_bytes` reference in the same directory, which is the permission the
publisher documents for new files.

Refs #6883

* fix(browserless): warn when capture reservation cleanup fails

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 12:44:09 +08:00
yi-coding
3db2fc8bb4
fix(subagents): fence durable batch lease lifecycles (#6764)
* fix(subagents): fence durable batch lease lifecycles

* fix(subagents): fence batch claims and require captured lease owners

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 12:02:00 +08:00
spud
e7a2a1c331
fix(runtime): own RunJournal writes and quarantine ambiguous outcomes (#6846)
* test(runtime): pin RunJournal write-ownership and cancellation safety

* fix(runtime): own started journal writes and quarantine unknown outcomes

* docs(runtime): document RunJournal write-ownership contract

* fix(runtime): address RunJournal review feedback

* fix(runtime): log successful settlement of abandoned journal writes

* fix(runtime): serialise explicit RunJournal flush and close ownership

* test(runtime): assert concurrent RunJournal close and flush success paths

* fix(runtime): make RunJournal close abort-aware and terminal-monotonic

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 11:48:21 +08:00
D41910
3195ed1f09
fix(serper): report a format error for results with no usable objects (#6779)
* fix(serper): report a format error for results with no usable objects

`_response_items` skipped non-object entries but returned the filtered
list even when it was empty, so a non-empty `organic`/`images` array
holding no objects fell through to "No results found" / "No images
found" -- telling the agent the query legitimately matched nothing
instead of surfacing the malformed payload.

Treat a non-empty container with no object entries as malformed, matching
the containment rule #6603 documents for this provider family (brave
#6550, groundroute #6603, tavily #6645, webz). `web_search` and
`image_search` share this helper, so both now report the provider's
format error. Missing, null, and empty containers keep reading as an
empty search, and mixed lists still keep their valid entries in order.

* fix: address review and validation findings for PR 6779
2026-10-15 11:37:59 +08:00
yetuge
ac9ed11b8b
fix(opensandbox): mark explicit command deadlines as Exit Code: 124 (#6885)
* fix(opensandbox): report the command deadline as Exit Code: 124

`execute_command` had no timeout branch, so a command that hit its deadline
was reported as whatever the SDK happened to surface. OpenSandbox is the last
provider without the `Exit Code: 124` marker that Local/AIO/Tenki/E2B/BoxLite
emit, and which #6847 established the evidence chain parses to recover the
real shell status.

The deadline is enforced by execd, not by the client: the timeout travels to
the server in `RunCommandOpts`, the SSE read timeout is disabled, and the
process group is reaped with SIGKILL. So a timed-out command does not raise --
it comes back as a `CommandExecError` result with the signal death reported as
exit code -1, which the adapter rendered as `Exit Code: -1`.

Normalize that pairing to the 124 marker, keeping the partial output the
command produced before the kill. The guard is deliberately narrow: a command
that exits non-zero on its own never carries the error event, and a failed
launch shares the same error event with no usable code, so neither is
relabelled a timeout.

Tests: three cases pin the deadline paths (client-side TimeoutError from a
bridge, the server-side -1 kill with output, and the free-text shape that must
stay on the existing missing-exit-code path as a start failure), plus a case
pinning that ordinary exits keep their own code.

RED/GREEN: the deadline cases fail on the unmodified source and pass with it;
test_opensandbox_provider.py 114 passed (110 pre-existing unchanged); the four
sandbox provider suites 601 passed; ruff check and format clean.

* fix(opensandbox): recognize explicit deadline results

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

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-10-15 11:36:41 +08:00
万家良
c324f351ee
fix(sandbox): truncate quarantine path keys to stay under Windows MAX_PATH (#6880)
* fix(sandbox): truncate quarantine path keys to stay under Windows MAX_PATH

The quarantine nests three hashed path components (scope, sandbox id,
container id). Three full 64-char SHA-256 hexdigests exceed Windows'
260-char MAX_PATH once the quarantine root lives under a long path, so
mark() fails with ENOENT creating the generation record. Truncate the
digest to 16 hex chars, matching the sandbox-naming convention used
elsewhere in this module.

* test(sandbox): pin quarantine key truncation to 16 hex chars

Guard against a regression that reverts the key digest to a full 64-char
SHA-256 hexdigest: three nested 64-char components exceed Windows' 260-char
MAX_PATH and re-break record creation, while Linux CI still passes.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 11:00:01 +08:00
Guo-Yixin
d2961aabae
fix(uploads): keep malformed metadata from aborting chat turns (#6882)
* fix(uploads): keep malformed metadata from aborting chat turns

* fix(uploads): log skipped probes and bound size labels

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:56:26 +08:00
BaiHeng Peng
55b1f1cfba
fix(client): preserve text appended before the first state snapshot (#6910)
* fix(client): preserve text appended before the first state snapshot

* test(client): exercise first-snapshot reconciliation with stable IDs

---------

Co-authored-by: BaiHeng Peng <191328900+Aldenshile@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:53:03 +08:00
Totoro
c994826d06
feat(channels): generalize group notifications and add opt-in task/goal IM notices (#6903)
* feat(channels): generalize IM group targets and add task/goal run notifications

- Generalize proactive group notification capability (proactive_group_notifications), binding validation, and scheduled task group selection across Feishu, QQ, and WeCom while keeping WeChat personal-only.
- Add QQ group active notification delivery via POST /v2/groups/{group_openid}/messages (msg_type=2) and WeCom group active notification delivery via WS Client.sendMessage(chatid, body) with bot_id:group:chatid workspace isolation.
- Generalize notification outbox sources via migration 0043_runtime_im_notices (source_kind, source_key, run_id, nullable task_id/task_run_id) with backward-compatible SQLite/PostgreSQL upgrades.
- Add opt-in ordinary task & goal run IM notifications (run_completed, run_failed, human_input_requested) on the shared outbox, delivery worker, renderer, and settings UI.

* fix(notifications): address review feedback and group/runtime notification edge cases

* test(migrations): update CURRENT_HEAD to 0043_runtime_im_notices across migration chain tests

* fix(channels): preserve group binding names and legacy authorization

Carry owner-chosen group names through existing connect state, keep private bindings out of legacy group fallback, and restore completion metadata retries. Cover real binding and worker finalization paths and synchronize notification docs.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:30:55 +08:00
XY
2d6b406801
fix(runtime): preserve run event timestamp offsets in SQLite (#6894)
* fix(runtime): preserve run event timestamp offsets in SQLite

* docs: clarify run event timestamp upgrade behavior

---------

Co-authored-by: XY-code1 <231763295+XY-code1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:19:26 +08:00
Lucien
8008a6195f
fix(artifacts): authenticate isolated HTML preview resources (#6701)
* fix(artifacts): authenticate isolated HTML preview resources

* docs(artifacts): retain precise path guidance within size budget

* fix: preserve HTML preview auth and rendering semantics

* fix(artifacts): quote rewritten CSS URLs

* docs: keep merged artifact guidance within size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:15:44 +08:00
kuku
a6b3552362
fix(models): preserve Codex tool_choice in requests (#6898)
* fix(models): preserve Codex tool_choice in requests

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>

* fix(models): validate Codex tool selection before HTTP

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>

---------

Signed-off-by: kuku <186267668+xbzz1018@users.noreply.github.com>
Co-authored-by: kuku <186267668+xbzz1018@users.noreply.github.com>
2026-10-15 09:58:16 +08:00
万家良
11979fdb57
fix(models): route gpt-6 tool calls through the OpenAI Responses API by default (#6863)
* fix(models): route gpt-6 tool calls through the OpenAI Responses API by default

The locked langchain-openai==1.2.1 routes gpt-6 models configured with
langchain_openai:ChatOpenAI through /v1/chat/completions, where OpenAI
rejects function tools for the gpt-6 family with an HTTP 400. Upstream
only added the gpt-6 + tools -> Responses API branch in 1.7.x, so
backport that routing decision at the model factory instead of bumping
the dependency (large uv.lock churn, wider regression surface).

_apply_openai_responses_api_default() injects use_responses_api: true +
output_version: responses/v1 for BaseChatOpenAI subclasses whose model
starts with gpt-6, unless the operator configured an explicit value.

Fixes #6841

* fix(models): narrow gpt-6 Responses API default per review

Addresses review findings on #6863:

- Custom endpoints (base_url/openai_api_base) are never touched, so
  OpenAI-compatible gateways/proxies (LiteLLM, vLLM, one-api) keep their
  chat-completions routing instead of being silently re-pointed at /responses.
- AzureChatOpenAI is skipped too (MRO-based, so env-configured Azure and any
  subclass are covered): Azure's Responses API requires api-version >=
  2025-04-01-preview.
- An explicit use_responses_api still always wins; a lone explicit
  output_version is preserved while use_responses_api: true is still injected,
  so gpt-6 no longer silently falls back to /v1/chat/completions.
- The default injection is logged so operators can trace the /responses switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(models): treat env-var-wired gateways as custom endpoints for gpt-6

Addresses the remaining review findings on #6863:

- A gateway wired purely via OPENAI_BASE_URL/OPENAI_API_BASE (the openai SDK
  fallback when no base_url is passed) is now treated like an explicit
  endpoint, so a gpt-6 model behind an env-var-wired gateway is not silently
  re-pointed at <gateway>/responses. The known api_base-subclass blind spot is
  documented in the comment block.

- Regression tests pin the env-var endpoint gate and the explicit-wins
  precedence (an explicit use_responses_api still wins on a custom endpoint).

* test(models): isolate Responses routing from endpoint environment

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 09:53:19 +08:00
Hyeonsang Cho
e02d5e2fc3
fix(mcp): skip tasks a cancel released while the poll claim was in flight (#6859)
* fix(mcp): skip tasks a cancel released while the poll claim was in flight

claim_due_tasks selected due rows with FOR UPDATE SKIP LOCKED, then set
the lease through an ORM flush that updates by id alone. FOR UPDATE is a
no-op on SQLite, so a request_cancel that committed between the SELECT
and the flush had its cleared poll lease written back. The cancellation
worker only claims rows without a live lease, so the remote cancel waited
for mcp_tasks.lease_seconds (120 s by default), and the task was polled
once more for a result apply_snapshot then discarded.

The claim now selects candidate ids and leases each one with a
conditional UPDATE that re-checks the claim predicates (pollable status,
no cancel request, due next_poll_at, no live lease), returning only the
rows it actually leased, the same atomic fence release_claim already
uses. On Postgres the row lock still orders the two writers.

* docs(changelog): reference #6859 for the MCP task claim fence

* docs(mcp): note that a short poll claim batch is not topped up
2026-10-15 09:42:46 +08:00
RongJie G
08560ea846
fix(knowledge): scope ragflow link cleanup to its native citation namespace (#6886)
* fix(knowledge): scope ragflow link cleanup to its native citation namespace

budget_source_artifact cleaned a delegated task summary with two regexes that
matched any #knowledge-... destination. When a task result carried both the
ragflow-native contract (#knowledge-<id>-<n>) and the unified contract from the
retrieval refactor (#knowledge-source-<id>), those regexes also flattened the
unified citation to plain text. budget_source_artifact only rebuilds the native
knowledge_sources, so the now-linkless unified evidence then failed the
prune_sources substring check and retrieval_sources was emptied. The native
citations in the same result survived, so a mixed result lost attribution for
its unified sources while keeping it for ragflow ones, even with budget headroom
to spare.

Anchor both regexes to the native namespace with a (?!source-) lookahead so they
leave #knowledge-source-<id> for prune_sources to reconcile downstream. Native
ids are 32-hex and never begin with "source-", so native link cleanup is
unchanged.

Add a regression that drives the real middleware entry point on a mixed-contract
task result and asserts the unified citation stays clickable and its
retrieval_sources record survives alongside the native one.

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

* fix(knowledge): remove dangling citation fragments from task summaries

---------

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 09:32:03 +08:00
ajayr
51bc0f160e
fix(llm-error): retry provider errors reported inside a 200 stream (#6897)
* fix(llm-error): retry provider errors reported inside a 200 stream

An OpenAI-compatible gateway that answers 200 early, to keep a slow
request alive with SSE keepalives, reports a later provider failure (a
504, say) as an SSE error event. The openai SDK raises that as the base
APIError, with no status code and no response, so _classify_error found
no status, no transient class name and no busy text, and called it
generic: the run failed on its first attempt.

An error with no status now takes an effective status from its own
contents, and the first source that yields one decides: a numeric code
on the error, or a numeric code, status or status_code in its body or
the body's error object; an explicit status in its message ([504],
HTTP 502, status 503); or type server_error (500). Its busy check also
reads each run of _, - and whitespace as one space, so rate-limit and
RATE_LIMIT_EXECUTION_TIMEOUT match "rate limit". Retry budget, backoff
and circuit breaker then treat it as the equivalent HTTP error. Errors
that carry a status are classified exactly as before, and the quota,
auth and burst-rate checks still run first.

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

* docs(llm-error): note how status-less errors are classified

Add one sentence to item 9 of the middlewares guide, and an llm entry
under Fixed in both changelogs, for the retry of provider errors
reported inside a 200 stream.

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

* test(llm-error): pin the in-band status source order and leftmost match

Two rules of _extract_in_band_status had no test: a numeric status
field decides before the message is read, and within the message the
leftmost in-range status wins. Either rule could be inverted with every
test still green. Add one stays-generic case for each: code 400 with
"[503]" in the message, and "HTTP 400 after [503]".

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

* fix(llm-error): do not read an array index as an in-band status

The bracketed alternative of the in-band status regex had no lookbehind,
so `messages[502]` or `tools[0][503]` matched and their number decided
the error's status. The bracketed form now starts with the lookbehind
`(?<![\w\]])`: a `[` directly after a word character or a `]` opens an
index, not a status. The `http` and `status` alternatives are unchanged;
`[504]: ...` in running text still counts.

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

* fix(llm-error): let an in-band status preempt the separator busy check

The separator-normalised busy check ran for every status-less error,
even when the in-band lookup had already found a status: "rate-limit
header malformed" with `code: 400` was retried as busy, while HTTP 400
is not. It now runs only when no source yields a status, so an in-band
4xx decides like an HTTP one. The same text with no status anywhere
still counts as busy.

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

* fix(llm-error): let only 4xx and 5xx values decide the in-band status

The in-band lookup accepted 100-599 values from its first two sources,
so a 1xx-3xx status could decide the classification: `HTTP 200` in a
message whose server_error type means 500, or `code: 200` alongside
`status: 503`. Only 400-599 now count: `_as_http_status` skips a
smaller value as it skips a non-numeric one, so the candidate loop
moves on, and an out-of-range message match is skipped so the scan
continues. An error that carries a status stays as it was, and the
server_error source is unchanged.

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

* docs(llm-error): describe the narrowed in-band status rules

The guide, the CHANGELOG and the CHANGELOG_zh now describe the three
narrowed rules: only a 4xx or 5xx value decides, an index such as
`messages[502]` is not a status, and the separator-matched busy check
runs only when no source yields a status. Only the sentences that
describe the affected forms changed.

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

* test(llm-error): pin the remaining in-band status guards

Cover the nested error.status_code field, a code above 599 that yields
to a later status field, a code written in non-ASCII digits, and an
in-band 4xx whose plain busy text still decides, as it does for an HTTP
4xx. These pin current behaviour; no code changes.

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

* docs(llm-error): say which in-band fields are read on the error itself

Only `code` is read on the exception; `status` and `status_code` are
read in its body and in the body's `error` object.

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

* docs(changelog): link the in-stream retry entry to #6897

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

* fix(llm-error): require a separator and ASCII digits in an in-band status

The message regex read `status503` and `statuscode503` as a status
because the separator after `status` was optional, and it read
Arabic-Indic or fullwidth digits because `\d` and `int()` accept every
Unicode decimal digit. A status-less error with such text was retried
as a 503.

The `status` form now needs spaces or tabs, or a `:` or `=`, before
the number, and all three forms read only ASCII digits.

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

* test(llm-error): pin the retry of an in-stream error after partial output

An `error` event that follows content chunks is retried like one that
comes first: the whole call re-runs and its result holds only the new
answer, while the tokens the first attempt already streamed to the
callbacks stay delivered, as with a mid-stream `ReadError`. The new
test pins that outcome, and a comment in `_classify_error` notes it.

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

* Handle transient errors based on status codes

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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-10-15 08:39:17 +08:00
Li-john1021
585f87d3b3
fix(slack): accept file_share events and download inbound files (#6855)
* fix(slack): accept file_share events and download inbound files

Slack was dropping every Events API message with a subtype, including
user uploads (subtype=file_share). Allowlist file_share, attach event
files to the inbound message, and download private URLs in receive_file
with the bot Bearer token (bytes via INBOUND_FILE_CONTENT_KEY).

Fixes #6853

* fix(slack): tighten inbound file download guards

Address review on #6855: allowlist https files.slack.com before Bearer
fetch, cap aggregate in-memory bytes per file_share message, report
Content-Encoding refusals as download failed, and share one
_web_client_for_connection helper for inbound/outbound.

* fix(slack): satisfy ruff format and AGENTS.md size gate

Reformat test_slack_file_share.py and drop the AGENTS.md file_share
note so channels/AGENTS.md stays under the 49152-byte hard limit
(file was already near the ceiling without it).
2026-10-15 08:28:36 +08:00
sjr666666
2fe325f8ae
fix(lark): retry the Windows install lock instead of failing after ~10s (#6881)
* fix(lark): retry the Windows install lock instead of failing after ~10s

msvcrt.LK_LOCK gives up after roughly ten seconds and surfaces contention
as Permission denied, while a real install (download + setup of the
lark/sandbox CLI) can hold the lock far longer. Retry the non-blocking
acquisition on the Windows fallback branch so concurrent Gateway workers
serialize instead of failing, mirroring the extensions manager's lock
introduced in #6020. The POSIX flock branch is unchanged.

Closes #6879

* test(lark): inject msvcrt stub with raising=False for Linux runners

The regression test patches the module-level ``msvcrt`` attribute, which only
exists on Windows (the POSIX branch imports ``fcntl`` instead), so
``monkeypatch.setattr`` with the default ``raising=True`` raised AttributeError
on Linux CI (backend-unit-tests shard 4).

Pass ``raising=False`` so the stub is injected on POSIX runners: ``fcntl`` is
patched to ``None`` first, and ``_exclusive_install_lock`` then takes the
Windows fallback branch against the fake. Windows behavior is unchanged.

Scope: backend/tests/test_lark_cli_install_lock.py, 1 line.
Verification: pytest tests/test_lark_cli_install_lock.py on Windows
(Python 3.12, pytest 9.0.3) -- 3 passed incl. the real-contention case;
Linux CI rerun is the authoritative check.

* fix(lark): harden and share advisory lock retries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 08:03:10 +08:00
NanPan
a63856f690
fix(checkpoints): confine Redis history purge to literal namespaces (#6709)
* fix(checkpoints): keep Redis history purge within literal namespace

* test(checkpoints): cover glob metacharacters in thread IDs

* test(checkpoints): cover Redis purge with non-UTF-8 client encoding

* fix(checkpoints): honor Redis client encoding in purge fence

* test(checkpoints): simulate Redis client encoders in purge regressions

* fix(checkpoints): restore Redis encoder fence assignment

* test(checkpoints): give Redis fake an encoder
2026-10-15 07:52:29 +08:00
Guo-Yixin
7bee2be22b
fix(acceptance): keep empty Cargo targets from vetoing executed tests (#6891) 2026-10-15 07:48:00 +08:00
yetuge
0bbcccfeb1
fix(serply): report a format error for results with no usable objects (#6796)
`web_search_tool` skipped non-object entries but returned the filtered list
even when it was empty, so a non-empty `results`/`entries`/`articles`
container holding no objects fell through to "No results found". That tells
the agent the query legitimately matched nothing instead of surfacing the
malformed payload -- the failure mode the sibling providers already contain
(brave #6550, groundroute #6603, tavily #6645, sofya #6719): a 200 body
wrapped around an upstream error page ends a research loop as "no hits".

Extract `_response_rows`, which treats a missing or null container as an
empty search and a container that is not a list, or a non-empty list holding
no objects, as the provider's format error, keeping non-object entries
skipped in order so valid rows survive. Both call sites (all three verticals
share one helper) now return the existing structured error.

RED/GREEN: the 7 new cases fail on main with "No results found" and pass
with the fix; test_serply_tools.py 34 passed, the five provider suites
(serply/serper/tavily/sofya/tencent_wsa) 284 passed, ruff check and format
clean.
2026-10-14 23:05:54 +08:00
Li-john1021
32d7dd5fe8
fix(discord): accept inbound attachments and file-only messages (#6857)
* fix(discord): accept inbound attachments and file-only messages

Empty content no longer drops messages that have attachments. Capture
CDN descriptors on inbound, download in receive_file via Bot auth, and
hand bytes through INBOUND_FILE_CONTENT_KEY like Slack/Telegram.

Closes #6856

* fix(discord): gate bot auth and share inbound file helpers

Address review on #6857: attach Bot Authorization only for Discord CDN
hosts, report Content-Encoding refusals as download failed, and hoist
filename/size helpers into app.channels.inbound_files (Telegram delegates
to the same module).

* fix(discord): sort telegram imports for ruff I001

CI lint-backend failed on unsorted imports in telegram.py after the
shared inbound_files helper was introduced.

* style: ruff-format discord inbound attachments test

* fix(discord): show the enforced attachment download limit

---------

Co-authored-by: Li-john1021 <Li-john1021@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 23:01:02 +08:00
RongJie G
44a896cc12
fix(mcp): preserve active calls during session eviction (#6900)
Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
2026-10-14 22:55:43 +08:00
Xuehao Xu
76102fab0b
feat(extensions): add opt-in script workflow MVP (#6858)
* feat(extensions): add opt-in script workflow MVP

* fix(workflows): clarify timeout handling and stabilize request identity

* fix(ci): register workflow catalog and trim inherited guidance

* fix(workflows): retain launch ownership and resolve injected input anchors
2026-10-14 22:42:17 +08:00
CSXizhang
160fbf40c0
fix(agents): capture standard URL content blocks as artifact handles (#6893) 2026-10-14 22:39:19 +08:00
LingYi
96f930f5be
fix(frontend): align one-time DST conversion with the scheduler (#6756)
Co-authored-by: LingYi-Liang <270316896+LingYi-Liang@users.noreply.github.com>
2026-10-14 22:17:51 +08:00
Nan Gao
33b8e84f11
fix(channels): keep IM runs alive when the creating request disconnects (#6876)
* fix(channels): keep IM runs alive when the creating request disconnects

The channel manager holds a `runs.wait` / `runs.stream` request open for
the whole run, and that request dies with the Gateway instance that
received the IM message: a leader hand-off, a rolling restart, a crash.
The Gateway cancels a run whose creating request disconnects unless the
request asked for `on_disconnect: continue`, so every IM run in flight
was cancelled on failover.

Channel runs now ask for `continue`, like scheduled-task and MCP-task
runs already do. The run finishes on the Gateway executing it and its
thread state is persisted, so the answer stays readable in the web UI
even though the IM reply is lost with the old transport. The key is set
after the fire-and-forget branch because the SDK's `runs.create` has no
`on_disconnect` parameter.

Docs: CONFIGURATION.md and the Helm/deployment guides no longer list
"failover cancels IM runs" as a known limit; IM_CHANNEL_CONNECTIONS.md
and the channels AGENTS.md describe the policy.

* docs(changelog): link #6876

* fix(channels): keep the inbound dedupe key once a run may be executing

With `on_disconnect: continue` a run outlives the manager worker that
requested it, but the worker loop released the inbound dedupe key on
cancellation, so a provider redelivery on the replacement instance
admitted the same event again and executed the turn twice (review on
#6876).

Each inbound message now carries an admission record: `requested`
flips before the run request leaves the manager, `confirmed` once the
Gateway proved it admitted the run (a run id, a stream frame, a
completed response). The generic release paths (worker cancellation,
worker/handler exceptions, a broken stream) keep the key when the run
may continue: after confirmation, on cancellation after the request
was sent, and on post-request transport failures (read timeout, read
error, protocol error). Pre-request failures, Gateway rejections
(busy thread, stale pin, status errors, connection failures) and the
stream's terminal `error` frame still release it so the redelivery
can retry.
2026-10-14 22:07:07 +08:00
Hyeonsang Cho
268eb55f6c
fix(persistence): compare safe_add_column drift as the database spells it (#6865)
* fix(persistence): compare safe_add_column drift as the database spells it

safe_add_column's drift check compared the model's generic type and
default spellings (String -> STRING, BigInteger -> BIGINTEGER,
server_default="0" -> "DefaultClause('0', ...)", sa.false()) with the
reflected dialect spellings (VARCHAR, BIGINT, Postgres TIMESTAMP, '0').
Only JSON/JSONB ever matched, so every column a revision found already
present was reported as drift:

- any database created before 0023 logged a runs.change_seq warning on
  upgrade, because 0025 re-runs 0023's safe_add_column;
- a schema whose head stamp was lost (the empty branch commits create_all
  before stamping) logged 29 warnings on SQLite and 36 on Postgres, burying
  the real TEXT-vs-JSON drift the check exists to surface.

Render both sides through the migration connection's dialect before
comparing: types via TypeEngine.compile (parameters stripped, the
Postgres time-zone suffix kept), defaults by unwrapping DefaultClause,
compiling SQL expressions and stripping one layer of string quoting.
Types the dialect cannot render (SQLite NullType) stay "missing info".

* docs(changelog): reference #6865 for the drift comparator fix

* fix(persistence): keep an empty-string default distinct from no default

Stripping one layer of quotes turned a reflected DEFAULT '' into "", and
`return text or None` then collapsed it to None. A desired
server_default="" (passed as a bare "") also normalized to None, so
_check_column_drift stopped warning when a column had no default but the
model wanted '' (or the reverse).

Render a plain-string DefaultClause argument as the quoted literal the DDL
emits, and return the unquoted value even when it is empty. None now means
only "no default".
2026-10-14 17:56:25 +08:00
Totoro
517a8309a2
feat(extensions): allow ModelTool plugins to return structured artifacts (#6861)
* test(plugins): capture opt-in artifact RED (18 failed, 13 passed)

* feat(plugins): add typed opt-in artifacts (31 ToolNode tests pass)

* test(extensions): require API 0.2.6 (version RED verified)

* build(extensions): pin API 0.2.6 (installed wheel guards GREEN)

* test(examples): verify plugin artifacts through checkpoint restart

* test(extensions): reproduce artifact key coercion (8 RED cases)

* fix(extensions): reject non-string artifact keys (185 tests GREEN)

* fix(frontend): list bundled retrieval example in extension catalog

* test(mcp): reconcile recovered notification at stored due time

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-14 17:24:32 +08:00
Guo-Yixin
8f0ca58653
fix(subagents): require test evidence beyond build success (#6868)
* fix(subagents): require test evidence beyond build success

* test(subagents): isolate test evidence from sandbox file readers
2026-10-14 17:15:59 +08:00
万家良
4e6c18ad9c
fix(memory): retry the scope lock open to absorb a Windows create race (#6870)
On Windows, two threads/processes racing to create a not-yet-existing
lock file with an append open can transiently fail on open() itself with
PermissionError (EACCES), before the byte-range lock is ever attempted.
The open was outside the retry loop, so the error surfaced from
load()/save() instead of being retried. Move the open inside the loop and
close the handle on any OSError before retrying.
2026-10-14 16:10:56 +08:00
FanouZeng-TT
064bb50602
fix(buzz): page a saturated history instead of skipping the oldest (#6792)
* fix(buzz): page a saturated history instead of skipping the oldest

buzz-relay serves at most RELAY_HISTORY_LIMIT (2000) stored events per
subscription, NEWEST-FIRST, even when the filter carries a `since`. A channel
that banked more than the cap across one disconnect therefore had its OLDEST
unread messages neither sent nor marked: the live page delivered the newest
2000, the watermark advanced past them, and the rest were lost -- the one
remaining path in this connector that could skip a message outright.

A chat/backlog page's EOSE now drives a descending `until` walk. When a page
comes back AT the cap it may have older matches held back, so the connector
re-asks with `until` set to that page's oldest `created_at` -- on a SEPARATE
`buzz-backlog-<uuid>` subscription id, never the live one -- and repeats until
a page comes back under the cap or reaches the `since` the live REQ was sent
with, bounded by MAX_BACKLOG_PAGES (50, warned when hit). Delivery is charged
to the subscription that carried it before any gate, so a page's saturation is
measured against what the relay sent, not what the allowlist/mention gates kept.

The walk is strictly additive: the live subscription, its filter, and the
watermark are never touched; a CLOSED for a backlog page only ends that walk;
re-opening the live subscription closes any superseded page rather than
orphaning it; and every failure simply leaves the pre-existing behaviour for the
next reconnect to retry.

* fix(buzz): skip the backlog walk when the channel has no read floor

Reviewer willem-bd noted the descending-until walk has no semantic stop when
`floor is None`: that is exactly a channel's first connect (no stored
watermark) and again after a MAX_CACHED_CHANNELS eviction of its cursor. On
those connects the walk would fetch up to MAX_BACKLOG_PAGES * RELAY_HISTORY_LIMIT
events per channel instead of the newest 2000 the connector used to take.

Require a floor before opening any backlog page. The backlog-loss case this walk
exists for always has a watermark (the cursor survives reconnects and restarts),
so it always has a floor; first connect and an evicted cursor keep the previous
behaviour. Covers the skip with a new test, retargets the page-bound test to a
real walk above its floor, and documents the floor requirement.

* fix(buzz): stop orphaned and stalled backlog walks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 15:59:46 +08:00
ajayr
28a06aa777
fix(deploy): exec the gateway server so SIGTERM reaches uvicorn (#6836)
* fix(deploy): exec the gateway server so SIGTERM reaches uvicorn

The production compose command, both CMDs in backend/Dockerfile and the
Helm chart start the Gateway as `sh -c "cd backend && ... uv run ..."`.
dash forks uv and stays PID 1 without a SIGTERM handler, so the kernel
drops the stop signal: uvicorn never shuts down, the lifespan teardown
(memory flush, run drain, service stops) never runs, and every stop waits
out the grace period and ends in SIGKILL (exit 137).

exec the server in every production launch path, as
docker/dev-entrypoint.sh already does, and give the compose gateway
`init: true` so orphaned grandchildren are still reaped once sh is no
longer PID 1. A regression test pins exec in each launch path.

Fixes #6835

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

* docs(changelog): link the gateway SIGTERM entry to #6836

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

* test(gateway): decouple dependency sync from exec launch form

* docs: compact startup guidance to pass inherited size limits

* docs: trim inherited startup guidance after main merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 15:59:08 +08:00
hataa
e377d1b5bb
fix(scripts): feed config-upgrade inline python on stdin, not argv (#6845)
* fix(scripts): feed config-upgrade inline python on stdin, not argv

On Windows setups where python/uv resolve through shim layers that
reparse arguments with cmd.exe (e.g. pyenv-win), the multi-line
`python -c "..."` payloads in config-upgrade.sh get mangled mid-flight
(observed as `IndentationError: unexpected indent` on a stray
`|| goto :error` line) and `make dev` fails before the Gateway starts.

Feed both inline scripts on stdin instead, which is safe across every
spawn chain, and load the checkout .env explicitly inside the resolver
probe: the default load_dotenv() discovery walks up from cwd/__file__,
which a stdin-fed script (`python -`) cannot satisfy. Pin both with a
static regression test.

* test(config): isolate upgrade fixtures from inherited dotenv selectors

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 15:04:52 +08:00