24 Commits

Author SHA1 Message Date
IronMurphy
b5d921c255
fix(scanner): cache decoded text atomically (#6510)
* fix(scanner): cache decoded text atomically

_cache_text_file keyed the cache on the virtual path and returned the path, and
the caller serves that path as the file's content, so a write that failed
part-way left a truncated entry that every later read handed back as the preview.
Publish through a temporary file plus os.replace.

* test(scanner): pin the cache entry against a failed publish

The helper-level case cannot distinguish the call site, so this drives the real
entry point with a failing publish and asserts the cached entry is untouched.

* docs(sandbox): keep agent guidance within its size limit

* fix(scanner): preserve publish errors during cleanup

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 21:30:07 +08:00
X77
8a3986730f
fix(deploy): fail fast when DEER_FLOW_HOME is not writable (dev stack root ownership) (#6488)
* fix(deploy): fail fast when DEER_FLOW_HOME is not writable

`make docker-start` bind-mounts the host `backend/` directory into the gateway
container, whose process runs as root, so any state it creates under
`backend/.deer-flow` ends up owned by root on the host. A later `make up` runs
`scripts/deploy.sh` as the invoking user and only fails deep into the run with a
bare "Permission denied" while persisting generated secrets (deploy.sh:234).

Check writability right after `mkdir -p "$DEER_FLOW_HOME"` and, when it fails,
print the exact `chown` recovery command instead of letting the run get halfway
through. `make down` is excluded so stopping the stack is never blocked.

Fixes #6462

* test(deploy): cover runtime home writability preflight

* fix(deploy): diagnose persisted secret permissions and preserve teardown

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 20:20:22 +08:00
siye566
1cd52b576c
test(auth): make user-list ordering independent of clock resolution (#6362)
* test(auth): make user-list ordering independent of clock resolution

* docs(test): clarify deterministic user-list ordering

Explain the lexical tie-break over stored canonical UUID strings and give
user-repository ordering its own guidance section, separate from router fixtures.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 10:19:40 +08:00
haotianqi666
4946b29d03
fix(uploads): avoid case-variant filename collisions (#6466)
* fix(uploads): avoid case-variant filename collisions

* test(uploads): cover case collisions across requests

---------

Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-08 09:54:12 +08:00
Guo-Yixin
3182ecb1e4
feat(subagents): preserve RAG evidence in durable batch exports (#6413)
* feat(subagents): preserve RAG evidence in durable batch exports

* docs(subagents): clarify evidence omissions and migration head

* docs(persistence): update the migration chain head

* test(persistence): track the new batch evidence migration head

* test(persistence): verify batch evidence migration on PostgreSQL

* docs(subagents): retain evidence migration compatibility guidance

* fix(subagents): propagate cited evidence forwarding omissions

* fix(tests): adapt batch evidence PostgreSQL CI connection options

* fix(tests): preserve TLS policy in batch evidence migration fixture
2026-10-07 18:37:31 +08:00
NanPan
8598687d3d
fix(channels): keep config reload publication under lifecycle lock (#6390)
* fix(channels): keep config reload publication under lifecycle lock

* test(channels): prevent stale config publish after cancellation

* test(channels): exercise real stale reload write

* test(channels): harden cancelled config reload regression

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-07 08:26:53 +08:00
Hyeonsang Cho
efc87b6cb2
fix(deploy): let make up start on a checkout without .env files (#6370)
* fix(deploy): let make up start on a checkout without .env files

.env and frontend/.env are gitignored and make up does not create them,
but docker/docker-compose.yaml listed both as short-form env_file entries,
which Compose treats as required. On a fresh clone make up, make down and
make prod-logs all stopped with "env file .../.env not found" (exit 14)
before building or touching anything.

Mark the three entries optional with the long-form path/required syntax
the development compose file already uses. Compose loads them when they
exist and skips them otherwise; an unreadable or non-file .env still
fails. This needs Docker Compose 2.24+, the floor the README already
documents, and deploy.sh's startup-failure hint now names it for older
clients.

deploy.sh deliberately does not seed the files from the examples the way
docker.sh start does: .env.example carries placeholder API keys the
production Gateway would receive, and make config skips files that exist.

* docs(changelog): reference #6370

* test(deploy): gate Compose clients and cover make up

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-07 07:28:29 +08:00
Guo-Yixin
60ec8a65d0
fix(agents): bind read marks to the current tool call (#6363)
* fix(agents): bind read marks to the current tool call

* test(agents): make file and proxy baseline checks portable

* fix(agents): address read-mark review diagnostics and cleanup

* docs(agents): keep read-mark guidance within the chain budget
2026-10-07 07:07:53 +08:00
Guo-Yixin
52edef4f12
fix(models): exclude thinking blocks from Claude cache breakpoints (#6339)
* fix(models): exclude thinking blocks from Claude cache breakpoints

* fix(models): declare Claude test SDK and clarify caching docs

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-06 09:05:55 +08:00
Daoyuan Li
2a81f913a5
fix(projects): synchronize document character-count cache (#6315) 2026-10-06 08:09:15 +08:00
siye566
d8ca2c9358
test(community): make URL offload regressions DNS-independent (#6314)
* test(community): make URL offload regressions DNS-independent

* test(community): reject unsupported DNS fixture families

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 21:53:55 +08:00
siye566
a6c2e913b8
test(plugins): guard symlink-dependent browser asset cases (#6267) 2026-10-04 19:27:54 +08:00
siye566
f11be0a7bc
fix(sandbox): pin utf-8 decoding on the local sandbox backend's subprocess calls (#5905)
* fix(sandbox): pin utf-8 decoding on the local sandbox backend's subprocess calls

The local container backend shells out to docker / the container runtime
with text-mode subprocess calls that do not pass an encoding, so output is
decoded with the platform's preferred encoding. On hosts whose ANSI code
page is not UTF-8 (cp936 Chinese Windows, cp1252 Western European), any
non-ASCII byte in the CLI output raises inside subprocess's reader thread:
stdout/stderr come back None, the JSON-parsing call sites then crash on
None or silently mis-parse, and stderr diagnostics are dropped.

Pin encoding="utf-8" with errors="replace" on every text-mode call in
the module. The container CLIs write UTF-8 on every platform; replace
keeps the parseable portion of any non-conforming output alive instead of
discarding it wholesale.

* test(sandbox): validate UTF-8 subprocess settings per call

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-03 08:27:54 +08:00
NanHan Qing
f8b69bd84b
fix(mcp): close thread-scoped MCP sessions on thread deletion (#5956)
* fix(mcp): close thread-scoped MCP sessions on thread deletion

Deleting a thread removed its filesystem data, checkpoints, runs, events,
feedback, metadata and browser session, but never closed its persistent MCP
sessions. MCPSessionPool.close_scope() had no production caller at all, so a
recreated thread id under the same user could reuse stale stateful MCP server
state, and the leaked owner task plus subprocess kept holding memory and file
descriptors.

Add MCPSessionPool.close_thread_scope(user_id, thread_id) and call it from the
reserved deletion path under the same best-effort policy as the neighbouring
cleanups. It matches every incarnation of the thread -- the legacy user:thread
scope and any v2:[user, thread, incarnation] scope -- because a deletion cannot
reliably read the current incarnation: the record may predate incarnation
tracking, and a new incarnation can be minted concurrently. close_scope() is
unchanged for callers that already hold one exact key.

Add mcp_scope_belongs_to_thread() to own the matching rule, and hoist the v2:
prefix into a shared constant so the encoder and matcher cannot drift.

Refs #5188

* test(mcp): bind owner identity in thread cleanup checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 16:53:18 +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
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
Totoro
f8ca969330
feat(examples): add opt-in advisory fetched-content screening (#5833)
* feat(examples): add opt-in fetched-content screening extension

* fix(examples): screen sync and async results through supported middleware hooks

* fix(examples): preserve empty message IDs when applying screening warnings

* fix(examples): include screening policy in assembly identity

* feat(examples): load fetched-content screening through the extension entry

Package the screening example as a plugins: extension instead of an
operator-configured middleware. install() validates the private config
and, once enabled, contributes one TOOL_VISIBLE middleware for lead
agents and subagents. The package imports only deerflow_extension_api.

Extension tool wrappers are observational, so detection and the warning
use two supported contracts: the tool wrapper classifies the visible
result and records flagged tool-call IDs in the run's task store, and
before_model replaces the matching messages of the latest tool step by
ID. The excerpt sent to Jev is now the redacted, sanitized text the
model sees. Runs without a task store send nothing. Unexpected local
errors surface as isolation diagnostics; provider errors fail open.

Tests load the package through load_extensions() and the host isolation
wrapper and run real lead/subagent graphs with a task store bound as the
Gateway worker binds it. The README records what the example exercised
in the extension API and the gaps it found.

* fix(examples): classify each tool message of a screened result on its own

A Command result was classified as one concatenated excerpt and the flag
landed on its first text message, so a benign first result could take the
warning while an injected later result reached the model unmarked.

Each text message now gets its own bounded excerpt and Jev request, for at
most eight messages per tool call. The requests share one client and run
concurrently in a task group, each under its own deadline, so one slow or
failed request cannot hide another message's flag. Every flagged message's
tool-call ID is recorded. The message cap joins the declared policy, and the
README describes the per-message requests and data sharing.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-25 14:38:59 +08:00
Totoro
fe70cfc5ae
feat(plugins): add standalone Jev text classification example (#5735)
* feat(examples): add a standalone text classification plugin

A deployment-installed package contributes one model tool, classify_texts,
and a status action through the full-stack plugin contract. The tool labels a
list of texts against caller-supplied categories with a deployment-configured
backend: Jev (one choice question per item over a shared batch state, checked
per item) or an OpenAI-compatible chat endpoint (a JSON label list validated
in code). Items are packed into requests under a size limit and sent with
bounded concurrency; every failure is explicit per call or per item;
credentials come only from deployment-named environment variables; item text
never enters instructions, question keys, results or errors, and the served
model name is kept to a short identifier. Each batch runs under an asyncio
timeout bounded by the call deadline, so the whole call stays inside the
host's 30-second limit, and ids and category names are bounded in bytes so
300 results fit the host's 64 KiB output limit.

* docs(examples): add an opt-in live smoke test for the text classification plugin

The script loads the package through the real extension path and classifies 20
synthetic English and Chinese sentences plus a blank and an over-long item with
the configured backend, printing labels and statuses only. Against real Jev it
agreed on all 20 labels in two requests and 1.6 seconds, with the served model
reported as jev-1.13.0.

* fix(examples): measure request size as sent and cover transport failures

httpx serializes json= bodies with the json.dumps default, escaping every
non-ASCII character as six bytes, while the size estimators measured raw
UTF-8, so CJK batches could reach about twice the 256 KiB limit. The
estimators now measure the same encoding; a test with CJK categories and
texts checks the bytes the transport actually received. The network status
gets a test through connect and protocol errors, and the deadline test's
wall-clock slack is widened for loaded runners.

* fix(examples): match classifier request wire encoding

* fix(examples): bound classification output keys by JSON size

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-24 16:36:30 +08:00
YxinMiracle
b8ab097ca2
fix(models): support official DeepSeek managed profiles (#5718)
Co-authored-by: YxinMiracle <“939157765@qq.com”>
2026-09-22 17:53:21 +08:00
Totoro
32088600aa
test(sandbox): share offline search contracts across providers (#5677)
* test(sandbox): share search contracts across providers

* test(sandbox): make search provider groups explicit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-22 11:16:33 +08:00
YxinMiracle
519afe4041
fix(security): prevent external system-role message injection (#5651)
* fix(agnet): system prompt bug

* fix(security): address system-role review feedback

* fix(ci): keep agent guidance within size budget

---------

Co-authored-by: YxinMiracle <“939157765@qq.com”>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-22 10:38:16 +08:00
PeaceMaker-best
e4ce26f3d2
feat(memory): add DeerMem scope-isolation benchmark (#5564)
* feat(memory): add DeerMem scope-isolation benchmark

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

* fix(benchmark): grade persisted summaries and retry failed extractions

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-20 20:01:28 +08:00
spud
906c3d4554
fix(mcp): make durable task claims cancellation-safe (#4966)
* feat(mcp): re-scope to MCP task claim lifecycle only

Keep PR #4966 a small, closed MCP lease/cancellation state-machine change and
move RunJournal and Run lifecycle work into dedicated follow-ups. This branch
contains only the MCP task claim lifecycle:

- mcp task release/snapshot fencing by owner + per-claim lease token
- phase-level single-flight poll/cancel/notification owners with retained handoff
- routine cancellation no longer persisted as a task failure diagnostic
- bounded ordinary release ownership retention past the drain deadline
- 0018_mcp_task_lease_tokens migration + migration/bootstrap head assertions
- wait_for_task_until helper (MCP uses it); worker-specific capture helper moved
  to the run-finalization follow-up

RunJournal (journal.py + test_run_journal.py) and run lifecycle
(manager/worker/store/run sql + run tests) are preserved on
backup/cancellation-safety-full and will be raised as separate follow-ups.

* fix(mcp): unblock claims after ambiguous handoff resolves

A phase-level single-flight owner only guards an ambiguous claim outcome. Once
the claim resolves, the phase owner is released immediately; the handoff may
continue releasing returned rows as bounded, service-owned background work
(transferred to _compensation_tasks on timeout). Per-claim token fencing rejects
a late release against a newer claim generation, so a stuck release no longer
locks the whole phase until process restart.

- README: drop the stale progress-snapshot sentence from the bounded ordinary
  release description.
- service: pop the identity-checked phase owner as soon as the claim outcome is
  known, then release returned rows with the bounded path; carry the release in
  _compensation_tasks if it exceeds the drain deadline.
- mcp/AGENTS.md: document that only an unresolved claim outcome (not the handoff)
  blocks later phase scans, and that returned-row releases may continue in the
  background once the owner is released.
- tests: pin that the phase owner is released before a stuck release finishes
  while the release stays service strong-owned.

* refactor(mcp): remove unused single-record claim wrappers

_poll_one, _cancel_one, and _notify_one are unreachable in production: the
worker always processes claimed records through _run_claimed_batch, so these
wrappers preserved a second, dead single-record lifecycle (state is None)
whose only observable behavior was a wrapper-specific cancellation release.

Remove the three wrappers and migrate the regressions that guarded their
cancel/release invariants to exercise the production _run_claimed_batch path
(operation=_*_one_claimed, release=_release_*_after_cancellation). The single
wrapper-only "state is None" contract (test_poll_release_hang_without_batch)
is deleted; all 11 remaining invariants (CancelledError preservation, repeated
cancellation, poll-only token-fenced lease release, notification claimed vs
dispatched phase release, hung compensation -> service ownership, and
background compensation exactly-once observation) are now covered through the
real batch lifecycle.

* fix(mcp): fence claim-owned mutations against stale generations

The per-claim token check in the ORM release/apply paths was only in the
SELECT; the final write went out by primary key. On SQLite (where
with_for_update() is a no-op) a mutation from an older claim generation
could therefore clear a claim that a newer generation had reclaimed after lease
expiry — the exact distributed lease-fencing failure the per-claim token was
meant to prevent.

Make every claim-owned mutation a single atomic conditional UPDATE with the
owner and per-claim token in the WHERE clause (rowcount 0 => stale, return
False, no mutation):

- release_claim: atomic fence; record the poll-failure event after the fence
  wins (same transaction, holding the write lock).
- apply_snapshot / apply_cancel_snapshot: atomic fence; record the event after.
- finish_notification_run: atomic fence; use a CASE on event_version >>
  dispatch_version to keep a newer event pending for redelivery instead of
  swallowing it as delivered.

Add one regression per path: a stale generation's release/apply/finish after a
same-worker reclaim is rejected and never clears the newer claim.

* test(mcp): pin the migration chain head to the lease-token revision

0026_mcp_task_lease_tokens becomes the alembic head, so the chain-head pin in the 0025 repair test had to move on. Follow the 0023 precedent there (single head plus expected predecessor) instead of pinning a literal head, and give the new revision its own migration test, which owns the pin and covers the nullable claim-token columns on upgrade and their removal on downgrade.

* refactor(mcp): close cancellation cleanup leftovers

* fix(mcp): retain cancelled release diagnostics

* test(mcp): remove obsolete settled compensation case

* test(mcp): cover interleaved lease reclaim races

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-20 19:11:42 +08:00
yeejhyang
5e85f8a183
fix(runtime): add deterministic executor starvation regressions (#5335)
* test(runtime): add deterministic executor starvation regressions

* docs(tests): document executor starvation test contract

* fix(runtime): address executor starvation review feedback

* docs(tests): clarify executor cleanup guidance

* docs(tests): clarify executor cleanup guidance

* docs(tests): clarify executor cleanup guidance

* test(runtime): apply remaining ruff formatting fix

* test(runtime): register executor test guidance path

* test(runtime): bound dedicated executor regression wait

* test(ci): align guidance budget test with checker semantics
2026-09-12 10:59:31 +08:00