3585 Commits

Author SHA1 Message Date
Totoro
457ac8cabd
docs(memory): correct backend contract details in the MemoryManager guide (#5943)
Follow-up to #5929. A config or custom backend written from a few
statements on the page fails:
- OpenViking's failure_policy.read takes fail_open or raise, and the
  backend supports middleware mode only.
- from_config is abstract, so it belongs in the mandatory set.
- Tool mode also needs get_memory() and fact CRUD, because it skips the
  per-turn add() writes.
- Management methods return the DeerMem shape, and create_fact returns a
  (memory_data, fact_id) tuple.
- staleness_review_enabled is on by default, and the governance flags
  live under memory.backend_config.

The zh page gets the same fixes plus a typo (回记 -> 召回).

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-27 20:46:04 +08:00
Zeren Wang
d700ad9edf
feat(memory): add Jev pre-screening and signal classification over a shared TypeSafe client (#5906)
* feat(memory): add Jev pre-screening and signal classification over a shared TypeSafe client

- refactor(typesafe): extract a shared Jev (TypeSafe) client and unify its
  configuration: one connection-precedence rule, credential fingerprint, wire-size
  counting and error taxonomy, shared by the guardrail gate and the memory hooks.
- feat(memory): wire `memory.prescreen` and `memory.signal_classification` into
  DeerMem through one injected judge hook (both off by default, `enforce` gated by
  the shadow evaluation).
- config: add the top-level `typesafe:` block and the two judging slots to
  `config.example.yaml` and the Helm values, bump `config_version` to 50, and sync
  the Helm README example with it.
- test(memory): add the shadow-evaluation script (`eval_memory_prescreen.py`) with
  per-stratum gates, censoring and the no-network baseline.

Review follow-ups folded in:
- `mutations_accepted` counts only facts that survive capacity enforcement and the
  upload-event scrub that precedes persistence, only removals/reinforcements of ids
  that existed, and persisted near-duplicate merges (matched by id, or by identity
  for legacy id-less facts); it is published only after persistence succeeds.
- The cached manager hot-reloads its judge when a memory slot or the shared
  `typesafe` block changes (including a credential rotation); publication is
  serialized and revalidates the config generation, so a build started before an
  `enforce` -> `off` rollback cannot land after it.
- Cache provenance is per answer: the `cached` flag and the reported model follow
  the answers a side actually consumed, across partial merges and answerless retries.
- An oversized noul integer stays a per-question error instead of raising.
- Incremental call savings use the shared scored denominator.
- The sensitive-stratum gate requires evidence for each of identity, preference
  and correction.

* fix(typesafe): resolve one credential per layer and refuse unusable connection values

Review follow-ups on the shared TypeSafe client (#5906):

- A consumer's `api_key_env` is no longer overridden by a literal `api_key` in the
  shared `typesafe:` block: the first layer that sets either credential setting
  decides it, and inside one layer the literal key wins (the standalone
  semantics). A consumer whose endpoint does not accept the block's key no longer
  sends it on every call, and the missing-credential message keeps naming the
  variable that layer configured.
- `base_url` is validated where it is configured: `http(s)` only, with no query,
  fragment, embedded credentials, whitespace or control characters, and a
  malformed port fails at construction instead of on every request (httpx raises
  `UnsupportedProtocol` / `InvalidURL` per call, and the query would have been
  swallowed into the appended endpoint path).
- A credential that cannot travel as a header value — surrounding whitespace from
  a secret mounted from a file, a non-printable character — is refused when the
  connection resolves. It used to construct fine and then put the whole
  `Bearer <key>` value into h11's `LocalProtocolError` message, which the
  guardrail middleware's `logger.exception` prints.
- The client raises transport failures without chaining the original exception,
  so that message can no longer carry the credential through the cause chain; the
  exception type stays in the message for diagnosis.
- `TypeSafeConfig` validates in strict mode, agreeing with the consumer `config`
  path: `max_attempts: true` was silently read as `1`. An integer stays a usable
  float, so `timeout: 5` keeps working.

Tests: 20 cases across `tests/test_typesafe_config.py` and
`tests/test_typesafe_client.py`, each red on the previous revision and green now.
367 focused typesafe/memory/config tests, `make test-blocking-io` (167) and the
eval-script suites (50) pass; `ruff check` and `ruff format --check` are clean.
Docs synced: `backend/docs/GUARDRAILS.md`, `typesafe/AGENTS.md`,
`config.example.yaml`.

* fix(typesafe,memory): bound the reply, record request failures, gate enforce on the evidence

Second review round on #5906: the remaining items from the review thread and from
the follow-up pass.

Typesafe client (the reply is treated as hostile input too):

- Send `Accept-Encoding: identity`, refuse a body that carries a non-identity
  `Content-Encoding`, and read through a bounded `iter_bytes` that stops past
  `MAX_RESPONSE_BYTES` (64 KiB). A gzip body that expands to 128 MiB used to be
  decoded in full and, on the gate path, allowed the tool call unexamined; it is
  now `invalid_response` (a denial with `fail_closed: true`).
- Map `RecursionError` from `json.loads` to `invalid_response`: deep nesting
  escaped the module's taxonomy, so the memory consumers lost the round's record
  instead of recording a fallback.

Memory judge:

- A request-level failure is recorded as `request_failed`, never `no_verdict`
  (`REASON_REQUEST_FAILED` was defined and never emitted, so an unreachable
  endpoint was indistinguishable from a provider with no usable answer). The
  adapters' `decide()` now propagates `TypeSafeError` for the coordinator to record
  and log; `no_verdict` stays for a question-level failure, and an ineligible side
  keeps its own reason even when the round's request failed.

Shadow evaluation (the enablement gate reads the evidence, not one number):

- The miss-rate gate requires the exact one-sided Clopper-Pearson upper bound to
  meet the 1% target; a sample that only meets the point estimate is
  `INSUFFICIENT` (0/250 was reported as a pass with a 1.19% bound).
- New `baseline_incremental_savings` gate: a baseline that saves as many calls as
  the model — the shipped all-trivial sample — fails instead of enabling.
- Negative counts (`mutations_accepted`, message / batch / token counts) are
  treated as absent rather than scored: `-3` read as "nothing lost", adding a
  phantom saving to the numerator and the denominator.

Docs: the design documents cited but absent from the repository are replaced with
in-repo pointers (`MEMORY_IMPROVEMENTS.md`, `agents/memory/AGENTS.md`,
`GUARDRAILS.md`), and the gate and limit wording now matches what the code
enforces.

Tests: 31 new or updated cases, each red on the previous revision (compressed body
refused even when it decodes to a valid envelope, body over the cap refused, body
at the cap answered, deep nesting mapped, `request_failed` on each request-level
failure, the bound and baseline gates, the negative counter). 454 focused tests,
`make test-blocking-io` (167) and the eval suites pass; `ruff check` and
`ruff format --check` are clean.

* fix(memory-eval): pair the baseline comparison on the same records

Review follow-up on the incremental-savings gate added in ed930478 (P2): both
sides of the comparison are now the records the no-network baseline can score
(`trivial_only` plus a readable signals list).

Subtracting that subset's heuristic savings from model savings over *all* scorable
skips credited the model with batches the baseline never saw: 300 reviewed skips
with a single annotated record computed 300-1=299 "incremental", passed the gate
and set `enable_recommended`, while the paired comparison was model=1 against
heuristic=1 (incremental 0).

- `Evidence` carries the paired numbers (`baseline_paired_skips`,
  `baseline_model_saved_calls`, `baseline_heuristic_saved_calls`) instead of one
  heuristic count, filled by `build_report` from the baseline summary.
- The gate reports `INSUFFICIENT` when no scored skip carries the annotation and
  when the annotation covers only a subset of the scored skips (the message carries
  the paired counts and the `paired of scored` coverage), `FAIL` on a non-positive
  paired gain, and `PASS` only on a positive gain over the full paired set.
- Docs state the pairing/coverage requirement in
  `backend/docs/MEMORY_IMPROVEMENTS.md` and in the script docstring.

Tests: the reviewer's counterexample as a report-level regression (it failed with
`assert 'PASS' == 'INSUFFICIENT'` on ed930478 while `enable_recommended` was true),
a unit case for the paired-subset `INSUFFICIENT`, and the renamed paired fields.
344 focused tests, `make test-blocking-io` (167) and the eval suites pass;
`ruff check` / `ruff format --check` clean.

* fix(typesafe): bound the sync body read by the deadline

`deadline_seconds` only covered the sync path's headers, the completed body and
the parse, so an endpoint that drips bytes just under the read timeout held the
caller for one read per piece — the whole of `MAX_RESPONSE_BYTES` past the budget.
The read loop now checks the budget before every transport read, which bounds the
overshoot to one read. The pieces are the transport's own reads, not re-chunked
output: `iter_bytes(chunk_size)` pulls many reads inside a single yielded chunk,
so a check at that granularity would never see a drip.

`tests/test_typesafe_client.py` pins it with a valid envelope delivered as 24
pieces of 50 ms against a 0.2 s deadline: red before (24 pieces pulled), green now
(~5), on both the sync and async paths. Docstring and `typesafe/AGENTS.md` track
the new granularity.

* fix(memory): read the YAML off spelling, and veto a skip over the whole batch

Two review findings on the memory judging path.

`memory.prescreen.mode` / `memory.signal_classification.mode` rejected the
`mode: off` spelling `config.example.yaml` ships and a rollback types: YAML 1.1
parses the unquoted value as a boolean, so both fields saw `False`. The failed
reload left the previous judge running, so both fields now normalize `False` to
`off` before the `Literal` runs. `True` stays a validation error (no mode it
could mean). The examples quote the value, and the docs record why.

The pre-screen's L3 veto read `detect_signals`' 6-message window while the batch
it answers for is the whole post-watermark feed, so an explicit correction in an
earlier turn of an eight-message batch did not stop the skip that then advanced
the watermark past it. `detect_signals` gains a `window` parameter (default
unchanged) and the updater scans the full feed for the veto; the extraction hint
and the reinforcement gate keep the window.

Both regressions are red on the previous head: the config test reproduced the
`input_value=False` rejection through `AppConfig.from_file`, and the batch test
reproduced the prescreen judging (and skipping) a batch whose first turn carries
the correction.
2026-09-27 20:28:05 +08:00
Grapette.L
e2feca1e56
fix(models): skip credential files whose access token is not a string (#5864)
* fix(models): skip credential files whose access token is not a string

* fix(models): normalize the file-route access token and guard the refresh token
2026-09-27 15:18:55 +08:00
xiaodu55
c368a9f229
fix(backend): stop forcing WindowsSelectorEventLoopPolicy so asyncio subprocesses work on Windows (#5937)
* fix(backend): stop forcing WindowsSelectorEventLoopPolicy on Windows

backend/sitecustomize.py switched every Python process with backend/ on
sys.path to WindowsSelectorEventLoopPolicy (added by #3236 for 'make dev'
on Windows). On Windows the selector loop cannot spawn asyncio
subprocesses - asyncio.create_subprocess_exec raises a bare
NotImplementedError - so ACP agents, stdio MCP servers, and local sandbox
command execution are all broken on native Windows, and the swallowed
exception surfaces as 'Error invoking ACP agent x: ' with no reason.

The forcing has no beneficiary left on Windows: the async postgres driver
is asyncpg (not psycopg, as #3236 states), and asyncpg 0.31.0 ships no
Windows wheels, so the postgres backend is unavailable on native Windows
regardless of the loop; the default SQLite backend (aiosqlite) runs on
either loop. The make dev startup fix in #3236 came from the serve.sh /
wait-for-port.sh changes, which are untouched.

With the Python-default ProactorEventLoop restored, subprocess features
work again on native Windows.

* fix(benchmark): scope a selector loop to the postgres checkpoint benchmark on Windows

Static review of the sitecustomize removal flagged the one caller that
relied on the old startup policy: bench_channels.py postgres workers run
bare asyncio.run() and enter AsyncPostgresSaver.from_conn_string(), which
the lockfile-pinned psycopg 3.3.3 rejects on Windows under
ProactorEventLoop before connecting.

Instead of restoring the global selector policy, the postgres branch of
the benchmark now scopes WindowsSelectorEventLoopPolicy to itself right
before asyncio.run(); memory/sqlite cases and every other backend process
keep the subprocess-capable Proactor default.
2026-09-27 15:07:39 +08:00
Hyeonsang Cho
5b8b69bd45
fix(gateway): keep /api/user-profile filesystem work off the event loop (#5935)
* fix(gateway): keep /api/user-profile filesystem work off the event loop

GET and PUT /api/user-profile resolved the per-user USER.md path (which
builds absolute paths on every call), stat'ed, read, created the user
bucket and wrote the file inline on the event loop, while every other
handler in the custom-agent router offloads that work with
asyncio.to_thread. The strict Blockbuster gate raises BlockingError on
both; in production a slow disk stalls every other request on the worker.

Offload the whole resolve-stat-read and resolve-mkdir-write sequences,
capturing the effective user id on the loop first like the sibling
handlers. The blocking-IO gate test for this router now covers both
profile routes (missing profile, write, read back).

* docs(changelog): reference #5935 in the user-profile blocking I/O entries
2026-09-27 15:06:49 +08:00
xiaodu55
e7576f9f5b
fix(tests): stop mcp task service tests hanging forever on Windows (#5941)
Running the suite on native Windows blocked forever inside
test_mcp_task_service.py, and the whole shard with it. Root cause chain:

1. The tests patch _CANCELLATION_DRAIN_TIMEOUT_SECONDS to 0.01s. On
   Windows + CPython 3.12, asyncio.wait_for over an already-created task
   with a 10ms timeout raises TimeoutError immediately (measured 30/30 on
   3.12.13, 0/30 on 3.11.9, same host; 50ms works on 3.12) - the claim
   never gets its first scheduling slice inside the drain window.
2. _claim_with_cancellation_release therefore abandons the instant claim
   to the background handoff; when it completes, the handoff calls
   repo.release_poll_claim_after_cancellation - which
   OrdinaryReleaseBatchRepository (the test fake) does not implement,
   so the release dies with AttributeError.
3. The test then waits unbounded on repo.release_started.wait(), which
   can now never fire: the whole suite hangs.

- add the missing release_poll_claim_after_cancellation to the fake so
  it matches the repository contract the service drives
- raise the patched drain 0.01 -> 0.25 and the outer caller wait 0.2 ->
  0.5 so the race window is above Windows timer granularity and the
  handoff-vs-outer-timeout ordering the assertions expect is preserved
- bound release_started.wait() so a contract/timing regression fails in
  seconds instead of hanging the run

The file now completes on Windows in ~12s (previously hung forever);
Linux/CI semantics are unchanged - the claims still complete well inside
the drain there.
2026-09-27 14:45:30 +08:00
Totoro
8190c86280
fix(goal): show the active goal to the agent on every model call (#5917)
* fix(goal): show the active goal to the agent on every model call

Nothing put ThreadState.goal into model requests. The agent only knew the
goal from messages: the objective the Web UI and IM channels send, and the
hidden <goal_continuation> message. Goals set through PUT /goal, the
embedded client or the TUI never reached it, and compaction could remove
the messages that stated the others.

DurableContextMiddleware now renders the active objective first in its
hidden durable-context data message, escaped, bounded, PII-redacted and
treated as data like the summary. Only the objective is rendered, so the
block changes only when the goal is set or cleared.

* fix(goal): let the agent pursue the active goal at user priority

The durable-context contract told the model never to follow instructions in
its data message, and that message now carried the goal. While a goal is
rendered, the contract ends with a static exception for the <active_goal>
element that opens the data message: work toward it as a request in a user
message, with no system or developer authority, while every other field
value, and any other text that calls itself a goal, stays data.

The element replaces the heading the goal used, which a summary could
reproduce. Other field values in the data message are HTML-escaped, and
active_goal joins the sanitizer denylist, so user input and remote tool
results cannot carry a raw one either.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-27 13:38:32 +08:00
Shxiao
230d8f03d7
fix(memory): validate Honcho's allow_insecure_http as a boolean (#5887)
* fix(memory): validate Honcho's allow_insecure_http as a boolean

* docs(memory): list the full Pydantic boolean vocabulary for allow_insecure_http
2026-09-27 11:18:03 +08:00
NanPan
b63906ce28
fix(sandbox): retain AIO acquire ownership across cancellation (#5888)
* fix(sandbox): retain AIO acquire ownership across cancellation

* test(sandbox): pin cancelled AIO acquire ownership

* docs(sandbox): document acquire worker ownership

* fix(sandbox): drain only started AIO acquire workers

* test(sandbox): cover queued and started acquire workers

* docs(sandbox): scope AIO acquire ownership guidance

* style(sandbox): separate acquire worker helper

* test(sandbox): queue reclaim deterministically

* style(sandbox): format acquire worker helper

* docs(sandbox): trim acquire guidance below chain limit

* fix(sandbox): separate AIO holder work from lock waiters

* test(sandbox): pin AIO holder executor separation

* docs(sandbox): separate AIO waiter and holder workers

* docs(sandbox): trim AIO worker ownership guidance

* test(sandbox): align acquire worker executor fixtures

* test(sandbox): fix reconciliation executor fixture and formatting

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 11:16:38 +08:00
李靖
10bd065615
feat(agents): persist tool artifacts as resolvable handles (#4676) (#4929)
* feat(agents): persist tool artifacts as resolvable handles (#4676)

MCP and sandbox tools return file paths, URLs, and task ids that are
lost when context compaction summarizes the conversation. This adds a
per-thread artifact handle registry so the model can reference
artifacts by short deterministic handles (art_xxxxxxxx) that survive
compaction and resolve to real references at the tool-call boundary.

- ThreadState.tool_artifacts: reducer channel capped at 100 entries,
  same-handle latest-wins, survives summarization compaction
- artifact_registry: deterministic handle derivation from
  (thread_id, tool_call_id, call_index); extraction precedence
  structured_content -> file/image blocks -> conservative text scan;
  HTML-escaped model-context rendering with char budget
- ArtifactCaptureMiddleware: before_model hook captures lightweight
  metadata (never content) and tracks handle consumption
- ArtifactResolutionMiddleware: rewrites handles in tool-call args to
  real references before execution; unknown handles untouched
- DurableContextMiddleware: projects the registry into the hidden
  durable-context data block ([available]/[consumed] status)
- config: new tool_artifacts section (all switches default on)
- frontend: artifact badges on tool-call steps keyed by tool_call_id

Fixes #4676

* fix(agents): harden artifact registry after review (#4676)

Review findings on #4929:

- capture/consumption updates emitted in the same before_model call are
  now concatenated instead of dict-merged; dict-merging clobbered the
  shared tool_artifacts key and permanently dropped fresh captures
- handles embed a per-result ordinal so multiple references inside one
  tool result never collide (two paths in one text block previously
  collapsed into one handle)
- structured_content extraction pulls concrete refs from known keys
  (file/path/url/task_id/..., string or list-of-string values) instead
  of storing a truncated JSON preview, falls back to an untruncated
  whole-object data entry, and no longer skips file/image content blocks
- plain-string tool results (e.g. bash/write_file output) are scanned
  for sandbox paths / remote URLs too, gated by detect_refs_in_text
- wire max_entries through configure_tool_artifact_max_entries at
  assembly (clamped 10..1000) and gate detect_refs_in_text scans
- resolution requires both enabled and resolve_handles_in_args,
  matching its docs
- add real ToolCallRequest.override coverage and the spec 14.4
  integration cycle (capture -> inject -> resolve -> consumption)

* fix(agents): address second-round artifact registry review (#4676)

Follow-up findings on #4929:

- drop generic `output`/`outputs` from structured ref keys: prose under
  those keys no longer becomes a bogus file entry (falls back to the
  whole-object data entry; path-shaped refs are still caught by the
  free-text scan)
- strip closing quotes, brackets, and backticks from detected refs so
  markdown- and JSON-wrapped paths resolve to clean real references
- remove the process-wide cap setter: the reducer enforces only an
  absolute ceiling (1000) and ArtifactCaptureMiddleware applies the
  configured max_entries per agent when emitting updates, so agents
  with different configs in one process never share or flap a cap
- skip extraction for tool messages whose tool_call_id already has
  registry entries and memoize known-empty results (bounded FIFO), so
  steady-state capture cost drops to the new message tail instead of a
  full-history rescan per model call
- correct the inverted ordering claim: capture is position-independent
  in the wrap chain because it is a before_model hook reading state
  messages; fixed the assembly comment, AGENTS.md item 15, and the
  ordering test docstring

CI parity verified locally: ruff check + format --check, full offline
suite, blocking-io suite, collect-only, uv lock --check,
check_agent_guidance, frontend format/lint/typecheck/build/tests.

* fix(agents): sliding-window cap and URI gates for artifact registry (#4676)

Third-round review findings on #4929:

- max_entries is now a sliding retention window: fresh captures are
  always registered, and when the projection exceeds the configured
  cap the middleware appends a trailing {"op": "trim_to", "keep": N}
  directive that the reducer applies by evicting the oldest entries.
  The previous remaining-budget emission froze the registry at the cap,
  permanently dropping post-compaction artifacts — the exact failure
  mode #4676 targets — and silently disabled the round-2 rescan
  optimizations at the point threads grew longest
- _track_consumption memoizes settled tool-call args in a bounded FIFO
  alongside the existing empty-result memo, so historical AIMessage
  args are not regex-rescanned every model call
- file/image content-block URLs must be http(s) or path-shaped;
  data:/blob: URIs (MCP embedded resources carrying base64 payloads)
  can no longer enter thread state, tool args, or crowd out the render
  budget; protocol-relative //host forms are rejected as well
- structured-content ref values go through the same URI-shape gate as
  content blocks, closing the embedded-payload hole via known keys such
  as url/urls
- tool_artifacts.enabled now gates the whole middleware including
  consumption tracking via an early return in before_model

Docs updated to describe the sliding window, URI gates, enabled scope,
and both bounded memos.

* fix(agents): final evictions and pending-aware consumption for artifact registry (#4676)

Fourth-round review findings on #4929:

- the sliding window interacted with the seen_call_ids skip to
  resurrect evicted entries every round: once an entry was trimmed, its
  tool_call_id left the seen set while its ToolMessage stayed in
  context, so the next model call re-extracted it, re-registered it as
  newest, and evicted something else — a never-converging rotation.
  The empty-result memo is now a unified _processed_results memo that
  records every extraction regardless of outcome, making evictions
  final; steady state over a saturated thread is quiescent (pinned by
  a multi-round test asserting no updates and zero extractions)
- the consumption scan built its handle map from pre-capture state, so
  a call referencing a handle captured in the same before_model call
  missed, got settled as quiet, and its consumption mark was lost
  forever. _track_consumption now receives the pending capture entries
  and resolves against state + pending; a call settles as quiet only
  after every referenced handle resolved, so transient misses retry
  next round instead of permanently suppressing the mark

AGENTS.md entry 15 updated to describe both memos and the
eviction-is-final semantics.

* fix(artifacts): address occurrence identity, PII and URL review findings

* fix(artifacts): enforce resolved policy checks and bounded registry handling

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 11:09:41 +08:00
Hyeonsang Cho
a253faefdf
fix(docker): give project-document uploads their own nginx location so they are not capped at 1 MB (#5934)
* fix(docker): give project-document uploads their own nginx location

POST /api/projects/{id}/documents is a multipart upload that Gateway
accepts up to uploads.max_file_size (50 MiB by default), but no nginx
location matched it. It fell through to the /api/ catch-all, which has no
client_max_body_size, so nginx's 1m default answered a 2 MB PDF with a
bare 413 before Gateway saw the request. Reproduced on nginx:alpine with
the shipped config; the same body to /api/threads/{id}/uploads passed.

Add a `~ ^/api/projects/[^/]+/documents` location to the Docker, make dev
and Helm configs with the thread-uploads settings (100M, streamed body)
and the 600s read timeout the catch-all already granted these routes. The
catch-all keeps nginx's defaults. The shared location-block helpers move
to tests/support/nginx_conf.py so the new test and the existing body-size
test use one implementation.

* docs(changelog): reference #5934 in the project-documents nginx entries
2026-09-27 10:58:54 +08:00
lihongyuan99
19618fc30b
fix(frontend): stop treating a fence line with an info string as a closer (#5919)
CommonMark forbids an info string on a closing fence, so "```python" inside a
plain ``` fence is content, not the end of the block. The closer check that two
fence consumers share only compared character and run length, which ends the
block early and pushes whatever follows back into prose: on the render path a
system marker inside the sample loses its tags and its content becomes visible
text, and on the export path stripInternalMarkers deletes real user content from
the thread markdown.

mermaid.ts already had the correct predicate privately. Move it into the shared
leaf module and use it from both consumers, so the three fence scanners in
core/streamdown and core/messages agree on what closes a block.
2026-09-27 10:57:24 +08:00
pclin
e080c194e0
docs(memory): explain the pluggable MemoryManager and how to implement your own backend (#5929) 2026-09-27 10:44:12 +08:00
xiaodu55
0f4c05bf28
fix(tests): skip POSIX-host-bound contract tests on Windows dev hosts (#5931)
22 backend tests assert facilities that only exist on a POSIX host and
hard-fail on Windows (CI is ubuntu-latest, so upstream never sees them):

- test_extension_dependency_sync.py: 11 tests spawn bare 'make -n';
  hosts without GNU make get WinError 2 (make is not part of the
  documented Windows setup).
- test_lark_broker.py: 10 tests execute the '#!' fake lark-cli / shim /
  launcher scripts; CreateProcess cannot run shebang scripts
  (WinError 193).
- test_bench_sandbox_provider.py: the boxlite shim workaround test
  asserts POSIX exec-bit semantics after os.chmod; Windows has no
  execute bit and boxlite sandboxes are Linux-only.

Each now skips with an explicit reason when its facility is absent;
Linux/CI coverage is unchanged. On a Windows host the three files go
from 22 failed to 0 failed / 22 skipped.

Fixes #5930
2026-09-27 10:43:19 +08:00
haochen
5d3972791d
fix(paths): reject trailing newlines in scoped IDs (#5911)
Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
2026-09-27 10:13:05 +08:00
Hyeonsang Cho
231335d309
fix(deploy): honor BETTER_AUTH_SECRET and DEER_FLOW_INTERNAL_AUTH_TOKEN from .env in make up (#5928)
* fix(deploy): honor BETTER_AUTH_SECRET and DEER_FLOW_INTERNAL_AUTH_TOKEN from .env

deploy.sh only checked the shell before reloading a persisted secret or
generating a fresh one and exporting it. Compose interpolation lets shell
variables outrank --env-file, so a secret written to the repo-root .env --
the surface every deployment doc points at -- was silently replaced by the
generated one: sessions were signed with a secret the operator never chose
and Gateway workers outside the stack got 401 with the configured token.

Leave a .env-provided secret unexported so Compose parses it itself
(shell -> .env -> persisted file -> generated), and keep generating when
the shell exports an empty value, since Compose would otherwise pass the
empty string through.

* docs(changelog): reference #5928 in the deploy.sh dotenv secrets entries

* fix(deploy): decide whether .env provides a secret with Compose's own parser

read_dotenv_value only matches KEY=VALUE and returns the literal text, but
Compose also accepts `KEY: VALUE` lines and interpolates `${VAR}` inside
values. So `TOKEN: abc` looked absent (and got shadowed by a generated
token) while `TOKEN=${UNSET}` looked present (and suppressed generation,
leaving the stack with an empty token).

Ask Compose for its resolved interpolation environment instead
(`docker compose config --environment`, same --env-file, stub project on
stdin, project directory docker/) and treat a resolved-empty value as
missing. Clients older than Compose 2.28 lack the flag and fall back to
the plain reader. Tests cover the canned Compose answers, the fallback,
and every dotenv spelling against the real Compose client when installed.

* fix(deploy): resolve .env secrets through Compose on every supported client

`config --environment` needs Compose 2.28, so 2.24–2.27 clients took the
plain KEY=VALUE fallback and kept both defects: `TOKEN: abc` was missed
and shadowed, `TOKEN=${UNSET}` counted as present and left the stack with
an empty token.

Drop the fallback. Render a stub project whose only environment entry is
`${KEY}` through `docker compose config` — same --env-file and project
directory as the real command — and read the interpolated value back;
`""` means empty or unset. This is plain interpolation, available on every
Compose v2, and verified identical on 2.24.0, 2.27.0 and 2.32. A failing
probe stops the script instead of guessing. The real-Compose tests now run
against the docker CLI and any standalone binaries listed in
DEER_FLOW_TEST_COMPOSE_BINARIES.
2026-09-27 09:57:58 +08:00
theater
cae4b25585
fix(agents): scope delegation ledger IDs to parent runs (#5898)
* fix(agents): scope delegation ledger IDs to parent runs

* fix(agents): preserve checkpointed replies from resumed runs
2026-09-27 09:50:30 +08:00
xbzz1018
9abdbc3870
fix(uploads): reject NUL characters in filenames (#5915) 2026-09-27 09:29:30 +08:00
Daoyuan Li
bd168e3552
fix(skills): prune ancestor cycles during discovery (#5921) 2026-09-27 09:25:00 +08:00
ZJPex
f196844625
feat: add optional message-role filtering to history_search (#5927)
* feat: add optional role filtering to task history search

* docs: scope history role guidance to task continuity

* test: register task continuity guidance in scoped inventory

* docs: separate English guidance from Chinese README translation
2026-09-27 09:10:20 +08:00
Shxiao
e8cb5e9c4f
fix(skills): keep unmatched markdown brackets out of quadratic backtracking (#5884)
* fix(skills): bail out of the markdown-link scan when no link construct exists (#5714)

* fix(skills): scan markdown links linearly and skip failed target runs (#5714)

* fix(skills): make link scan exactly match finditer, blank spans by position

Address review feedback: drive the scan by openers (the regex engine's
own start-position scan) so it reproduces finditer exactly, blank
matched spans by position instead of str.replace, pin the equivalence
with tests, and add CHANGELOG entries.

* fix: resolve skill review test lint errors

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 09:10:08 +08:00
Totoro
4028b63647
fix(models): keep Claude prompt-cache markers out of thread messages (#5892)
* fix(models): keep Claude prompt-cache markers out of thread messages

ClaudeChatModel._apply_prompt_caching set cache_control on payload blocks in
place. langchain-anthropic forwards Claude-native blocks (image or document
blocks with a source, search results) by reference, so the marker landed in
the thread's own message and was checkpointed with it. Each later request
carried those stale markers on top of its own four, and from the third turn
of a thread with such blocks the API rejected every request (400, "A maximum
of 4 blocks with cache_control may be provided").

Breakpoints are now placed on copies, and markers already in the request are
dropped first, so threads whose checkpoints already carry them recover
without a migration. The OAuth path's _strip_cache_control popped markers
from the same shared objects; it now copies too, and prompt caching reuses it.

* fix(models): drop stored Claude cache markers with caching off too

The strip only ran inside _apply_prompt_caching, so with
enable_prompt_caching: false a thread whose checkpoint already carried five
or more markers kept failing: exactly the switch an operator would flip in
response to these 400s. _get_request_payload now strips on the non-caching
path as well, so recovery no longer depends on the flag.

The stored-checkpoint recovery is now pinned through _get_request_payload
with real messages, for caching on and off. The docstring and the AGENTS.md
note now say which objects are really shared (blocks, list-form system
blocks, a reused binding's tool dicts), that the message-dict and list
copies are defensive, and that _apply_prompt_caching must strip first.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-27 08:29:56 +08:00
xbzz1018
e2c5027b0c
fix(tracing): tag graph runs with the effective model (#5902) 2026-09-27 08:06:03 +08:00
NanPan
fa2afa6a39
fix(sandbox): drain network policy mutations across cancellation (#5899)
* fix(sandbox): drain network policy mutations across cancellation

* test(sandbox): pin network policy mutation ownership

* docs(sandbox): pin network policy mutation ownership

* style(sandbox): sort network policy test imports

* fix: resolve sandbox cancellation lint checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 07:58:15 +08:00
Yeager0204
b433e3c6a0
fix(runtime): record textless human input only once per run (#5903)
Co-authored-by: kwsYegar <331533331+kwsYegar@users.noreply.github.com>
2026-09-27 07:41:51 +08:00
Hyeonsang Cho
1dfd307c92
fix(docker): strip the IPv6 listen in the production nginx launcher too (#5900)
* fix(docker): strip the IPv6 listen in the production nginx launcher too

docker/nginx/nginx.conf listens on 2026 and [::]:2026. On a host booted
with ipv6.disable=1 (or a kernel without IPv6) nginx refuses to start —
socket() [::]:2026 failed (97: Address family not supported by protocol) —
so the container restart-loops under `restart: unless-stopped` and
`make up` waits on a readiness probe that never turns healthy. #2027 added
a launcher guard that deletes the IPv6 listen when /proc/net/if_inet6 is
absent, but only to docker-compose-dev.yaml; the Helm chart mirrors it;
docker-compose.yaml — the file behind `make up` and scripts/deploy.sh —
still ran the bare copy-and-start command.

Give the production nginx service the same launcher as the dev file:
`set -e`, copy the template, guard, and `exec nginx` so nginx is PID 1 and
receives docker stop's signal directly.

The new test pins the guard in all three launchers (prod, dev, Helm),
requires the three sed patterns to be identical so they cannot drift again,
checks the guard edits the file nginx actually loads, and applies the
pattern to the shipped compose and Helm configs to confirm it removes only
the IPv6 listen. Verified on nginx:alpine: BusyBox sed strips exactly that
line and `nginx -t` accepts the result; the rendered production launcher
runs unchanged when IPv6 is present.

* docs(changelog): reference #5900 in the nginx IPv6 guard entries
2026-09-27 07:36:35 +08:00
Hyeonsang Cho
bf9a019f28
fix(skills): let remove_file/write_file handle binary support files and reject directories (#5893)
* fix(skills): let remove_file/write_file handle binary support files and reject directories

A .skill archive may carry binary support files such as assets/logo.png:
the installer only rejects executable binaries by magic bytes. But both
skill_manage file actions read the previous content as UTF-8 text before
touching the file — purely to fill the history record's prev_content — so
a binary member raised UnicodeDecodeError out of
SkillStorage.remove_custom_skill_file (and out of the tool's write_file
prelude) and was never removed or overwritten.

A bare support directory ("assets") also slipped through
ensure_safe_support_path, because it resolves to its own allowed root and
passes the containment check, and then raised IsADirectoryError from the
same read.

Add read_text_or_none for the history reads: UTF-8 text is returned as
before, undecodable content yields None (the record simply has no previous
text), and IO errors still propagate. Require a filename below the support
subdirectory in ensure_safe_support_path, and reject an existing directory
with ValueError in both remove_custom_skill_file and the tool's write_file,
so the model gets an actionable validation error instead of a crash.
prev_content is informational for support files — rollback only restores
SKILL.md — so recording None loses nothing that could be used.

* docs(changelog): reference #5893 in the support-file entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 18:14:34 +08:00
WuWenbo
fd5b8e275c
fix(middleware): preserve metadata during input sanitization (#5875)
* fix(middleware): preserve metadata during input sanitization

* fix(middleware): isolate sanitized content blocks
2026-09-26 18:01:48 +08:00
NanPan
79a5af071a
fix(events): retain JSONL snapshot ownership across cancellation (#5795)
* fix(events): retain JSONL snapshot lock through cancellation

* test(events): cover cancelled JSONL snapshot ownership

* docs(events): record JSONL snapshot ownership invariant

* test(events): pin snapshot cancellation failure semantics

* docs(runtime): name drained JSONL snapshot tasks

* test(events): pin snapshot writer exclusion on disk

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 17:59:31 +08:00
Bxl
41e8e75f56
fix: balance temporary sandbox leases for uploads (#3261)
* fix: balance temporary sandbox leases for uploads

* fix: address sandbox upload review feedback

---------

Co-authored-by: Bxl <188532127+BXL1015@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 16:22:20 +08:00
Daoyuan Li
1b0b27ff43
fix(skills): stop integration discovery at package boundaries (#5890) 2026-09-26 15:56:54 +08:00
asts
b5c5cdd6bc
fix(mcp): restore task owners for background credential selection (#5870)
* fix(mcp): restore task owners for background credential selection

* fix(mcp): narrow background task owner binding
2026-09-26 15:37:52 +08:00
yc2bgr8
cdb5028d88
feat(community): offload image search to a thread via asyncio.to_thread (#5788)
* feat(community): offload image search to a thread via asyncio.to_thread

Make image_search_tool async and run the blocking ddgs call in a worker
thread with asyncio.to_thread, continuing the TODO.md planned feature
('Replace sync requests with httpx.AsyncClient in community tools').

The ddgs SDK exposes no async API, so instead of swapping the HTTP
client the blocking DDGS.images() call is offloaded to a thread — the
same pattern the infoquest/jina_ai tools use for readability
extraction. Callers see no change in behavior.

Tests invoke the async tool via ainvoke under @pytest.mark.anyio.

* fix(image-search): offload config lookup with DDGS search

* test(image-search): pin DDGS search off the event loop

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 15:21:47 +08:00
sxh
3bb9a1c6ba
fix(logging): redact credential-bearing header values in urllib3's header-parse dump (#5752)
* fix(logging): redact credential-bearing header values in urllib3's header-parse dump

* fix(logging): bind redacted before the header-dump pass runs

* fix(logging): collapse the first field of a header dump, which shares its line with the url argument

* fix(logging): collapse every header-dump field, in the message and in the exc_info traceback

* test(logging): give the traceback message copy real CRLF

The message half of the record built its payload with literal backslash-r
text while the exc_info copy used real CRLF, so the two halves rendered the
dump in different shapes and the message only exercised the pass through a
fragment match instead of the escape the real record produces.

* fix(logging): redact obs-fold continuations and Proxy-Authentication-Info

A malformed response can fold a header value onto the next line, and this
warning exists precisely to dump malformed upstream bytes, so a folded
credential cannot be assumed absent: the pass replaced only the matched
segment and left the continuation - `CookieSecret; Path=/` - in the log
whole. A collapsed field now also swallows the SP/HTAB-prefixed segments
that follow it, while a non-sensitive field keeps its own continuation.

Proxy-Authentication-Info is the proxy-side twin of Authentication-Info,
which was already on the list; nextnonce reaches the payload and was
logged verbatim in both the message and the exception copy.

* fix(logging): redact a credential folded under an unlisted field, past repr escapes, and behind an empty first segment

Addresses the three P1 findings on the header-parse dump pass:

- A credential that exists only in an obs-fold continuation is now collapsed.
  ``_CREDENTIAL_FIELD_RE`` no longer carries the ``^``: the anchor is applied
  past a segment's fold marker instead, because under RFC 7230 unfolding a
  bearer token folded under a field this list does not name is still a bearer
  token.
- ``_OBS_FOLD_RE`` now also recognises the repr-escaped ``\t`` spelling. The
  payload reaches the record through ``!r``, so a real tab is the two
  characters backslash-t and never matched a literal-class marker.
- The continuation walk runs even when the matched value is empty, so
  ``Set-Cookie:\r\n Secret; Path=/`` collapses instead of leaving the secret in
  the segment that follows.

Evidence: 3 new regression tests, red on the previous head (3 failed / 23
passed) and green on this commit (26 passed) against urllib3 2.7.0; ``ruff
check`` and ``ruff format --check`` clean on both files.

* test(logging): move the header-parse tests out of the EOF hunk

Upstream `22b5b59` appended a redaction test at the end of this file, the same
position this branch appends its own block to, so the PR reported a conflict.
Relocating this branch's block to the preceding top-level boundary frees the
EOF hunk; the three-way merge against current `main` is then clean.

Content is unchanged: base->head is a single pure insertion of the same 312
lines (0 deletions, same line multiset as before this commit), and the merge
result is byte-identical in aggregate to resolving the conflict by hand.

* fix(logging): treat an escaped bare CR as a header-dump line break

urllib3's header-parse warning carries the unparsed block through repr(), so
its separators are escaped text. The splitter recognised escaped CRLF and
escaped LF but not escaped bare CR, which http.client also accepts as a line
terminator: a payload separated on bare CR alone stayed in one segment, no
field name landed at a segment start, so the credential anchor never fired
and the Set-Cookie and signed Location values were logged verbatim in both
the message and the repeated exc_text. Add the escaped \r alternative and an
end-to-end case driven through the real parse_headers/assert_header_parsing
pair on the pinned urllib3 2.7.0.

Co-Authored-By: sxh313 <sxh313@users.noreply.github.com>

* test(logging): pin the bare-CR header dump on the JSON path too

The Gateway runs with logging.enhance.format=json, and JsonTraceFormatter
renders the exception itself, so the text-format case alone left the deployed
emission unguarded for an escaped bare-CR payload. Record the measured cost of
reordering the break alternation, which obs-fold continuations depend on.

---------

Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
2026-09-26 15:06:48 +08:00
Hyeonsang Cho
59aa91d622
fix(projects): label untitled threads in the conversation-files view (#5879)
* fix(projects): label untitled threads in the conversation-files view

GET /api/projects/{id}/threads/files returns each member thread's
display_name as `str | None`: the thread meta row is created with NULL and
only filled once title generation has run after the first reply, or never
if that step fails. The frontend's ProjectThreadFileGroup declared the field
as a required string and ConversationFileGroup rendered it verbatim, so a
thread whose files were uploaded before its first reply showed them under
an empty heading. The sibling ProjectThread type already models the field
as nullable and every consumer of it falls back to t.projects.untitled.

Make the group type nullable to match the wire, and render the heading
through a shared threadTitle() helper that both the thread-name map and the
file group now use, so the "Untitled" rule lives in one place.

The DOM test covers null, missing and whitespace-only titles.

* docs(changelog): reference #5879 in the conversation-files entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 12:06:12 +08:00
Hyeonsang Cho
9c53ebb06f
fix(skills): unlink a symlinked custom skill package on delete (#5881)
* fix(skills): unlink a symlinked custom skill package on delete

The storage contract (_is_external_skill_directory_symlink) accepts a
one-level symlink under custom/ that points at an operator-managed skill
package, and every read path honours it: the skill loads, lists, validates
and can be enabled. delete_custom_skill, however, removed the package with
shutil.rmtree, which refuses symlinks. The delete raised OSError after the
history record had already been appended, skill_projection_mutation cleared
the user's projected custom view on the way out (rebuilt on the next
acquire), and DELETE /api/skills/custom/{name} answered 500 — for both
LocalSkillStorage and UserScopedSkillStorage, which inherits the method.

Unlink the entry when it is a symlink and keep rmtree for real directories.
The external tree is not ours to delete: removing the skill means removing
the link. Checking is_symlink() first also covers a dangling link, which
exists() would report as absent.

Tests cover both storages: the link is gone, the skill no longer exists,
the external package (SKILL.md and a support file) is untouched, the
history record was written, and the sibling skill's projected view
survives the delete.

* docs(changelog): reference #5881 in the symlinked-skill delete entries
2026-09-26 11:44:48 +08:00
yc2bgr8
17f4722d2b
feat(community): migrate tavily tools to AsyncTavilyClient (#5784)
* feat(community): migrate tavily tools to AsyncTavilyClient

Replace the synchronous TavilyClient with AsyncTavilyClient in the tavily
community tools, as tracked in backend/docs/TODO.md planned features
('Replace sync requests with httpx.AsyncClient in community tools').

web_search_tool and web_fetch_tool become async functions and await
client.search()/client.extract(). External behavior (return values,
error strings, credentials handling) is unchanged; the client is still
constructed with api_key=None when the tool config has no key, letting
the SDK read TAVILY_API_KEY from the environment.

Tests are updated to invoke the async tools via ainvoke under
@pytest.mark.anyio, and the credential assertions now verify the client
is constructed with the right api_key (AsyncTavilyClient keeps the key
inside its httpx client and exposes no public api_key attribute).

* fix(tavily): close async clients after each tool request

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 11:43:32 +08:00
NanPan
f2dd189cf9
fix(sandbox): retain AIO flock across cancellation (#5820)
* fix(sandbox): retain AIO flock across cancellation

* fix(sandbox): drain async create before flock release

* test(sandbox): cover cancellation during AIO create

* docs(sandbox): pin create drain ownership

* fix(sandbox): keep flock waits cancellable

* test(sandbox): pin AIO flock lifecycle drains

* docs(sandbox): document cancellable flock wait

* test(sandbox): follow nonblocking flock helper

* test(sandbox): update create cancellation lock mock

* style(sandbox): sort lifecycle imports

* test(sandbox): assert cancelled flock wait closes file

* docs(sandbox): trim cancellation guidance below chain limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-26 11:18:58 +08:00
haochen
fc9fb2dea2
test(lark): make blocking I/O CLI fixture work on Windows (#5826)
* test(lark): make blocking I/O CLI fixture work on Windows

* docs(tests): document portable Lark CLI fixtures

---------

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
2026-09-26 08:17:28 +08:00
Hyeonsang Cho
74f6dce489
fix(dev): keep whole worktree paths when serve.sh builds DEERFLOW_ROOTS (#5856)
* fix(dev): keep whole worktree paths when serve.sh builds DEERFLOW_ROOTS

serve.sh collects every deer-flow worktree root so `make stop` / `make dev`
can reclaim the shared dev ports (8001/3000/2026) from a sibling checkout.
It parsed `git worktree list --porcelain` with `awk '{print $2}'`, but that
output does not quote paths, so a worktree at `.../deer flow two` was
recorded as `.../deer`. `_is_deerflow_pid` matches open files against
"<root>/", which the truncated root never satisfies, so a Gateway or
frontend started from that worktree was treated as a foreign process and
the start aborted with "port already in use".

Take the remainder of the line after the `worktree ` key instead. The
dedupe and longest-first ordering that follow are unchanged.

The new test builds a scratch repo with a spaced linked worktree and runs
the DEERFLOW_ROOTS block from serve.sh against it, from both the main
checkout and the spaced one.

* docs(changelog): reference #5856 in the worktree-roots entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 22:49:12 +08:00
Hyeonsang Cho
bbab1e84d0
fix(uploads): never let a malformed files[*].size abort the run (#5855)
* fix(uploads): never let a malformed files[*].size abort the run

UploadsMiddleware._files_from_kwargs validates every field of a
client-supplied file entry fail-soft — non-dict entries, non-basename or
staging filenames, and files missing from the uploads dir are all skipped —
except `size`, which went straight to int(). A value int() cannot convert
("abc", a list, inf) raised ValueError/TypeError/OverflowError out of
before_agent (and the executor-wrapped abefore_agent), failing the whole run
before the model was called. The Gateway strips only server-owned metadata
keys and edit/regenerate deep-copies `files` verbatim, so the same entry
failed again on every retry of that message.

The size only feeds the human-readable "(N KB)" line in <current_uploads>,
so route it through _coerce_file_size: a non-negative finite number (or a
numeric string such as "2048") is kept as int; None, bools, negatives,
non-finite floats and anything unconvertible fall back to 0, the value a
missing size already gets.

Tests pin the fallback for each unusable shape, the kept values, and that
before_agent still injects an existing upload whose size is malformed.

* docs(changelog): reference #5855 in the uploads size entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 22:35:28 +08:00
Jholly
8121dbcb57
fix(frontend): prevent run creation retries (#4725)
* fix(frontend): prevent run creation retries

* test(frontend): cover safe run stream reconnection

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 21:46:34 +08:00
Grapette.L
63401d79fd
fix(community): reject bool and fractional web-search max_results like image search does (#5852) 2026-09-25 21:43:48 +08:00
哈基米
5242fcfb63
fix(tools): route bound task and batch tools' sync path through the bound runtime (#5861)
bind_task_tool and _bind_batch_tool copied their process-wide singletons
with only the coroutine rebound. Once get_available_tools had wrapped a
singleton in place with a sync func, the copy inherited that wrapper
around the unbound coroutine, so synchronous invocation bypassed the
explicit SDK submitter/capacity and fell through to the process-global
one -- or raised NotImplementedError in a fresh process where the
singleton was never wrapped.

Rebind the copy's func to a sync wrapper around the bound coroutine so
both invocation paths use the runtime's own submitter and capacity.
2026-09-25 21:40:41 +08:00
哈基米
f92118e1e9
fix(sandbox): list the skills root when only category mounts exist (#5857)
* fix(sandbox): list the skills root when only category mounts exist

With the default (non-policy-scoped) LocalSandboxProvider layout, the
four skill categories (/mnt/skills/public, custom, legacy, integrations)
are mounted at separate host directories and there is no mapping for the
aggregate /mnt/skills root itself. LocalSandbox.list_dir resolved the
root to the unmapped host path, and the host list_dir raised
FileNotFoundError before the virtual sub-directory overlay -- which
exists exactly so the agent can discover the categories via
"ls /mnt/skills" -- could run, so the ls tool reported "Directory not
found: /mnt/skills" even with skills installed.

Treat a FileNotFoundError from the host scan as an empty listing when
mappings are mounted inside the requested container path, so the overlay
can surface those children. Paths with nothing mounted inside them keep
raising FileNotFoundError as before.

* fix(sandbox): preserve missing virtual parent errors

* test(screening): align PII fixture with keyed placeholders

* fix(sandbox): preserve non-directory errors for virtual parents

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 21:27:25 +08:00
yc2bgr8
3d921afa5f
feat(community): migrate infoquest client to httpx.AsyncClient (#5782)
* feat(community): migrate infoquest client to httpx.AsyncClient

Replace blocking sync requests.post calls with httpx.AsyncClient in the
InfoQuest community tool, as tracked in backend/docs/TODO.md planned
features. fetch/web_search/image_search and their raw-results helpers are
now async, keeping the 30s transport timeout and external behavior
(return values, error strings) unchanged.

The langchain tool wrappers in infoquest/tools.py become async functions;
web_fetch offloads readability extraction to a thread (same pattern as
the already-migrated jina_ai tool). Tests are updated to mock
httpx.AsyncClient instead of requests.post.

* test(community): use AsyncMock for infoquest fetch in relative-links test

The infoquest web_fetch_tool now awaits client.fetch, so the test stub
must return an awaitable instead of a sync lambda.

* fix(community): follow InfoQuest API redirects like the sync client did

Address the review note on #5782: requests.post follows redirects by
default while httpx.AsyncClient defaults to follow_redirects=False, so
a 3xx answer from the reader/search endpoints would have surfaced as
'Error: fetch API returned status 30x' (or fallen into the generic
error path after raise_for_status) instead of the final content.

All three AsyncClient call sites now pass follow_redirects=True. Adds
regression tests asserting each client is constructed with
follow_redirects=True, and documents the parity in the test contract.

* test(community): fix InfoQuest mock endpoint typo
2026-09-25 20:58:17 +08:00
IronMurphy
65ed502e54
fix(release): refresh backend/uv.lock when bumping the version (#5859)
* fix(release): refresh backend/uv.lock when bumping the version

`scripts/bump_version.sh` rewrote backend/pyproject.toml,
frontend/package.json and the Helm chart's `version`/`appVersion`, but
`backend/uv.lock` also records the root package version (uv keeps its PEP 440
form, so `2.1.0-rc0` is stored as `2.1.0rc0`). The documented release step left
the lock pinning the previous version, and that is not cosmetic drift:
`uv lock --check` (lint CI) fails on a stale lock and `uv sync --locked`
(e2e / plugin workflows) refuses the tree, so the release commit could not pass
CI. RELEASING.md only mentioned the manual `cd backend && uv lock` step for
release candidates.

The script now refreshes the lock via `uv lock` and refuses to start without
`uv` — it exits before editing anything rather than leaving a half-bumped tree
whose lock CI rejects. `uv lock` stays the authority for the normalized
prerelease form; the lock diff for a bump is a single line. RELEASING.md and
AGENTS.md now describe the lock as part of the bump.

Tests: backend/tests/test_bump_version_lockfile_sync.py (red on main, green
here) pins both halves — the lock moves with the sources, and a missing uv is an
error instead of a silent no-op.

* docs(changelog): record the release bump lockfile fix (#5859)

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 20:42:27 +08:00
xiaodu55
64e499be98
feat(memory): redact queued memory payloads (pii_redaction follow-up) (#5577)
* feat(memory): redact queued memory payloads (pii_redaction follow-up)

Closes the remaining vector from #3190 (memory storage): request-scoped
redaction leaves originals in thread state, and MemoryMiddleware queued
that raw conversation into the durable extraction buffer — so the
extraction model's input and the persisted facts carried raw PII even
with pii_redaction.enabled.

- MemoryMiddleware gains pii_redaction_config (same pattern as
  summarization / durable-context / title); the payload returned by
  _resolve_add_args() is redacted at the enqueue boundary before
  add()/aadd(), covering both after_agent and aafter_agent
- Reuses the shared redact_text() seam from #5527 via
  active_pii_detectors/_Redactor/_redact_content; the buffered payload
  itself becomes clean, not just the extraction model's view
- Wired at the lead-agent assembly site
- Tests: queue-payload redaction enabled/disabled, detector toggles,
  message immutability, token stability across turns

* fix(memory): reserve existing placeholder indices across the queued batch

Review round 1 on #5577: thread state mixes raw user turns with
already-redacted tool results and summaries, so a token in a later
message must not collide with a new value in an earlier one. Pre-scan
the whole queued batch with redactor.reserve() before allocating.

Also extend the two MemoryMiddleware stubs in
test_lead_agent_model_resolution.py to accept the new
pii_redaction_config kwarg (CI backend-unit-tests red).

* fix(memory): redact the compaction-triggered memory_flush_hook queue too

Review round 2 on #5577: with memory + summarization enabled,
memory_flush_hook() queues list(event.messages_to_summarize) directly
into MemoryManager.add_nowait(), bypassing MemoryMiddleware entirely —
and compaction removes those messages from state right after, so the
after-agent redaction can never repair a raw batch queued there.

Extract the batch redaction (reserve pre-scan + rebuild) into a shared
redact_queued_messages() used by both MemoryMiddleware's enqueue
boundary and the hook; the hook gains an optional pii_redaction_config
wired via functools.partial at the summarization factory (subagents use
skip_memory_flush=True and are unaffected). Regression covers the hook
path enabled/disabled/absent.

* test(summarization): unwrap the partial in the factory hook identity check

edd916e wrapped memory_flush_hook in functools.partial to carry the
pii_redaction config, but missed committing the matching update to
test_factory_attaches_memory_flush_hook_by_default — a functools.partial
is not equal to its wrapped function, so the identity assertion failed
deterministically (caught by static review on #5577).

* fix(memory): cover tool-call arguments and lenient text blocks in queued payloads

Review round 4 on #5577 — two reproduced gaps still allowed raw PII into
memory payloads with pii_redaction.enabled=true:

- OpenViking retains the full message object, so parsed tool_calls args
  and provider-format arguments in additional_kwargs bypassed the
  content-only redaction; queued payloads now redact tool-call args
  recursively (and reserve them in the pre-scan).
- DeerMem's format_conversation_for_update reads p.get("text") of any
  dict block, not only type=="text" — _redact_content now redacts every
  string-valued text field so a lenient consumer cannot bypass it.

Both repro shapes are pinned as regressions.

* fix(pii): value-derived placeholders for stable capture identity

Review round 5 on #5577: sequential allocation is order-dependent — the
same unchanged message re-redacted in a later batch can get a different
token, which flips OpenViking's content-signature and replays already
recorded turns into persistent memory sessions. Placeholders are now a
pure function of the matched value (sha256-6 per category): identities
stay stable across batches, enqueues, seams, and backends with no shared
state and no stored mapping.

- _Redactor becomes stateless; the reserve machinery (index pre-scans,
  _PLACEHOLDER_PATTERN) is removed along with its call sites
- redact_texts keeps its signature, now a simple per-text redaction
- reserve-semantics tests are superseded by cross-enqueue identity tests
  (two successive captures where the second batch carries higher existing
  placeholders); the compiled-graph echo regex matches the new format

* fix(pii): 128-bit placeholder digests; redact tool-argument keys

Review round 6 on #5577:
- [P2] The 24-bit truncation collided distinct identities (reproduced:
  contact3513@ / contact3727@example.com both rendered [EMAIL_575dbb]).
  Retain 128 bits of the digest so separate people stay separate at any
  realistic volume; the compiled-graph echo regex and the AGENTS.md entry
  follow the new format.
- [P2] _redact_strings redacted values but preserved mapping keys, so
  {"contacts": {"alice@example.com": "manager"}} kept the raw email
  as a key. Redact string keys along with values; deterministic 128-bit
  tokens make distinct-key collisions a hash-collision non-event, and a
  redacted key equal to another key merges the entries. Regression pins
  the email-keyed arguments shape.

* docs(agents): trim PiiRedactionMiddleware entry; drop stale reserve wording

The chain grew past the 98304-byte AG002 hard limit again (main growth
plus the round-6 format example). Trim the entry to fit, and replace the
stale reserve-indices wording with the value-derived semantics that
replaced it.

* docs(agents): shrink PiiRedactionMiddleware entry below the chain budget

main moved (#5609) and the base comparison flipped the nearly-over chain
back to an AG002 error (98441 > 98304). Trim the entry to the facts that
are not documented elsewhere: seams, allowlist, order, enqueue boundary.

* fix(pii): letters-only value-derived tokens; wire the factory SDK path

Review round 7 on #5577:
- [P2] Hex digests carry digit runs that later digit-anchored detectors
  re-scan, corrupting minted tokens into nested placeholders (reproduced:
  user280@example.com -> [EMAIL_…[PHONE_…]…], ~1/140 emails). The 128-bit
  digest is now encoded over a-z, so a minted token survives the full
  pinned detector order.
- [P2] create_deerflow_agent (the SDK path) constructed MemoryMiddleware
  and DurableContextMiddleware without pii_redaction_config; the knob is
  now an explicit optional parameter threaded through
  _assemble_from_features (the factory reads no config files by design,
  so SDK callers pass it explicitly).
- [Nit] hoist the Command rebuild comprehension out of the per-message
  loop in _redact_result.
- [Nit] drop the dead _iter_tool_calls helper and Iterator import.

* test(factory): pin pii_redaction_config wiring; document the parameter

Review round 8 on #5577:
- [P3] test_create_deerflow_agent.py had zero coverage of the
  pii_redaction_config parameter; two regressions now assert that
  create_deerflow_agent(pii_redaction_config=cfg) builds a
  MemoryMiddleware and a DurableContextMiddleware carrying cfg, and that
  the default is None (redaction off).
- [Nit] document the parameter in the create_deerflow_agent docstring:
  None leaves redaction off; a middleware= takeover bypasses the
  assembly that would apply it.

* fix(pii): HMAC-scoped placeholder tokens; install model-bound redaction in the SDK chain

Review round 10 on #5577:
- [P2] sha256(category||value) is an unkeyed, globally deterministic
  fingerprint: a provider or memory backend can confirm candidate values
  offline and link identical values across users and threads, contradicting
  the documented no-re-identification guarantee. Placeholders are now
  HMAC-derived over a deployment-scoped token_secret (new config field);
  the docstring documents the linkability scope for both modes.
- [P2] _assemble_from_features() never appended PiiRedactionMiddleware, so
  the SDK knob enabled only the downstream seams while the primary model
  still received raw content. The middleware is now appended in the same
  relative position as the lead-agent base chain when the gate is on,
  with a model-call regression through the assembled SDK chain.

* docs(pii): document token_secret; update the placeholder format examples

Follow-up to the round-10 review on #5577:
- config.example.yaml documents the new token_secret knob: a
  deployment-scoped secret makes placeholders HMAC-derived and only
  linkable within one deployment; leaving it unset keeps publicly
  computable fingerprints (documented trade).
- the PiiRedactionConfig class docstring's placeholder example now
  matches the shipped format (128-bit HMAC digests over base-26
  letters).

* docs(pii): update the module docstring's placeholder format example

The module docstring still showed the sequential [EMAIL_1] example;
align it with the shipped value-derived format (128-bit HMAC digests
over base-26 letters).

* fix(pii): require a non-empty token_secret when redaction is enabled

Review round 11 on #5577: with the secret optional, the default enabled
configuration minted empty-key HMAC digests — publicly computable, globally
linkable fingerprints of the raw values, confirmable by offline guessing for
low-entropy identifiers such as phone numbers.

- PiiRedactionConfig now rejects enabled=true without a non-empty
  token_secret (minimum 16 characters); the unkeyed mode is gone entirely.
- The request-scoped redactor in _process_request now carries the configured
  key: it built its redactor without it, so user-message placeholders
  silently diverged from every other seam for the same value. The empty-key
  guard in _placeholder_token is the backstop that keeps every path keyed.
- Module/config docstrings and config.example.yaml no longer describe the
  placeholders as irreversible or the secret as optional; the stale
  reserve-semantics wording and the "memory path not covered" note are
  corrected.
- Regressions: validation errors for missing/blank/short secrets, and the
  user-message boundary minting the same keyed token as redact_text.

* fix(memory): redact original_user_content on queued memory payloads

Review round 12 on #5577: UploadsMiddleware preserves the raw user turn in
additional_kwargs.original_user_content, but redact_queued_messages rewrote
only content, tool_calls, and provider-format tool_calls — the queued copy
still carried the raw value, so the memory queue and backend received raw PII
with redaction enabled.

The shared helper now redacts the provenance string field on the queued copy
(same batch redactor, merged with the existing additional_kwargs rewrite,
rebuilt via model_copy — thread-state messages stay untouched), covering both
the enqueue boundary and the compaction flush hook. Regressions pin the
redacted provenance field through both paths and the untouched original.

* fix(memory): redact human_input_response on queued memory payloads

Review round 13 on #5577: clarification replies keep the raw user answer in
additional_kwargs.human_input_response.value (DeerMem reads that mapping via
its read_human_input_response mirror), but redact_queued_messages rewrote
only content, tool_calls, and original_user_content — the queued copy still
carried the raw value into the memory queue and backend.

The shared helper now rebuilds that mapping with value redacted (same batch
redactor) while preserving the protocol fields (version/kind/source/
request_id/option_id/response_kind), merged with the existing
additional_kwargs rewrite and rebuilt via model_copy so thread-state
messages stay untouched. Regressions pin the redacted value, preserved
protocol fields, the untouched original, and the flush-hook path.

* chore(config): bump config_version to 47; generate token_secret on upgrade

Review round 13 on #5577: existing deployments can legitimately sit at
config_version 46 with pii_redaction.enabled: true and no token_secret; the
mandatory-secret validator would fail their startup while config.example.yaml
still said 46, so the upgrade checker considered them current and emitted no
migration warning.

- Bump config_version to 47 so the outdated-config warning fires for v46.
- Add a v47 migration to scripts/config-upgrade.sh: when pii_redaction is
  enabled and token_secret is missing/blank, generate a random deployment-
  scoped secret (secrets.token_urlsafe) and persist it, so the upgraded
  deployment starts under the mandatory validation.
- Regressions: a v46 enabled config is flagged outdated against the real
  example; the upgrade run generates a >= 16-char secret and the result
  passes AppConfig validation; a disabled config gets no secret.
- Also switch the two v41 script tests from a hardcoded "bash" to
  SCRIPT_BASH/skipif, matching the documented Windows constraint and the
  neighboring tests (hardcoded bash resolves to the WSL stub there).

* chore(helm): sync chart config_version to 47

Review round 14 on #5577: check_config_version.sh fails whenever the chart's
embedded config_version lags config.example.yaml, and the chart workflow is
triggered by config.example.yaml — so the v47 bump left this head failing
that check. Update the values.yaml config block and the README example to
47; scripts/check_config_version.sh now passes.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-25 20:05:36 +08:00
Totoro
72933db4e9
fix(examples): harden the Jev fetched-content screening example (#5858)
Follow-up to #5833 after an independent review of the merged code.

- A malformed answer (deeply nested JSON raising RecursionError, or a
  huge noul integer raising OverflowError outside the try) escaped the
  fail-open path, and in the per-message task group it cancelled the
  other messages' requests and dropped every flag of that tool call.
  Parsing now happens inside the try, which also catches those errors.
- The 16 KiB response cap counted decoded bytes, so a compressed body
  could expand far past it first. Requests now ask for identity encoding
  and drop encoded responses.
- A plain-HTTP loopback endpoint followed HTTP_PROXY/ALL_PROXY, sending
  the bearer key and excerpt to the proxy in cleartext. Such endpoints
  now ignore proxy environment variables; HTTPS endpoints keep them.

Tests cover each case, including a real local server behind an
unreachable proxy, and pin previously untested behavior: independent
per-request deadlines, the sync hook without a task store, the >=
threshold and all four named remote tools, warnings for messages that
share a tool-call ID, consumed flags, and scanning past host messages.
The root README no longer says the excerpt is always PII-redacted, and
the example README documents the new limits and that the sync path's
deadline does not cover a stalled DNS lookup.

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-25 19:58:36 +08:00
Willem Jiang
c8d9ed1b08
chore(doc): updated the documents of the develope of Version 2.2 (#5862)
* chore(version):update the version to 2.2.0-dev

* chore(doc):Updated the CHANGLOG with the 2.1.0 release version

* chore(doc): Updated the CHANGLOG for the latest changes

* chore(doc): Added the release SOP document
2026-09-25 19:57:33 +08:00