2584 Commits

Author SHA1 Message Date
Hyeonsang Cho
9b0c7cbc37
fix(browser): never replace an existing output file with browser_screenshot (#6913)
* fix(browser): never replace an existing output file with browser_screenshot

browser_screenshot published straight onto outputs/<name> through the
replacing atomic publisher, so a screenshot saved as report.png silently
replaced the user's own report.png, an earlier capture, or another
default-named capture taken in the same second (the default stem has
second resolution). The sibling web_capture tool already keeps existing
files and takes a -1, -2, ... suffix.

Stage the complete capture under a hidden sibling and hard-link it onto
the first free candidate name. The link is the whole no-overwrite guard:
it fails with FileExistsError when the name exists as anything, so no
name is checked before it is claimed and a concurrent writer cannot be
replaced; the final name never shows a partial file. Staging shares the
outputs directory, so the link is same-filesystem, and like Gateway
uploads other link failures propagate instead of falling back to
replacement. New files keep the publisher's normal creation mode.

Hidden per-action progress frames keep the replacing _write_screenshot:
their names carry microsecond timestamps in a server-owned directory.

* docs(changelog): reference #6913 for the browser_screenshot no-overwrite fix

* fix(browser): refuse a staged screenshot swapped before it is linked

The sandbox can write outputs/, so it can see the hidden staged capture
and replace it with a symlink before the hard link. os.link follows
symlinks by default, which would publish a hard link to the host file as
the screenshot artifact (the replacing publisher this PR moved away from
never followed one).

Stage the capture with an exclusively created descriptor and keep its
identity from fstat, link without following symlinks where the platform
supports it (decided once at import), and require the published name to
be that inode; anything else is removed and the capture fails, like
Gateway uploads' identity check. A failed staging write still leaves no
files behind.

* docs(readme): note that browser_screenshot never replaces an output file

* fix(browser): keep the staged screenshot open until its link is verified

Linux filesystems reuse a freed inode number at once. Closing the staged
capture before linking let a file swapped in at the staged name take the
same number and pass the samestat check, so the symlink and planted-file
swap tests failed on Linux CI while passing on macOS. Hold the staged
descriptor through the link and identity check, as Gateway uploads do,
and require the published name to be a regular file.

* test(browser): race the peer claim against the real publication step

The claimed-name test patched _publish_bytes_atomically, which the
screenshot path no longer calls, so its hook never fired and the race
assertions passed vacuously. Plant the peer file right before os.link or
os.replace lands bytes on a visible name, and assert that the claim
happened, the capture moved to report-1.png, and the peer file survived.

* refactor(capture): share one output-name candidate sequence

browser_screenshot hand-copied web_capture's probe bound and suffix
order, and the copies had already drifted: Browserless retried a held
timestamped name five times, the screenshot tool once. Move the bound,
the retry count and the candidate order into utils/output_names.py and
have both tools iterate it, so the naming cannot drift again. Browserless
behavior is unchanged; its tests now patch the shared module.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 10:52:22 +08:00
DDT
453563af5b
fix(tencent_wsa): fall back when digit-class max_results cannot parse (#6973)
* fix(tencent_wsa): fall back when digit-class max_results cannot parse

str.isdigit() accepts values like "²" and "1²" that int() rejects, so a
configured max_results could crash web_search_tool before the HTTP call.
Parse strings with int() inside try/except (matching sibling providers)
while still rejecting bools and non-integral floats.

Fixes #6921

* docs(tencent_wsa): clarify that all float max_results fall back to default
2026-10-18 10:51:13 +08:00
yinx65588-boop
75de35628d
fix(scheduler): fence expired launch recovery against fresh claims (#6975)
Co-authored-by: yinx65588-boop <yinx65588-boop@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 10:46:32 +08:00
ranjianghong526-hash
606db804d5
fix(sandbox): surface local grep file read failures (#7004)
* fix(sandbox): surface local grep file read failures

* fix(sandbox): skip non-regular local grep candidates

* fix(sandbox): preserve local grep failure context and scan errors

* fix(sandbox): reverse resolve local grep error filenames

---------

Co-authored-by: ranjianghong526-hash <228174768+ranjianghong526-hash@users.noreply.github.com>
2026-10-18 10:46:13 +08:00
NanPan
f96aca5059
fix(checkpoints): skip unserializable Redis history cache entries (#7014)
* fix(checkpoints): skip Redis cache entries that cannot be serialized

* style(checkpoints): format Redis serialization regression

* fix(test): assert Redis serialization payload bytes

* test(checkpoints): format Redis cache serialization regression

* chore: sync frontend agent guidance with upstream size-compliant main

* fix(checkpoints): track Redis cache serialization failures

* test(checkpoints): consolidate Redis serialization fallback regression

* style(tests): format Redis cache regression signature

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 10:30:14 +08:00
Xuehao Xu
c056ee556a
feat(knowledge): add WeKnora read-only extension (phase 3) (#6950)
* feat(knowledge): add WeKnora read-only extension (phase 3)

* fix(weknora): complete extension catalog and harden response validation

* test(extensions): cover WeKnora discovery in browser catalog

* docs(weknora): explain extension and MCP use cases

* docs(weknora): correct guide language and sync Chinese overview

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 10:28:30 +08:00
DEVJHAWAR11
158c54508f
test(backend): fix three more Windows-host test failures (#7008)
- test_local_dev_loopback_bind: the stale-render test compared Git Bash's
  MSYS path (/tmp/...) against a native path; route it through
  _normalize_rendered_path like its sibling tests.
- test_project_documents_repo: TestShelfSnapshot relied on back-to-back
  inserts getting increasing updated_at, which ties on Windows' ~15.6 ms
  clock and falls through to the id tie-break. Pin updated_at explicitly.
- test_multi_worker_run_ownership: the transient-renewal test compared two
  immediate lease timestamps with a strict >; start from a deadline inside
  the lease window, as test_scheduled_goal_handoff already does.
2026-10-18 10:25:26 +08:00
Totoro
eae908c00d
fix(sandbox): make failed AIO teardown safely retryable (#7001)
* fix(sandbox): make failed AIO teardown safely retryable

* docs(sandbox): add sanitized teardown retry verification

* fix(sandbox): clear stale teardown fence on warm reclaim

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 09:57:12 +08:00
ZHOU YANG
6e0ab091e7
fix(infoquest): screen result URLs through the shared public-URL owner (#7031)
* fix(infoquest): screen result URLs through the shared public-URL owner

Three providers already delegate result-URL screening to
community/url_safety.safe_public_result_url; infoquest registers the
image_search tool too but imported the owner zero times, so organic,
news and image rows all handed raw provider URLs to the model. A row the
owner rejects is now dropped rather than raised, keeping the containment
rule #6932 established for this client.

tests: test_infoquest_result_url_safety.py (new, red on main),
test_image_result_url_safety.py (infoquest added to the pinned modules).

* fix(infoquest): reject unscreened search response fallbacks

---------

Co-authored-by: ZHOU YANG <236804849+Zhouyang66639@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 09:55:27 +08:00
Guo-Yixin
1c433c0273
fix(streaming): retire memory buffers without orphaning subscribers (#6992)
* fix(streaming): retire memory buffers and wake existing subscribers

* fix(streaming): preserve end for fully consumed retired streams

* docs: keep frontend guidance within its hard budget

* fix(streaming): report gaps when retirement wins initial attachment
2026-10-18 09:51:31 +08:00
RongJie G
8941fbe872
fix(runtime): bound RunJournal progress teardown (#6990)
Cancel event and progress writes before force-close waits, bound progress settlement, and supervise cancellation-resistant reporters until their late result is observed.

Fixes #6986

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
2026-10-18 09:39:08 +08:00
DennyHo0917
513c03e8c0
feat: add API Route setup wizard preset (#6579)
* feat: add API Route setup wizard preset

Signed-off-by: dennyho0917 <dennyho0917@gmail.com>

* fix: address API Route wizard review feedback

Signed-off-by: dennyho0917 <dennyho0917@gmail.com>

---------

Signed-off-by: dennyho0917 <dennyho0917@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 09:26:45 +08:00
Louisss
829edac602
fix(tests): pass Windows base system env to the isolated live-policy pytest child (#6439)
* fix(tests): pass Windows base system env to isolated pytest child

The live-test policy regression tests spawn a pytest child with a minimal,
from-scratch env. On Windows that env lacks SystemRoot/TEMP/USERPROFILE, so
the child dies during import asyncio (WinError 10106, winsock provider init)
and tempfile cannot resolve a directory, making the tests fail for
environment reasons instead of testing the opt-in policy.

Extract the env assembly into _child_pytest_env() and pass through a small
allowlist of Windows base system variables (SystemRoot, windir, COMSPEC,
PATHEXT, TEMP, TMP, USERPROFILE) when present. These carry no credentials,
so the isolation contract (no API keys, no real .env) is unchanged, and
POSIX behavior is byte-identical. A new unit test pins the contract: only
the allowlisted base vars may pass through, and SystemRoot must be present
on Windows.

* test(live-policy): seed Windows base env vars in the bootability contract

The contract test asserted TEMP/TMP pass-through without seeding them, so
on Windows hosts that do not export those optional variables (CPython then
falls back to %USERPROFILE%\AppData\Local\Temp) it failed on host
configuration rather than on the helper's contract.

Seed the representative base variables with monkeypatch and assert the
forwarded set exactly, so the isolation contract is deterministic on every
host. Red/green evidence: with TEMP/TMP stripped from the environment the
old assertion fails (extra = {COMSPEC, PATHEXT, SystemRoot, USERPROFILE,
windir}) while the seeded assertion passes.

Addresses the P3 finding in the review of PR #6439.

* ci(tests): cover Windows live-test policy

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 09:24:51 +08:00
NanPan
e6a3c9ffb7
fix(checkpoints): serialize shared memory history cache operations (#6708)
* fix(checkpoints): serialize shared memory history cache operations

* fix(checkpoints): align memory cache contract with locking

* test(checkpoints): make cache race regression deterministic

* docs: remove overlapping cache note from memory-cache PR
2026-10-18 08:13:27 +08:00
Totoro
16d36ccd21
feat(sandbox): add bounded AIO prewarming and lifecycle benchmarks (#6998)
* fix(benchmark): validate sandbox lifecycle and concurrent measurements

* feat(sandbox): prewarm likely AIO tool turns

(cherry picked from commit 3a1017ed032ed9331987bc3dc61f23dcdac4e905)

* fix(sandbox): bind bounded prewarming to execution leases

* docs: trim frontend guidance below its existing size limit

* fix(benchmark): separate failed lifecycle stage timings

* test(sandbox): measure prewarm overlap and unused resources through agent graphs

* fix(sandbox): finish lease and sampler cleanup after cancellation or errors

* docs(benchmark): distinguish initialization wait from model overlap

* docs(helm): align the configuration example with version 61

* fix(sandbox): preserve synchronous agent graphs when prewarming is enabled

* test(cargo): isolate native fixtures from inherited make jobserver flags

* fix(sandbox): require managed cleanup before prewarming

* fix(sandbox): log denied prewarming as an expected skip

* fix(benchmark): filter provider summary input records

---------

Co-authored-by: nothingyuancando <1579035512@qq.com>
2026-10-18 08:10:56 +08:00
Totoro
002bcc521a
fix(skills): let skill_manage lock waiters wake across event loops (#7020)
* fix(skills): let skill_manage lock waiters wake across event loops

The sync tool wrapper runs every call under its own asyncio.run, and
ToolNode runs same-turn calls on separate threads. The per-(user, skill)
asyncio.Lock bound itself to the first contending loop, so a release from
another thread never woke that waiter, and a third caller failed with
"bound to a different event loop".

Reuse the read-before-write gate lock: each waiter parks on its own
loop without holding an executor thread, and release wakes every waiter
to re-contend, so a cancelled waiter cannot strand the skill. Keep the
gate's waiters in arrival order, so same-turn calls on one skill still
take the lock in the order they reached it, as with asyncio.Lock. Guard
the registry lookup, which the sync path runs on several threads.

Fixes #5846

* fix(skills): hand the skill lock to the oldest waiter

Reusing the read-before-write gate let a later same-skill call overtake
one already waiting. Release unlocked the gate and woke every waiter, so
a call whose storage lookup finished in that same loop step took the
free lock first. asyncio.Lock on main never lets a new caller pass a
queued one.

Give skill_manage its own small lock. Release hands the lock straight to
the oldest waiter and keeps it locked, so a later caller queues behind
it. Each waiter still parks on its own future and wakes on its own loop,
so waiting holds no executor thread. Claiming the future settles a race
with its cancellation, and a waiter cancelled after the handoff passes
the lock on. Releasing an unlocked skill lock raises, as with
asyncio.Lock. read_before_write_middleware.py is back to main.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-18 08:03:13 +08:00
ranjianghong526-hash
b731d979f1
fix(streaming): validate numeric reconnect cursors before subscribing (#6690)
* fix(streaming): validate numeric reconnect cursors before subscribing

* fix(streaming): address reconnect cursor review feedback

---------

Co-authored-by: ranjianghong526-hash <228174768+ranjianghong526-hash@users.noreply.github.com>
2026-10-18 07:51:24 +08:00
ordinary being
f34e1a8105
fix(mcp): consume resolved notification snapshots instead of launching empty events (#6693)
* fix(mcp): consume resolved notification snapshots instead of launching empty events

A tracking-degraded snapshot recorded by a failed poll can resolve before any
notification worker claims it: the next poll succeeds, or an answered
input_required prompt returns the task to working. _record_event_if_changed
left that pending snapshot in place when the event disappeared, so
claim_notification_work rebuilt it as None and still claimed it, and the
service launched an empty notification run (dispatch_event or {}).

Consume such snapshots at claim time instead: advance notified_version, return
the status to none, clear the claim, and skip the row. The same guard consumes
rows left without a dispatch event by pre-fix claims, which would otherwise
relaunch empty notifications on every retry until dead-lettering.

* fix(mcp): clear consumed snapshot state for the review follow-ups

Review feedback on the claim-time consume guard:
- clear event_fingerprint when consuming, so an identical later attention
  event is recorded again instead of being suppressed by a snapshot that was
  never delivered;
- clear stale notification_error/attempt/dispatch state so a status
  projection cannot show a notification that no longer exists;
- cover the reclaimed expired-claim shape (claim, lease expiry, resolve,
  reclaim) and the identical-event sequence with regressions.

* docs(persistence): document the notification consume rule

mcp/AGENTS.md's effective chain has only 85 bytes of headroom on main (98,219 / 98,304), so the rule moves to persistence/AGENTS.md, which owns the mcp_tasks storage contract. No behavior change.

* fix(mcp): fence the notification consume against newer events (review)

Two P2 findings on the claim-time consume:
- the consume acknowledged the latest event version from a stale read, so on
  SQLite a completion recorded after the claim's SELECT was overwritten with
  status none and lost forever. The consume is now a conditional UPDATE fenced
  on the event version and phase that were read; a newer event makes it a
  no-op and stays claimable;
- a legacy launching/dispatched row whose dispatch payload is empty but whose
  event version moved on consumed that newer event. Such rows are now rebuilt
  and claimed; only a generation with nothing newer is consumed.
Regressions: a completion recorded while a claim is paused after its SELECT,
and legacy launching/dispatched rows with and without a newer completion.
2026-10-18 07:49:46 +08:00
Hyeonsang Cho
3c08a9f93e
fix(sandbox): let the local ls tool list the /mnt/user-data root (#6991)
* fix(sandbox): let local file tools read the /mnt/user-data root

The local provider maps the aggregate /mnt/user-data root so listing it
matches AIO, but the tool layer only accepted paths below it: ls, glob
and grep on the root answered "Permission denied" (with a trailing
slash, from the resolved-path "path traversal detected" check).

validate_local_tool_path now accepts the bare root for reads, and the
user-data resolver accepts the shared parent of workspace/uploads/
outputs itself when the caller opts in. Only ls and the shared read
path opt in; writes to the root and files placed directly in it stay
rejected.

* docs(changelog): reference #6991 for the /mnt/user-data root fix

* fix(sandbox): list the /mnt/user-data root through its allowed dirs only

The first version let glob, grep and ls start at the user-data root
after validating only the root itself. The walkers then descended into
everything under the host user-data/ directory, so a file or directory
stored directly under it was listed and searched although read_file
rejects it.

ls now lists the root one allowed directory at a time (workspace,
uploads and outputs, each one level deep) through the sandbox's own
list_dir, so the host root is never walked and list_dir's existing
per-directory containment check still filters symlinks. The resolver
is back to main's behavior, so glob, grep and read_file keep rejecting
the root.
2026-10-18 07:48:06 +08:00
哈基米
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
ChengFa
86e64f2a95
docs(uploads): clarify the document conversion opt-in (#7010) 2026-10-17 20:01:45 +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
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
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