2260 Commits

Author SHA1 Message Date
哈基米
a77a454ca7
fix(sandbox): resolve every entry of colon-joined path lists in forward path translation (#7002)
* fix(sandbox): resolve every entry of colon-joined path lists in forward path translation

* fix(sandbox): keep the original text of colon-list entries when reassembling
2026-10-18 07:46:57 +08:00
Fish
9b3397a0ea
fix(scripts): honor project root when detecting optional extras (#6934)
* fix(scripts): honor project root when detecting optional extras

* fix(scripts): admit project-root config during startup

---------

Co-authored-by: wxhking <186391338+wxhking@users.noreply.github.com>
2026-10-18 07:45:41 +08:00
xiaodu55
7e22e42365
test(backend): skip the POSIX-only output-capture boundary test on Windows (#7013) 2026-10-17 20:53:43 +08:00
EastonJiang
708f16766a
fix(uploads): offload document conversion for every file size (#6924)
* fix(uploads): offload document conversion for every file size

* test(uploads): clarify cancellation regression failures
2026-10-17 19:58:50 +08:00
jay
721caca023
fix(sandbox): keep a concurrent quarantine fence when retire() races it (#6961)
* fix(sandbox): keep a concurrent quarantine fence when retire() races it

`SandboxQuarantine.retire()` unlinks every generation record and then calls
`directory.rmdir()` unguarded. A peer fencing the same logical sandbox between
the `iterdir()` and the `rmdir()` — a concurrent teardown of a replacement,
which this caller's cross-worker lease does not exclude because it fences only
its own logical ID — leaves a fresh record in the directory, so `rmdir()`
raises ENOTEMPTY. That OSError escapes `_retire_absent_quarantine`'s teardown
path and aborts the release flow on a fence a peer just wrote.

Its sibling `retire_replaced_generations()` already guards the identical
`rmdir` with `try/except OSError` for exactly this reason. Apply the same guard
here: the fresh record proves the directory is live, so it is kept and teardown
does not abort.

Regression test replays the race deterministically by wrapping `Path.rmdir`;
red on main (OSError: directory not empty), green with the guard.

* docs(sandbox): scope the retire() fence guard to what it actually holds

Review feedback from @willem-bd on #6961: two non-blocking docstring accuracy
notes, both about the paragraph added by e62e0c3c. Docstring-only, no
behaviour change.

1. The mechanism was described as if a peer teardown of a replacement were a
   different key. It is not. Per `_claim_ownership` / `_held_teardown_lease`
   the teardown lease is keyed by the logical ID and a replacement shares that
   ID, so while the `del:` marker is held a peer's claim — and an acquire-side
   `take()` for the replacement — *are* excluded. What is not guaranteed is
   that the exclusion is still held at `rmdir` time: it is a TTL lease, not a
   lock, so the heartbeat can lose it mid-teardown (the documented "Lost the
   teardown exclusion" path stops refreshing while the stop continues) and the
   config schema permits TTLs below a normal stop. Reworded to that effect,
   which also corrects the same sentence in e62e0c3c's message.

2. The paragraph read as if the listing → `rmdir` window were the only one.
   Added an explicit scope clause: the guard is best-effort rather than full
   fence preservation. A peer record landing before the listing is still
   unlinked along with the rest, and the unlink loop has unguarded sibling
   windows of its own — a concurrent `retire()` can remove the directory
   between the existence check and the listing (`FileNotFoundError`), and on
   Windows a peer's `mark()` still holding the fresh record open for its
   `fsync` raises `PermissionError`.

Validation: `ruff check` and `ruff format --check` pass on the file and across
`backend/`; the new regression test plus the existing quarantine suite are 9
passed; `scripts/check_agent_guidance.py` reports 46 AGENTS.md / 0 errors.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-17 19:36:37 +08:00
EastonJiang
5b2063d8b9
fix(tui): cancel pending persistence work when the bridge times out (#6927)
Co-authored-by: Jaxon1216 <224378083+Jaxon1216@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-17 19:25:18 +08:00
Xuehao Xu
169002a4c9
feat(workflows): add Phase 2 approvals, immutable artifacts and configuration evidence (#6920)
* feat(workflows): add versioned approvals and run configuration evidence

* docs: trim repeated setup guidance to respect inherited size limit

* docs: leave room in inherited agent guidance

* test(channels): align Buzz capability expectation with upstream registry

* fix(workflows): fence file consumers and preserve review access

* fix(runtime): reuse assembly evidence key at run admission
2026-10-17 09:41:15 +08:00
yetuge
4f35eb586a
fix(infoquest): contain malformed search payloads instead of crashing on them (#6932)
* fix(infoquest): contain malformed search payloads instead of crashing on them

`clean_results` indexed the envelope directly:

    for content_list in raw_results:
        content = content_list["content"]
        results = content["results"]
        if results.get("organic"):
            for result in results["organic"]:

so a payload that did not match the expected shape either raised a raw Python
error that the tool then reported to the agent as the search result, or was
silently swallowed:

    {"search_result": {"results": "maintenance"}}      -> {"error": "... error='string indices must be integers'"}
    {"search_result": {"results": [{"content": "x"}]}} -> {"error": "... error='string indices must be integers'"}
    {"search_result": {"results": [1, 2]}}             -> {"error": "... error='int' object is not subscriptable"}
    {"search_result": {}}                              -> {"error": "... error='results'"}

and, worst of them, a malformed container was skipped silently, so the call
answered `[]` -- telling the agent the query legitimately matched nothing:

    {"results": [{"content": {"results": {"organic": "x"}}}]}          -> []
    {"results": [{"content": {"results": {"organic": {"a": 1}}}}]}     -> []

This is the containment rule the sibling bundled providers already follow
(brave #6550, groundroute #6603, tavily #6645, sofya #6719, serper #6779,
serply #6796, tencent-wsa #6799): a payload that is not a result page is a
format error, not an empty search.

`_envelope_entries` now validates the `content.results` envelope and
`_rows` validates one vertical's container, both shared by the web and image
verticals. A missing or null container stays an empty search; a container that
is not a list, or a non-empty one holding no object entries, returns the
provider's format error; malformed entries in an otherwise valid payload are
skipped in order so the usable rows survive. Transport failures keep their
existing error text.

Diagnostics log the container, the envelope position, or the unexpected type --
never queries or payload values.

* test(infoquest): cover the envelope guards and log skipped rows by position

Three review follow-ups on the malformed-payload containment: drop the
accidentally stacked @staticmethod on _envelope_entries, give _rows the same
indexed skip warning _envelope_entries already emits (with the container name
so a provider-side drift leaves a trace), and pin the four isinstance guards --
a non-object top-level payload and a non-object search_result across both
verticals -- which no existing case reached.
2026-10-17 09:00:50 +08:00
asts
64dab5897a
fix(mcp): preserve remote URLs during local file rewriting (#6878)
* fix(mcp): preserve remote URLs during local file rewriting

* fix(mcp): honor escaped delimiters in remote URLs
2026-10-17 08:13:35 +08:00
Hyeonsang Cho
6a6aeb4bf2
fix(sandbox): reject shell-variable path prefixes in the host-bash guard (#6930)
* fix(sandbox): reject shell-variable path prefixes in the host-bash guard

The local path guard scans commands for absolute paths with a regex whose
lookbehind skips a `/` that follows a word character. `~/`, `${HOME}/` and
`"$HOME"/` were rejected, but the bare `$HOME/...` form (quoted or not)
was never matched, so `cat $HOME/.ssh/id_rsa` ran on the host when
`sandbox.allow_host_bash` was enabled.

Scan `$NAME/...` separately and reject it, since the variable's value is
unknown at validation time. Variables inside URLs and non-ASCII prose keep
the existing exemptions. `$PWD/...` stays allowed because it names the same
file as the equivalent relative path, which the guard already accepts,
unless the command rebinds `PWD` itself.

* docs(changelog): reference #6930 for the host-bash variable path fix

* fix(sandbox): cover positional parameters and CDPATH in the host-bash guard

Review follow-up. `cat $1/etc/passwd` slipped both scans: the new variable
scan only matched names, and the absolute-path regex skips a `/` after a
digit. `bash -c` gets no positionals, so `$1` expands to nothing and the
shell opens `/etc/passwd`. Match `$0`-`$9` as well, but keep awk-style
arithmetic such as `$1/1024` allowed, since it was accepted before and
standard host roots do not start with a digit.

`CDPATH=$HOME cd docs` moved a relative `cd` outside the workspace, taking
both `$PWD/x` and a plain relative `x` with it. Reject any command that
mentions CDPATH in the working-directory check, so the `$PWD` exemption's
equivalence with relative paths holds.

* fix(sandbox): cover named homes and non-ASCII variable paths in the host-bash guard

- Reject `~name/...`, which expands to that user's home and slips the
  absolute-path scan the same way `$HOME/...` did.
- Stop applying the non-ASCII prose exemption to the variable scan:
  `$HOME/文档/...` is a host path.
- Allow awk field division `$1/$2`, which main accepts, and capture the
  remainder in a lookahead so skipping `$1/` does not hide a later
  `$2/etc/passwd`.
2026-10-16 17:18:17 +08:00
Guo-Yixin
34ccd79351
fix(acceptance): recognize native Node test summaries (#6953)
* fix(acceptance): recognize native Node test summaries

* test(acceptance): isolate native Node suite labels

* fix(acceptance): recognize native Node test summaries

* fix(acceptance): filter Node warning points

* test(acceptance): cover Node warning reporter points

* test(acceptance): cover Node warning labels with native reporters

* fix(acceptance): keep failure counts on one diagnostic line
2026-10-16 17:09:02 +08:00
ranjianghong526-hash
2adc667fec
fix(streaming): report gaps when Redis retention disappears (#6951)
Co-authored-by: ranjianghong526-hash <228174768+ranjianghong526-hash@users.noreply.github.com>
2026-10-16 17:03:25 +08:00
ShitK
8558afd062
fix(docker): handle empty provisioner URLs in both launchers (#6945) 2026-10-16 17:01:51 +08:00
haotianqi666
9c74b1529b
fix(client): accumulate incremental streaming token usage (#6939)
* fix(client): accumulate incremental streaming token usage

* test(client): cover OpenAI and vLLM stream usage

* fix(client): clamp completed usage reconciliation deltas

---------

Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-16 10:38:10 +08:00
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
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
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
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