744 Commits

Author SHA1 Message Date
DanielWalnut
9654ba2c13
fix auth setup redirects (#3844) 2026-06-28 16:10:44 +08:00
yong
7ea72087bf
fix(feishu): stop creating thread topics and throttle card updates (#3810)
* fix(feishu): stop creating thread topics and throttle card updates

- Remove reply_in_thread(True) so replies appear as normal messages (#1332)
- Use chat_type=p2p for shared LangGraph thread in P2P chats
- Add two-level stream throttle (1.0s interval / 60 char buffer) with OR logic
- Add cursor indicator for streaming
- Filter values from overwriting delta text during streaming

Closes #3801

* fix(feishu): stop creating thread topics, throttle card updates, preserve clarification

- Remove reply_in_thread(True) so replies appear as normal messages (#1332)
- Use chat_type=p2p for shared LangGraph thread in P2P chats, with stored
  mapping fallback for backward compatibility with pre-upgrade threads
- Add two-level stream throttle (1.0s interval / 60 char buffer) with OR
  logic and cursor indicator for non-final outbounds
- Re-publish clarification text from values snapshots mid-stream
- Update streaming tests to the new contract (cursor glyph, clarification)

Closes #3801
2026-06-28 11:45:52 +08:00
ly-wang19
b32ee26454
fix: reap macOS nginx processes on stop (#3828)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-27 23:30:54 +08:00
Huixin615
e5a610e845
fix: ignore middleware prompts in run journal input (#3780) 2026-06-27 23:30:14 +08:00
Yufeng He
7a6c4a994a
fix(channels): serialize per-chat thread creation to avoid duplicate threads (#3799)
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
2026-06-26 15:39:57 +08:00
Yufeng He
2e789eae18
fix(skills): synchronize skill storage singleton lifecycle (#3778)
get_or_new_skill_storage() and reset_skill_storage() touch the
process-global skill storage singleton without a lock - the same
unsynchronized check-then-create that #3730 just fixed in
sandbox_provider.py, the module this file documents itself as mirroring.

Two callers racing a cold start can both see _default_skill_storage is
None and each build a SkillStorage, so the second overwrites the first;
a reset_skill_storage() racing a get can also null the global between
the None-check and the return.

Guard the build/return and the reset with a module-level threading.Lock
and a double check, mirroring get_memory_storage(). Construction stays
inside the lock (rather than sandbox_provider's build-outside-then-
discard-the-loser) because SkillStorage has no teardown hook, so a
losing racer's orphan could not be cleaned up.

Add backend/tests/test_skill_storage_lifecycle.py with concurrency
regression tests (8-thread cold-start race asserting a single instance;
reset racing gets asserting no None is returned).

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
2026-06-25 23:18:57 +08:00
Huixin615
73874a9b35
fix(mcp): isolate tool discovery failures (#3772) 2026-06-25 22:57:00 +08:00
Huixin615
b0ce4b513c
fix: allow valid heredoc commands in sandbox audit (#3786)
* fix: allow valid heredoc sandbox audit commands

* test: cover unparseable heredoc sandbox audit path
2026-06-25 22:43:28 +08:00
Willem Jiang
cb3f9ac723
feat(tui): Update AGENTS.md with the new feature 2026-06-25 22:36:52 +08:00
DanielWalnut
ef5f54c5bf
feat(tui): Hermes-like terminal workbench (deerflow) backed by DeerFlowClient (#3760)
* feat(tui): add Hermes-like terminal workbench backed by DeerFlowClient

Implements the `deerflow` TUI from RFC #3540: a terminal-native, embedded
workbench over the existing harness (no Gateway/frontend/nginx/Docker), built
Python-native with Textual and learning UX patterns from tao-pi.

Architecture — every layer except the Textual app is pure and unit-tested:
- view_state.py: ViewState + reduce(state, action), the testable heart
- runtime.py: StreamEvent -> reducer actions (pure translate + threaded driver)
- message_format / command_registry / input_history / render / theme: pure
- app.py: Textual App; runs the sync DeerFlowClient.stream() on a worker thread
  and marshals actions back to the UI thread. Slash command palette, model and
  thread modal pickers, ↑/↓ history, Ctrl+C interrupt, TTY-aware fallback.
- cli.py: pure launch-mode planning + headless --print/--json + `deerflow`
  console script (textual is an optional [tui] extra; degrades to headless help)

Web UI visibility (the RFC's key decision): persistence.py writes a threads_meta
row under the local default user into the same DB the Gateway reads, so terminal
sessions appear in the Web UI sidebar without running the Gateway. Best-effort,
no-op on the memory backend; all DB work on one long-lived background loop.

Tests: 95 TUI tests — pure layers via pytest, app/palette/overlays via Textual's
pilot harness with a fake session, and a threads_meta read/write round-trip.
ruff clean; respects the harness->app import boundary. Docs: backend/docs/TUI.md
plus CLAUDE.md/README updates and preview screenshots.

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

* fix(tui): de-duplicate streamed assistant text and tool cards; keep Tab in composer

Self-test surfaced three issues, all root-caused to consuming non-strict
streaming from DeerFlowClient (proven by the client's own
test_dedup_requires_messages_before_values_invariant, which shows the client can
re-emit a message id's full content twice):

- Assistant text was doubled (e.g. "answer answer") because the reducer blindly
  concatenated same-id deltas. Now merges by content: a re-send or cumulative
  snapshot replaces; only genuine increments append.
- Tool activity showed duplicate and empty "gear" cards from partial/re-emitted
  tool-call chunks. ToolStarted now de-dupes by tool_call_id, drops id-less
  noise chunks, and fills the name on a later chunk; a tool result with no prior
  card still surfaces as a completed card.
- Tab moved focus off the composer to the scroll region (felt like broken cursor
  logic). Tab is now consumed by the composer (completes a command when the
  palette is open, no-op otherwise).

Adds reducer tests for each case plus a Tab-focus test; 102 TUI tests pass.

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

* fix(tui): make Esc interrupt an active run (matches the status hint)

The status line advertised "esc interrupt" but Esc was only wired to close the
slash palette, so it did nothing during a run. Esc now: closes the palette when
open, interrupts the active run when streaming, and is a no-op when idle. The
interrupt logic is shared with Ctrl+C via _interrupt_run(). Adds a regression
test.

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

* fix(tui): stop prior answers duplicating on threads with history

On a thread with history, DeerFlowClient re-emits every prior message on each
new turn (its streamed_ids dedup is per-stream-call), and a re-emitted older
message can arrive after a newer message has already started. The reducer only
matched the *most recent* assistant row by id and otherwise appended, so each
re-emitted older answer was duplicated verbatim at the end of the transcript.

Match an assistant row by id anywhere in the transcript and merge in place.
Tool cards already de-dupe by call id globally, so they were unaffected.

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

* fix(tui): correct CJK cursor drift in the composer

Confirmed a Textual Input bug (latest 8.2.7): Input._cursor_offset adds an
unconditional +1 at the end of the value, overshooting by one cell after
double-width (CJK) characters. That misplaces the hardware/IME cursor — the
drift seen when typing Chinese in iTerm2 (the on-screen block cursor, drawn
separately in render_line, is fine; English doesn't use an IME so it looks
correct). Reproduced with a bare Input, so it's upstream, not our layout.

Add ComposerInput(Input) overriding _cursor_offset to the true cell position and
use it for the composer. Numeric tests pin the CJK end/mid and ASCII cases.

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

* feat(tui): render finalized assistant messages as Markdown

The transcript showed raw Markdown (literal **bold**, ## headings, - lists,
links). Finalized assistant messages now render as Rich Markdown — headings,
bold/italic, lists, inline code + code blocks, blockquotes, horizontal rules and
links — with the ● speaker marker aligned to the top of the body.

The actively-streaming message stays plain text so partial Markdown doesn't
reflow/jump, then snaps to its rendered form when the run ends. Transcript
re-renders are coalesced on a ~60ms timer (dirty flag) so per-token Markdown
re-parsing stays smooth on long threads. Tests cover both the rendered and the
streaming-plain paths.

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

* style(tui): apply ruff format

CI lint runs `ruff format --check` via uvx (latest ruff); apply the formatter so
the lint-backend job passes. No behavior change.

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

* chore(tui): address code-quality review comments

From github-code-quality[bot] on #3760:
- runtime.py: give the `_ClientLike` Protocol method a docstring body instead of
  a bare `...` (flagged as a no-effect statement), matching the harness
  convention for Protocol stubs (e.g. SafetyTerminationDetector).
- test_tui_cli_main.py: drop the unnecessary `lambda: _FakeSession()` wrappers in
  monkeypatch.setattr; pass `_FakeSession` directly (same behavior).

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

* fix(tui): keep history Markdown-rendered when a follow-up run starts

Previously the transcript rendered "the last assistant row" as plain text while
streaming. But when a follow-up turn starts, the last assistant row is the
*previous, finalized* answer until the new message begins — and the client
re-emits prior messages early in the turn — so sending a follow-up reverted the
previous answer from rendered Markdown back to raw text.

Track the actively-streaming message id in ViewState instead: it's reset on
RunStarted, set only when an AssistantDelta actually adds new content (history
re-emits are no-ops and don't mark it), and cleared on RunEnded. The renderer
keeps only that one message plain; all history stays Markdown.

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

* docs(readme): add Terminal Workbench (TUI) section to root README

Mention the new `deerflow` TUI alongside the Embedded Python Client in the root
README.md and README_zh.md (install, launch/headless commands, feature summary,
Web UI visibility), with a ToC entry and a preview screenshot. Links to
backend/docs/TUI.md for the full guide.

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

* fix(tui): address review feedback (willem-bd)

Ten findings from the TUI code review:

1. /resume was dead-ended — registered + in /help + tested as a builtin, but no
   dispatch branch. Wired it to thread resolution / the switcher.
2. --resume <title> was forwarded raw into the checkpointer (blank thread).
   Added Session.resolve_ref() to resolve id-or-title via list_threads; used by
   --resume and /resume.
3. str(get("id","")) returned "None" for an explicit id:None (truthy), defeating
   the empty-id guard so unrelated null-id tool calls collapsed into one card.
   Coerce via a None-safe helper.
4. Headless --print/--json no longer spin up the persistence loop/engine/pool
   (open_session(persistence=False)).
5. _LoopThread + engine are now closed: Session.close() (dispose engine + stop
   loop) called from a try/finally around app.run().
6. --cli --continue (and piped --cli) now run headless instead of erroring.
7. Cancelled runs no longer persist a truncated title (guard on _cancelled).
8. Palette highlight resets to the top when the filter set changes.
9. Dropped the never-populated tools count from the header.
10. Documented the `not row.error` merge guard.

Adds regression tests for each; 126 TUI tests pass, ruff check + format clean.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-25 20:10:49 +08:00
DanielWalnut
ff7ecdbd37
docs: adopt AGENTS.md as source of truth (CLAUDE.md imports via @AGENTS.md) + refresh module guides (#3770)
* docs: add root-level CLAUDE.md to orient the monorepo

Adds a thin top-level CLAUDE.md that maps the monorepo and delegates depth
to backend/CLAUDE.md and frontend/CLAUDE.md, per issue #3761.

Includes the project overview + service topology (Nginx 2026, Gateway 8001,
Frontend 3000, optional Provisioner 8002), a top-level repository map, root
`make` vs. per-module command sections, "where to go next" links to the module
guides and primary root docs, and the repo-wide cross-cutting conventions
(documentation-update policy, TDD expectation, format before pushing).

No code or behavior changes; root points down, modules own depth.

Closes #3761

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

* docs: make AGENTS.md the source of truth, CLAUDE.md a thin @AGENTS.md importer

Adopt the AGENTS.md convention so the same agent guidance serves Claude Code,
Codex, and other tools. At each level (root, backend, frontend) the content
lives in AGENTS.md and CLAUDE.md just imports it via `@AGENTS.md`.

- root: move the monorepo orientation layer to AGENTS.md; CLAUDE.md -> @AGENTS.md.
  Fix an incorrect "TUI" reference (not present on main) and repoint the module
  links to the AGENTS.md files.
- backend: move the guide to AGENTS.md (was an AGENTS.md -> @CLAUDE.md pointer;
  direction is now flipped). Refresh stale content: rebuild the full middleware
  chain (~26 ordered steps incl. InputSanitization, ToolOutputBudget,
  DynamicContext, TokenBudget, SafetyFinishReason) from the actual build
  functions; drop the brittle "11 middleware components" count; expand the
  community-tools list to the real set.
- frontend: merge the practical Next.js guide with the existing AGENTS.md's
  unique sections (LangGraph diagram, tech-stack versions, interaction
  ownership, resources) into one AGENTS.md (CLAUDE.md -> @AGENTS.md). Fix the
  stale src/ layout (remove the no-longer-present server/ better-auth entry;
  add the now-active auth/agents/blog/... modules and routes) and drop a bogus
  interaction-ownership bullet referencing files that don't exist.

Docs only; no code or behavior changes.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:15:07 +08:00
Czile
30841c3b9f
fix(sandbox): synchronize sandbox provider singleton lifecycle (+ concurrency regression tests) (#3730)
* fix(sandbox): synchronize sandbox provider singleton lifecycle

get_sandbox_provider() used an unsynchronized check-then-create, so two OS
threads (e.g. the main event loop and the Feishu channel thread, which runs
its own loop) could double-initialize the provider. With AioSandboxProvider
the overwritten instance leaks its idle-checker thread, since the only code
that joins it (shutdown()) is reachable only through the reference that was
overwritten.

reset_sandbox_provider(), shutdown_sandbox_provider() and set_sandbox_provider()
also touched the global without a lock, so a reset/shutdown racing an in-flight
create could clear it mid-construction or tear down an instance another thread
was about to return.

Guard all four lifecycle sites with a single module-level threading.Lock and
use double-checked locking in the getter, mirroring get_memory_storage().

* test(sandbox): add concurrent regression tests for provider singleton

- 8 threads racing on cold start, synchronized with a threading.Barrier so
  the check-then-create race fires deterministically; asserts exactly one
  provider instance is created.
- reset racing concurrent gets: asserts every returned value is a fully
  constructed provider (never None / half-built).

* fix(sandbox): lock get read path, run provider callbacks outside the lock

Addresses the three review findings on #3730:

1. get_sandbox_provider()'s read+return ran outside _provider_lock, so a
   concurrent reset/shutdown/set could null or tear the global between the
   check and the return, handing callers None / a torn instance. The hot read
   and the install reconciliation now both happen under the lock.

2. The non-reentrant _provider_lock was held across plugin-supplied callbacks
   (resolve_class import + provider __init__ in get; provider.reset()/shutdown()
   in reset/shutdown). A custom provider that re-entered these lifecycle
   functions would self-deadlock, and a slow teardown blocked every concurrent
   get(). resolve_class + construction now run outside the lock; reset/shutdown
   detach the reference under the lock and invoke the callback outside it.

   Tradeoff: racing cold-start callers may each construct a candidate. Exactly
   one is installed and returned to everyone; the losers (e.g. an AioSandbox
   instance that already started an idle-checker thread) are shut down so they
   do not leak the orphan thread #3721 is about. set_sandbox_provider() documents
   that it replaces but does not shut down the prior instance.

3. test_reset_racing_get reset the singleton to None *before* the barrier, so
   the racing reset was a no-op and never exercised reset-of-a-live-provider.
   It now populates the singleton up front so the reset tears down a live
   instance while getters read it.

Tests: rename the cold-start test to assert "one installed singleton, observed
by all" (construction is no longer single under the new design); add
shutdown-vs-get, set-vs-get, and a losing-racer-shuts-down-its-orphan case.
All five pass; the existing sandbox/middleware/mounts/uploads suites are green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:06:17 +08:00
Zhipeng Zheng
f656440245
fix(middleware): prevent ID-swap recursive injection and orphan peer compression (#3746)
- Add endswith('__user') guard in _is_user_injection_target to prevent
  unbounded suffix growth (id__user__user__user...) when a prior ID-swap
  peer is mistakenly treated as a new injection target
- Add peer rescue in _preserve_dynamic_context_reminders to keep
  ID-swap __user and __memory messages out of summary compression,
  preventing orphaned SystemMessage reminders and lost user context
- Add 6 regression tests covering both failure modes
2026-06-24 17:55:26 +08:00
ly-wang19
820560e576
perf(runtime): index MemoryRunEventStore events by run_id to avoid O(n) scans (#3686)
* perf(runtime): index MemoryRunEventStore events by run_id to avoid O(n) scans

list_messages was already served from a thread-wide messages projection
(#3531), but list_events and list_messages_by_run still scanned the whole
thread's event log (every run, every category) to return one run's events --
O(N_thread) on every run-scoped /messages page-load and /events request.

Add _events_by_run / _messages_by_run projections (same dict objects, kept
in lockstep in _put_one / delete_by_run / delete_by_thread), so both reads
are O(M_run), with bisect cursor pagination for messages. Semantics are
unchanged, pinned by a brute-force parity test over interleaved traces and
both cursors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: ruff format test_run_event_store_by_run_index.py

Clears the lint-backend (ruff format --check) failure on the PR; the original
commit ran `ruff check` but not `ruff format`. No behavior change (test fixture
formatting only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:47:04 +08:00
AnoobFeng
debb0fd161
feat(persistence): wire alembic migrations, bootstrap schema on startup (#3706)
* feat(persistence): wire alembic migrations + bootstrap schema on startup

Closes #3682. Pre-#3658 DBs lack the `runs.token_usage_by_model` column
because alembic was never wired up — startup only ran `create_all`,
which never ALTERs existing tables.

Adds a hybrid bootstrap in FastAPI lifespan (replaces bare `create_all`):
- empty DB     → create_all + stamp head
- legacy DB    → stamp 0001_baseline + upgrade head
- versioned DB → upgrade head

Concurrency: Postgres `pg_advisory_lock` (cross-process); SQLite
per-engine `asyncio.Lock` + 30s `PRAGMA busy_timeout` on both prod and
alembic engines. Column revisions use `safe_add_column` /
`safe_drop_column` idempotent helpers as fallback.

Other bits:
- 0001 baseline (chain root) + 0002 add `runs.token_usage_by_model`
- `include_object` filter so alembic ignores LangGraph checkpointer tables
- `make migrate-rev MSG="..."` for authoring new revisions
  (no migrate/stamp targets — startup is the only execution path)
- Tests: three-branch decision, concurrency, #3682 regression, env
  filter, blocking-IO gate anchor
- CLAUDE.md: new "Schema migrations" section

* fix(style): fix lint error

* perf(persistence): address review feedback on alembic bootstrap

Behavioural fixes
- _SQLITE_LOCKS now keyed via WeakKeyDictionary so id-reuse after GC
  cannot return a stale, loop-bound lock and the cache cannot leak one
  entry per disposed engine.
- safe_add_column compares nullable / server_default against the desired
  column when the name already exists and emits a warning on drift,
  surfacing manual-ALTER workarounds instead of silently no-op'ing.
- _postgres_lock issues SET LOCAL idle_in_transaction_session_timeout=0
  before pg_advisory_lock, so managed Postgres cannot kill the idle
  lock-holding session mid-upgrade and silently release the advisory
  lock.
- legacy branch now backfills missing baseline tables via a restricted
  create_all (Base.metadata.create_all scoped to _BASELINE_TABLE_NAMES).
  Restores pre-#1930 upgraders whose channel_* tables were never
  provisioned, without pre-empting future create_table revisions for
  newly-added models.

Schema parity
- runs.token_usage_by_model gains server_default=text("'{}'") in both
  the ORM model and the 0001_baseline create_table, matching what 0002
  adds via ALTER. create_all and alembic-upgrade paths now produce
  identical column definitions.
- New parity test compares Base.metadata.create_all output against a
  pure alembic upgrade base->head, asserting column-set, nullable, and
  server_default agree across all tables (normalized through the same
  helper safe_add_column's drift check uses).

Guards
- test_baseline_table_names_constant_matches_0001 pins
  _BASELINE_TABLE_NAMES to 0001_baseline.upgrade()'s actual output --
  the constant cannot drift silently when someone edits 0001.
- test_legacy_backfill_skips_non_baseline_tables verifies the restricted
  backfill does not create a phantom table on Base.metadata, modelling
  a future revision that would otherwise collide on op.create_table.

Doc residuals
- Three-branch decision table is now consistent across bootstrap.py
  top docstring, engine.py comment, test module docstring, and
  CLAUDE.md.
- Stale test anchor in blocking_io/test_persistence_engine_sqlite.py
  docstring now points at the real file.

* fix(style): fix lint error

* fix(persistence): close drift detection holes

- _check_column_drift compares column type via a family equivalence
  allowlist ({JSON, JSONB}). Catches the wrong-type workaround
  `TEXT NOT NULL DEFAULT '{}'` that previously slipped through silently,
  while keeping Postgres JSON/JSONB dialect reflection quiet. Reflected
  and desired type are also echoed in every drift warning's payload for
  operator triage.

- Extract _escape_url_for_alembic so bootstrap._alembic_safe_url and
  scripts/_autogen_revision share the ConfigParser % escape rule
  instead of duplicating it.

- backend/README.md: add `make migrate-rev MSG=...` to Commands and a
  Schema Migrations section per the repo's README/CLAUDE.md sync policy.

- test_base_to_dict.py: scope the test ORM class to an isolated MetaData
  so the create_all-vs-alembic parity test (added in the previous
  commit) is not polluted by the phantom table on the full pytest
  session.
2026-06-24 13:57:12 +08:00
ly-wang19
346164d740
perf(runtime): index MemoryRunStore by thread_id to avoid O(n) scans (#3562)
* perf(runtime): index MemoryRunStore by thread_id to avoid O(n) scans

MemoryRunStore is the default run backend (database.backend=memory) and backs
RunManager.list_by_thread, which calls it on every thread-runs query to hydrate
persisted runs. list_by_thread scanned every run in the store (O(total runs))
to filter by thread_id, so listing one thread's runs got linearly slower as
unrelated runs accumulated across all threads.

Add a thread_id -> insertion-ordered run_id set secondary index, maintained in
lockstep with _runs in put()/delete(), and use it in list_by_thread for an
O(runs-in-thread) lookup. This mirrors the index RunManager already keeps over
its own in-memory records (#3499); the store extraction — whose docstring notes
it is "Equivalent to the original RunManager._runs dict behavior" — did not
carry the index across.

Behavior is unchanged: same user_id filtering, newest-first ordering, and limit.
The store runs each method without awaits on the event loop, so the index and
_runs stay consistent without a lock.

Extends tests/test_persistence_scaffold.py::TestMemoryRunStore with coverage for
unknown-thread, newest-first ordering, limit, and index cleanup on delete
(including empty-bucket removal).

* perf(runtime): route aggregate_tokens_by_thread through the thread index too

list_by_thread already uses the _runs_by_thread index this PR adds, but
aggregate_tokens_by_thread (the /token-usage endpoint) still scanned every run
in the process to pick out one thread's runs. Route it through the same index
for an O(runs-in-thread) lookup, completing the thread-scoped read coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:57:45 +08:00
ly-wang19
435edbd8c6
fix(artifacts): offload blocking filesystem IO in artifact serving (#3551)
get_artifact ran its filesystem work directly on the event loop: virtual-path
resolution (os.path.abspath via .resolve()), exists/is_file probes, MIME sniffing
(mimetypes lazily stats the system MIME DB on first use), full-file
read_text/read_bytes, is_text_file_by_content (open+read), and .skill ZIP
open+extract. So serving any artifact blocked the loop for the whole read;
`make detect-blocking-io` flagged it. Same class as #3457 / #3529.

Offload each branch's IO via asyncio.to_thread: one sync helper per branch
(_load_skill_archive_member, _read_artifact_payload) folds stat + MIME + read /
extract into a single worker hop and returns a small (kind, mime, payload) plan
the handler turns into the response on the loop. FileResponse (download / active
content) keeps streaming the file itself. Behavior, branching, error codes, and
security boundaries are unchanged.

Add tests/blocking_io/test_artifacts_router.py anchor (text / binary / .skill
member), verified red->green under the strict Blockbuster gate. The gate also
caught a blocking call the static scan missed: resolve_thread_virtual_path's
.resolve() (os.path.abspath), now offloaded too.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:44:46 +08:00
Yufeng He
7be73fcf19
fix(channels): let UI runtime channel config win over config.yaml (#3674)
* fix(channels): let UI runtime channel config win over config.yaml

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* test(channels): update assertion to expect runtime/UI value to win over yaml

The test was written when yaml took precedence over runtime config.
This PR inverts that precedence so UI-entered credentials win; the
assertion now correctly reflects that runtime value (xapp-ui) beats
the yaml value (xapp) on a shared key.

---------

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
2026-06-24 10:24:36 +08:00
Eilen Shin
cc1df2d038
refactor: share one message->text helper for journal + thread messages (#3747)
Part of #3742. RunJournal._message_text and the gateway thread-messages
helper (thread_runs._message_text) reimplemented the same 'extract display
text from a message' logic — str / list of string|{text}|nested{content}
blocks joined without a separator / mapping with text|content key. They
differed only in two ways: journal reads a BaseMessage attribute while
thread_runs reads dict-shaped run_events rows, and journal falls back to
message.text.

Add deerflow.utils.messages.message_to_text(message, *,
text_attribute_fallback=False) that handles both message shapes (attribute
or mapping content access) and gates the .text fallback behind a flag, and
have both call sites delegate. journal passes text_attribute_fallback=True;
thread_runs uses the default. Behavior is unchanged at both sites.

Verified behavior-preserving with an equivalence harness running both
original implementations vs the shared helper over 98 inputs (BaseMessage
and dict messages; str/list/mapping/None/numeric content; mixed blocks;
.text attribute present/absent/non-str) -> 0 mismatches. Added
tests/test_utils_messages.py; the journal last_ai_message extraction tests
still pass.
2026-06-24 09:28:43 +08:00
Eilen Shin
67dd75db62
chore: remove unused private helper functions (dead code) (#3749)
These three module-private helpers have no callers anywhere in the repo
(verified by a repo-wide identifier scan + direct greps; each appears only
at its own definition):

- agents/memory/updater.py: _create_empty_memory (a no-caller wrapper around
  storage.create_empty_memory, which is used directly elsewhere)
- runtime/runs/worker.py: _extract_human_message (also drops the now-unused
  HumanMessage TYPE_CHECKING import and TYPE_CHECKING from typing)
- sandbox/tools.py: _looks_like_unsafe_cwd_target (cwd safety is enforced via
  _is_allowed_local_bash_absolute_path)

Underscore-prefixed = module-internal, so zero references is conclusive.
Route handlers and @compiles-registered functions were excluded as false
positives. Public-surface candidates are noted in the issue, not touched here.

Fixes #3748.
2026-06-24 09:21:50 +08:00
Zhipeng Zheng
fde6885aea
fix(agents): coalesce SystemMessages before LLM request (#3711)
DynamicContextMiddleware (PR #3630, p0) uses the ID-swap technique to
inject a SystemMessage(reminder) into the middle of the conversation.
create_agent then prepends the static system_prompt as another
SystemMessage at request time, so strict OpenAI-compatible backends
(vLLM, SGLang, Qwen) and Anthropic reject the request with
'System message must be at the beginning'.

New SystemMessageCoalescingMiddleware runs in wrap_model_call — after
create_agent prepends system_prompt — and merges every SystemMessage
into a single leading one before the request reaches the provider.
Non-system messages keep their original order; the merged SystemMessage
preserves the id of the first system message. Only the request payload
is touched; checkpoint state is unchanged, so every consumer that scans
history (memory builder, journal, summarization, dynamic-context
detection) keeps working.

Mirrors the per-request coalescing already done for Claude in
claude_provider._coalesce_system_messages (PR #3702) but at a
provider-agnostic layer so every backend benefits from a single fix.

Closes #3707
2026-06-24 07:28:17 +08:00
Yufeng He
cefc53c72a
fix(middleware): fix positional fallback consuming unrelated todo when same-content list is exhausted (#3709)
* fix(middleware): fix positional fallback consuming unrelated todo when same-content list is exhausted

When next_todos contains the same content string twice (e.g. two "A"
entries), the first iteration pops the matching previous todo from
previous_by_content["A"], leaving an empty list. The second iteration
finds that empty list, treats it as falsy, and sets previous_match=None.
The positional fallback then fires unconditionally — consuming
previous_todos[index] even if it holds a completely different entry
(e.g. "B"). That marks "B" as matched, so the final loop never emits
a todo_remove action for it.

Fix: guard the positional fallback with `content not in previous_by_content`
so it only fires for genuinely new content (the key was never in previous),
not for same-content entries whose list has simply been exhausted.

Add a regression test to _build_todo_actions covering this exact case.

* style: ruff-format the token usage middleware test

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

---------

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-23 23:37:41 +08:00
Huixin615
4f192cb469
fix: align sandbox artifact mounts with channel user (#3729)
* fix: align sandbox artifact mounts with channel user

* fix: clean up sandbox user-scoped ids
2026-06-23 23:13:12 +08:00
Huixin615
1ac9c9edee
fix: preserve langgraph resume command (#3732) 2026-06-23 22:22:07 +08:00
Vibhu Dixit
caf54938db
fix(gateway): propagate thread_id for LangGraph context API (#3735) 2026-06-23 16:18:43 +08:00
ly-wang19
b66e3253a0
perf(config): index AppConfig.get_*_config by name (O(n) -> O(1)) (#3688)
get_model_config / get_tool_config / get_tool_group_config did a next(...)
linear scan of self.models / self.tools / self.tool_groups on every call.
These sit on hot paths: get_tool_config runs 2-3x per community-tool
invocation (web_search etc.) and get_model_config several times per agent
build, across 40+ call sites.

Build name -> config dicts once in a mode="after" validator (stored as
PrivateAttr), so each getter is an O(1) dict lookup. A config reload
constructs a fresh AppConfig, which rebuilds the indexes; setdefault keeps
first-match-wins on duplicate names, matching the prior next(...) semantics.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:39:40 +08:00
ly-wang19
f956682f31
perf(runtime): resolve SSE resume offset in O(1) via the event-id seq (#3700)
MemoryStreamBridge._resolve_start_offset scanned the retained event buffer
(up to queue_maxsize=256 entries) on every subscribe/reconnect carrying a
Last-Event-ID. Event ids are "{ts}-{seq}" where seq is a per-run monotonic
counter that equals the event's absolute offset, so the offset is computable
arithmetically. Parse seq, index into the buffer, and verify the id matches
exactly -- a stale/evicted/foreign/malformed id falls back to
replay-from-earliest, identical to the previous scan.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:25:18 +08:00
Zhipeng Zheng
bf70280319
fix(memory): filter hide_from_ui HumanMessages from memory builder (#3697)
Middleware-injected hidden messages (TodoMiddleware.todo_reminder,
ViewImageMiddleware, p0 DynamicContextMiddleware.__memory) were being fed
to the memory-updating LLM as if they were real user input, polluting
long-term memory with framework-internal text. The p0 __memory payload
could also trigger a self-amplification loop.

Fix: skip HumanMessages with additional_kwargs['hide_from_ui'] in
filter_messages_for_memory, consistent with the frontend logic.

Closes #3695
2026-06-23 10:02:38 +08:00
Yufeng He
8167275cef
fix(memory): skip whitespace-only facts in _apply_updates (#3719)
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
2026-06-23 08:34:41 +08:00
Eilen Shin
f7f2a500d1
perf(sandbox): cache local-path masking patterns instead of recompiling per match (#3713)
mask_local_paths_in_output runs once per glob/grep match (tools.py:1540,
:1625) and once per bash/ls output, and each call rebuilt every skills /
ACP / user-data masking regex from scratch — Path.resolve() syscalls +
re.escape + re.compile. A grep returning up to 100 matches recompiled the
same patterns 100x.

Compile the host->virtual patterns once per ordered source set via
functools.lru_cache (keyed on the config-stable + per-thread (host,
virtual) pairs, so it self-invalidates when they change) and collapse the
three byte-identical replacer blocks into one applier. Behavior is
unchanged: same patterns, same application order, same per-thread mappings.

Verified behavior-preserving with an equivalence harness (original vs
refactored logic over 54 inputs incl. slash-style variants, base-only
matches, subpaths, overlapping mappings, missing skills/ACP, thread_data
None) -> 0 mismatches. Added cache/consistency tests alongside the
existing masking tests.

Fixes #3712.
2026-06-23 08:32:51 +08:00
Tianye Song
b990da785f
feat(memory): add guaranteed injection for correction facts with graceful fallback (#3592)
* feat(memory): add guaranteed injection for correction facts with graceful fallback

When the token budget is tight, high-value facts (e.g. user corrections)
can be silently evicted by lower-priority regular facts. This change:

- Introduces configurable 'guaranteed_categories' (default: [correction])
  whose facts draw from a separate 'guaranteed_token_budget', ensuring
  they are never dropped due to budget pressure.
- Adds a graceful fallback to confidence-only ranking when the
  guaranteed-category path raises an unexpected exception.
- Refactors fact selection into a header-agnostic helper
  (_select_fact_lines) with explicit token accounting in the caller,
  eliminating double-counting of separators.
- Emits a single 'Facts:' header regardless of whether both guaranteed
  and regular facts are present.
- Extends the final safety truncation limit to account for the
  additional guaranteed budget so guaranteed facts survive end-to-end.

* refactor(memory): address review feedback on guaranteed injection

- Restore strict break-on-overflow in `_select_fact_lines` to preserve
  the caller's confidence-ordered ranking; add a regression test locking
  in the invariant that a shorter lower-confidence fact never slips
  ahead of a skipped higher-confidence one.
- Account for the inter-group `\n` separator between guaranteed and
  regular fact blocks in the regular budget (1-token precision fix).
- Clarify docstrings on `format_memory_for_injection` and
  `MemoryConfig.guaranteed_token_budget` to distinguish the common
  *displacement* case (total stays within `max_tokens`) from the rarer
  *additive* case (safety-truncation ceiling raised when guaranteed
  lines alone would overflow).

* fix(memory): address P1 safety truncation + P2s from review

- Structure-aware safety truncation: Facts block is now a protected
  suffix so guaranteed-category facts can never be silently discarded
  by a prefix-cut on overflow. Only the preceding (user/history)
  sections are eligible for truncation.
- Extend the same protected-suffix treatment to the except/fallback
  path by returning fact lines alongside the formatted section from
  _fallback_format_facts, avoiding string parsing.
- Single inter-section separator: facts section no longer embeds its
  own leading \n\n; the final "\n\n".join(sections) is the single
  source of truth for section-to-section spacing.
- Bare string for guaranteed_categories now raises TypeError instead
  of silently iterating single characters.
- Category-less / malformed facts no longer default-promote into the
  guaranteed "context" pool — only facts with an explicit category
  field qualify.
- Lift valid_facts pre-filter outside the try so the fallback path
  reuses it instead of re-doing validation work.
- MemoryConfigResponse + DeerFlowClient.get_memory_config now expose
  guaranteed_categories / guaranteed_token_budget.
- config.example.yaml: document the two new fields and bump
  config_version from 12 to 13.
- Add regression tests for every finding.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-23 08:10:12 +08:00
Huixin615
90fdda77e8
fix(runtime): skip hidden human messages in journal (#3698)
Co-authored-by: rayhpeng <rayhpeng@gmail.com>
2026-06-23 06:58:31 +08:00
kapil971390
d050fba07f
fix(token-budget): acquire _lock in _apply, before_agent, and _drain_pending_warnings (#3714)
_lock was defined in __init__ and used only in reset(), leaving _apply(),
before_agent(), _clear_run_state(), and _drain_pending_warnings() unprotected.
Concurrent after_model callbacks for the same run_id could race on
usage_accum.input += diff_input, causing lost-update on token counters.

Also fix stray bare 'a' character in frontend/src/content/zh/introduction/index.mdx
that rendered as visible text on the documentation page.

Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-22 20:44:07 +08:00
Nan Gao
5ffc9a1cc7
test(agents): multi-turn message-stream invariants (graph integration) (#3708)
* test(agents): multi-turn message-stream invariants (graph integration)

Add a graph-integration net for the class of bug behind #3684: build a real
create_agent graph with DynamicContextMiddleware plus a checkpointer, drive two
user turns on one thread with a deterministic fake model and memory injection
stubbed on, then assert the message stream stays well-formed -- the newest user
message is the latest human turn, no duplicate ids, no __user__user suffix
explosion.

Runs at unit speed in `make test` (backend-unit-tests), with no gateway, SSE,
fixtures, or API key. Verified red on the pre-fix middleware and green after.
Catches this class earlier than e2e replay, which disables memory, uses a
single-turn golden, and asserts SSE shape only.

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

* test(agents): address review — semantic-first asserts, _STREAM_MIDDLEWARES, DRY

- Check the semantic invariant (newest user message is the latest human turn)
  before the structural id checks, so a regression surfaces as a meaning-level
  failure; verified it now fails first on the pre-#3685 middleware.
- Add module-level _STREAM_MIDDLEWARES (the docstring referenced it; it did not
  exist) so the net is trivially widened with more state-touching middlewares.
- _last_human_text reuses _msg_text instead of re-implementing content flattening.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:55:20 +08:00
Nan Gao
2f9ceacb98
fix(agents): skip dateless reminders in dynamic-context date scan (#3685)
* fix(agents): skip dateless reminders in dynamic-context date scan

_last_injected_date returned at the first message flagged
dynamic_context_reminder regardless of whether it carried a
<current_date>. Since #3630 split injected context into a date
SystemMessage plus a separate dateless <memory> HumanMessage, the
reverse scan hit the memory message first and returned None, making a
later turn look like the first turn. The middleware then re-injected,
re-targeted the previous turn's __user message via the ID swap, and left
the stale message as the latest human turn -- so the model re-answered
the previous message and the persisted message stream got scrambled.

Skip flagged reminders without a <current_date> so the scan reaches the
real date SystemMessage. Add a regression test for the 2nd-turn +
memory-enabled scenario.

Fixes #3684

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

* fix(agents): make injected reminder date authoritative via metadata

Promote the dynamic-context date out of message content into
additional_kwargs["reminder_date"] on the date SystemMessage.
_last_injected_date reads that key instead of regex-parsing content, with a
SystemMessage-scoped content fallback for checkpoints written before the key
existed.

This removes two structural fragilities raised in review of #3685:
- the boolean dynamic_context_reminder flag no longer has to also mean
  "carries a date" -- dateless reminders simply lack reminder_date and are
  skipped, so a future reminder type cannot reproduce the shadowing bug.
- the date regex no longer runs on the user-influenceable memory HumanMessage,
  so a memory fact containing a literal <current_date> cannot spoof the
  injected date (false same-day skip / false midnight crossing).

Add regression tests for memory date-spoofing, structured-date stamping, and
legacy (no-key) detection; update existing date-reminder fixtures to the
production shape.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:25:34 +08:00
ly-wang19
9535a4f1c2
perf(subagents): dedup streamed AI messages via a seen-id set (O(n^2) -> O(n)) (#3687)
_aexecute collects AI messages from agent.astream(stream_mode="values"),
which re-yields the full state every super-step. The duplicate check rescanned
the append-only ai_messages list on every chunk -- any(m["id"] == message_id) --
so a run with M messages did O(M^2) work, and M reaches max_turns=150 for the
general-purpose / deep-research subagent.

Track an id-keyed set alongside ai_messages: id-bearing messages become O(1)
set lookups, and the id-less full-dict-compare fallback is preserved. Behavior
is unchanged.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:31:27 +08:00
AnoobFeng
c495736f0a
fix(middleware): prevent title middleware from streaming tokens (#3566) 2026-06-21 22:30:35 +08:00
Chetan Sharma
78fff5a5e2
feat(middleware): add TokenBudgetMiddleware for per-run token budget e… (#3412)
* eat(middleware): add TokenBudgetMiddleware for per-run token budget enforcement

* address copilot comments

* resolve feedback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 22:25:15 +08:00
ly-wang19
c177d0e542
fix(config): coerce null object config sections to their defaults (#3573)
* fix(config): coerce null object config sections to their defaults

#3434 made commented-out list sections (models/tools/tool_groups) parse as []
instead of crashing, but the same scenario still crashes for object sections:
commenting out every key under e.g. memory: / summarization: / guardrails:
makes PyYAML parse the value as None, and AppConfig then raises "Input should
be a valid dictionary" for that section — breaking the documented
`cp config.example.yaml config.yaml` first-run flow.

Generalize the handling: a model_validator(mode="before") drops None-valued
sections so each field falls back to its default (list sections -> [], object
sections -> their default config). This subsumes the previous list-only
field_validator and the database special-case. Required sections without a
default (sandbox) still error when null.

Adds test_app_config_coerces_commented_out_object_sections; the existing
list-section regression test still passes.

* docs+test: address review on null-section coercion

- Correct the _drop_null_config_sections docstring: it does not subsume the
  database special-case; _apply_database_defaults still owns `database` and
  applies concrete defaults beyond null-coercion.
- Strengthen test_app_config_coerces_commented_out_object_sections to assert
  each null section falls back to its expected default config type, not just
  that it is non-None.
- Add test_app_config_null_required_section_still_errors covering the
  required-section (sandbox) "still errors when null" claim.

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-21 22:16:00 +08:00
Willem Jiang
0d732b65dd fix the unit test error after merging #3651 2026-06-21 21:43:43 +08:00
Eilen Shin
9c62420d67
fix(gateway): attach thread-message feedback by real event_type (#3651)
list_thread_messages matched event_type == "ai_message" to find each
run's last AI message, but RunJournal stores AI messages as
"llm.ai.response" and the event store returns that verbatim. No code
writes "ai_message", so the match never hit: feedback was never attached
(every message returned feedback=null) and the grouped-feedback query ran
on every request for nothing.

Match "llm.ai.response", and only run the grouped-feedback query when the
thread actually has an AI message to attach it to. Adds a regression test
for the per-run attachment and the no-AI-message lazy-query path.

Fixes #3650.
2026-06-21 21:11:40 +08:00
Zhipeng Zheng
c0ce759763
fix(security): inject system context as SystemMessage for role isolation (#3630) (#3661)
P0: DynamicContextMiddleware role isolation — framework data as SystemMessage,
memory as HumanMessage (OWASP LLM01), user input unchanged.

Includes E2E hash stabilization: frontend hide_from_ui filter fix in
suggest_agent, backend _canonical_messages() hide_from_ui skip, fixture
hash updates.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 20:56:42 +08:00
Zheng Feng
25e33b1927
chore(deps): sync uv.lock with groundroute extra (#3678)
The deerflow-harness pyproject.toml declares a groundroute optional
extra (added in #3675), but uv.lock provides-extras was left out of
sync. Regenerate the lock so the declared extra is recorded.
2026-06-21 19:09:27 +08:00
Zhipeng Zheng
e7b88a97ed
fix(security): add input sanitization middleware for prompt-injection defense (#3630) (#3662)
InputSanitizationMiddleware: escapes blocked XML tags in user messages (<system> -> &lt;system&gt;) — de-identify, don't reject (AWS PII-style). Wraps user input in plain-text boundary markers (--- BEGIN/END USER INPUT ---) per OWASP structured-prompt guidance. System-Context Confidentiality clause prevents LLM from revealing internal instructions. Boundary-marker layer hardened against delimiter injection (self-suppression + break-out). Multimodal content blocks preserved during sanitization.

Addresses review feedback from @fancyboi999 and @WillemJiang:

1. Boundary-marker injection: strict startswith+endswith idempotency, neutralize user-supplied BEGIN/END tokens, forged-idempotency bypass fixed

2. Multimodal data loss: _rebuild_content preserves interleaved non-text blocks

3. CI fixes: replay_provider strips boundary markers before hashing, middleware chain assertions updated

4. Lint: ruff format applied on Linux (slice spacing + f-string collapse)

Test: 71 input_sanitization + 1 replay_golden + 14 tool_error + 93 tool_output_budget = 179/179 passed
2026-06-21 19:01:43 +08:00
ly-wang19
a09f9668a5
fix(persistence): offload sqlite dir creation off the lifespan event loop (#3574)
init_engine runs on the FastAPI lifespan event loop, but created the SQLite
data directory with a synchronous os.makedirs (a stat + mkdir syscall),
blocking startup. Dispatch it via asyncio.to_thread, mirroring the #1912 fix
for the checkpointer's ensure_sqlite_parent_dir.

Adds a Blockbuster-gated regression test in tests/blocking_io/ that drives the
real init_engine path with a not-yet-existing sqlite_dir; it trips
BlockingError if the makedirs regresses onto the event loop.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-21 17:43:49 +08:00
Miracle778
5a699e24a1
feat(guardrails): expose authenticated runtime context in GuardrailRequest (#3665)
* docs: guardrail runtime attribution spec

* docs: guardrail request attribution implementation plan

* feat(guardrails): add runtime user context and attribution fields to GuardrailRequest

Extend GuardrailRequest with optional runtime attribution fields so that
pluggable GuardrailProviders can access authenticated user context and
tool-call-level attribution:

- Gateway injects user_role, oauth_provider, oauth_id into runtime context
  alongside the existing user_id (server-authenticated only, client spoofing
  prevented)
- GuardrailRequest gains: user_id, user_role, oauth_provider, oauth_id,
  run_id, tool_call_id (all optional, backward compatible)
- GuardrailMiddleware reads these from ToolCallRequest.runtime.context
- thread_id now actually populated from context (was always None before)
- Tests: 15 new/expanded tests covering Gateway injection, runtime context
  reading, partial/missing fields, and client spoofing prevention
- Docs: new Runtime Attribution section in GUARDRAILS.md with provider
  example and YAML policy illustration

* fix(guardrails): propagate attribution to subagents

* fix(guardrails): complete subagent attribution propagation

---------

Co-authored-by: Miracle778 <miracle778@no-reply.com>
2026-06-21 16:08:25 +08:00
jp0xz
a6dd2876f0
feat(community): add GroundRoute web search + fetch engine (#3675)
* feat(groundroute): add GroundRoute community web_search + web_fetch tools

GroundRoute is a meta search layer over six engines (Serper, Brave, Exa,
Tavily, Firecrawl, Perplexity) with price-based routing and failover. This
adds a self-contained community engine module (httpx only, no new required
deps) mirroring community/brave + community/tavily:
- web_search: POST /v1/search, normalize to {title,url,snippet,source_engine}.
- web_fetch: fetch a URL via mode=page.
- unit tests covering normalization, auth, clamping, and graceful errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(groundroute): register GroundRoute search + fetch in wizard and config

Add GroundRoute to the setup wizard provider lists (SEARCH_PROVIDERS +
WEB_FETCH_PROVIDERS) and as commented web_search + web_fetch examples in
config.example.yaml, mirroring tavily/serper/brave so SEARCH_API can select it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(groundroute): apply repo ruff format (line-length 240)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(groundroute): add GroundRoute to tools docs and config reference

Adds GroundRoute as a web_search and web_fetch option in the en + zh
tools.mdx pages (new tab alongside Tavily/Brave/Exa/etc.) and documents
GROUNDROUTE_API_KEY in CONFIGURATION.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(groundroute): define empty groundroute extra for clean install

The docs install line 'uv add deerflow-harness[groundroute]' (mirroring the
tavily/exa/firecrawl pattern) referenced an undefined extra, which uv accepts
but warns about. GroundRoute needs no extra packages (httpx is a core dep), so
declare an empty 'groundroute' extra in deerflow-harness optional-dependencies
so the documented command resolves without a warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(groundroute): per-tool api key + honor caller max_results (review)

Address maintainer review on PR #3675:
- _get_api_key(tool_name): web_fetch now reads the web_fetch config block's key
  instead of always web_search, so a flow that pairs GroundRoute fetch with a
  different search engine authenticates correctly. Mirrors serper/exa/firecrawl.
- web_search honors a caller-supplied max_results (sentinel default None),
  falling back to the configured value only when omitted, so the documented
  parameter is no longer silently discarded.
- warn-once is now keyed per tool. Tests cover both fixes (web_fetch key,
  agent max_results honored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 15:55:10 +08:00
Zheng Feng
ee8ad1bc67
feat(auth): add OIDC SSO support (#3506)
Add provider-agnostic OIDC authentication with Keycloak-compatible configuration, frontend SSO UI support
2026-06-21 15:47:53 +08:00
Chetan Sharma
4572217038
feat: persist AI turn duration in backend and UI (#3663)
* feat: persist AI turn duration in backend and UI

* chore: restore uv.lock to match main

* refactor(frontend): use Math.floor instead of Math.ceil for reasoning timer
2026-06-21 09:35:45 +08:00
Recep S
9072075311
feat: add fastCRW provider (#3585)
* feat: add fastCRW provider

* test(fastcrw): fix env isolation and cover error, no-content, and env-fallback paths

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 09:30:55 +08:00