* fix(subagents): terminalize a background cancel that beat the loop
request_cancel_background_task() calls Future.cancel() on the future handed
back by _submit_to_isolated_loop_in_context(). That call only wins while the
coroutine has not been picked up yet; when it does win, run_with_timeout never
starts, so no one writes a terminal status and no one opens the cleanup fence.
The registry entry then stayed PENDING forever: task_tool polled its full
budget for a result that could not arrive, and cleanup_background_task()
refused to drop a non-terminal entry.
Publish CANCELLED from the future's own completion callback when the coroutine
body never ran. A cancellation that did reach a running coroutine keeps its
existing owner -- the future reports cancelled() for that case as well, which
is why the callback checks a start marker instead of cancelled() alone.
* fix(subagents): publish a pre-start cancel from the isolated loop
`Future.cancel()` on the future returned by
`_submit_to_isolated_loop_in_context()` fires `forget_future` in the calling
thread, but the loop keeps owning the coroutine until its first step runs. A
cancel that lands in that window read the start marker as clear and published
CANCELLED — opening the cleanup fence — while the worker could still take its
capacity slot.
Hand the decision to the isolated loop and re-check the marker there, which is
the only place serialized against that first step. A loop that is gone cannot
start the coroutine, so the inline fallback keeps the original guarantee.
* test(subagents): accept both terminal owners in the task-created cancel window
Task.__init__ queues the first step inside the task factory, so the step can
run before the deferred pre-start decision that was handed to the loop. When
it does, the coroutine's own CancelledError branch legitimately publishes
"Cancelled by user"; the harness had pinned the pre-start message instead.
Assert the invariants both owners share -- terminal cancelled, fence opened
only after the loop settled the body, cleanup removes the entry -- and let
either error message pass.
* fix(subagents): refuse to start a cancelled execution before taking a slot
A cancel that wins the race against the loop is decided on the loop, one turn
after the submission callback queues the wrapper's first step. That turn is not
a guarantee: the wrapper could still start, create the execution task, and take
a capacity slot after the terminal status and the cleanup fence had already been
published from that decision.
request_cancel_background_task sets cancel_event before it cancels the
future, so the wrapper can treat that event as the startup half of the handshake
and decline to create the execution task. The fence then cannot be outrun by a
worker that acquires capacity afterwards.
* test(subagents): hold the cancel window open with an event barrier
The factory blocked the loop for a fixed 200 ms and then resumed on its own. A
caller delayed longer than that -- a loaded CI worker is enough -- let execution
start before the cancel landed, and `assert not execution_ran.is_set()` failed
even though cancellation was handled correctly.
Block the factory on an event instead and release it from the test's `finally`
after the cancel request and the closed-fence assertions, so the window stays
open exactly as long as the test needs it.
---------
Co-authored-by: gouyu-hou <213296172+gouyu-hou@users.noreply.github.com>
* fix(middlewares): prevent todo retries after text-only output caps
* docs(middlewares): clarify text-only output cap markers
* test(subagents): isolate cleanup test from shared assembly pool
* docs(middlewares): keep cap guidance within chain budget
---------
Co-authored-by: linkangjun <331227811+linkangjun@users.noreply.github.com>
* fix(harness): keep a cancelled batch cancelled when retrying a failed item
retry_item only kept a paused batch as it was, so retrying a failed item
of a cancelled batch reset the batch to queued and the worker claimed and
ran the item. A cancelled batch now rejects the retry (409), re-checked
under the write lock because SQLite ignores FOR UPDATE, and the workspace
no longer offers Retry there.
* docs(changelog): reference #7792 for the cancelled batch retry fix
scan_skill_content and the goal evaluator built their chat model inline on
the Gateway event loop. When the provider reads local credential files (the
Codex provider reads the Codex CLI auth.json while it is constructed), each
scanned skill edit, rollback, install or agent-managed skill write, and the
evaluator construction in each goal run, did synchronous file IO on the
shared loop, so other requests and streams waited for it. #7484 removed the
same stall for the one-shot helpers and listed these two constructors in the
utils guidance as not covered.
Build both providers through run_assembly, as the one-shot helpers do: the
moderation model in scan_skill_content, the evaluator in
evaluate_goal_completion when no model is injected, and the run worker's
cached evaluator factory. A scan without app_config (the in-graph
skill_manage call) also reads the live config in that pool, and its
fail-closed fallback reuses that config instead of reading config.yaml again
on the loop; an unreadable config still fails closed. Verdicts, the
fail-open/fail-closed policy, error propagation and tracing are unchanged,
and cancellation during construction never starts inference.
tests/blocking_io/test_system_model_offloop.py builds a real CodexChatModel
under the strict Blockbuster gate. On main the scan fell back to its
fail-closed "unavailable" block, the evaluator raised BlockingError
(os.stat), and the worker stood the goal down as evaluator_failed.
* fix(agents): store an explicit null description as empty
PUT /api/agents/{name} accepted "description": null and wrote it into the
agent's config, but AgentConfig.description is a str. Every later load of
that agent failed validation: the PUT itself, GET and repair PUTs returned
500, the file backend dropped the agent from the list, and the db backend
failed the owner's whole list.
Store an explicit null as the empty default, and have parse_agent_config
read a stored null description (already written by this route, or a bare
YAML key) as empty so affected agents load again.
* docs(changelog): reference #7638 for the agent null description fix
* docs(agents): document that a null description resets it to empty
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Loop detection, clarification and dangling-tool-call repair parse model-provided
tool arguments and options. A JSON string, Python structure or native option
label nested past the interpreter recursion limit raised RecursionError and
aborted the run; these paths now treat it like any other unusable payload
(opaque args, an empty replay object, or a skipped option), and the dict-option
flatten and _stable_tool_key use guarded serialization.
Rebased onto current main; the stale-base docs commit is superseded by main and
the AGENTS note is trimmed to stay inside the guidance chain budget.
* fix(runtime): charge a steered subagent's usage to the token budget
A task that finishes after a Steer reports through a receipt HumanMessage, so its usage never reached the dispatching AIMessage the token budget reads. Carry the usage over when the receipt is delivered.
* fix(runtime): report deferred usage with no AIMessage to charge
Log the rare case instead of dropping the usage silently, and scan for AIMessages once per delivery.
* fix(runtime): resume children paused for a withdrawn steer
wait_tool pauses every child as soon as a steer is pending, but only a
delivered steer sets steering_active, the flag finish_graph needs before it
resumes them. Removing the steer before delivery left the children parked
and the run waiting on them.
Release the children when deliver finds no input and no steer is in
progress. They stay paused during later model turns of a delivered steer.
* fix(runtime): release children paused by a steer withdrawn mid-turn
The steering_active guard skipped the release whenever another steer was
being handled. A child started during that turn is outside its
interrupted_children snapshot, so nothing resumed it after a second steer
was withdrawn and finish_graph waited forever.
Release every child a delivered steer does not hold. Each deliver re-takes
that snapshot, so a paused child outside it can only belong to a withdrawn
steer.
* fix(runtime): keep a replacement reusing a call ID out of the held steer
interrupted_children stores call IDs, and child() can replace a closed gate under the same ID. The replacement then looked held by an earlier delivered steer, so a withdrawn steer left it paused forever in a replaced turn. Drop the ID when its gate is replaced.
* fix(memory): tolerate unbounded stored fact confidence
A stored memory fact can carry a JSON integer such as 10**400. float() on it
raises OverflowError in _coerce_source_confidence and in the dedup merge
comparison, aborting the whole update cycle even though both sites already
handle every other unusable confidence. Treat the overflow like a missing or
non-finite value: 0.5 for ranking, 0.0 for the merge comparison.
* fix(memory): harden the remaining fact-confidence coercions
The import normalizer, prompt builder and relevance scorer each coerce a
stored fact confidence with float() and caught only TypeError/ValueError, so
an unbounded JSON integer such as 10**400 raised OverflowError: import failed
before the updater fallbacks ran, and prompt/relevance ranking could abort.
Catch OverflowError and take the existing defaults; document the full set of
coercions.
* fix(mcp): keep a tool argument named runtime out of LangGraph's runtime injection
The pooled stdio wrapper and the background-submit wrapper took LangGraph's
ToolRuntime in a parameter called `runtime`. ToolNode injects by that name, so a
tool whose own input schema has a `runtime` property had the model's value
replaced by the ToolRuntime and then lost: the server received the other
arguments only, and TaskSubmitRequest.arguments missed it too.
Name the injected parameter `deerflow_runtime` in both wrappers (it is still
detected through its ToolRuntime annotation) and read it under that name in the
personal-MCP ownership guard. Existing tests that call the wrappers pass the new
name; two ToolNode tests cover a `runtime` business argument on each path.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(mcp): read the injected runtime under the right name in the personal-tool guard
The personal-tool ownership guard read kwargs["deerflow_runtime"], but only
the pooled stdio wrapper was renamed; personal HTTP/SSE tools still carry
langchain-mcp-adapters' `runtime` parameter, so the guard never saw the
authoritative runtime user for them. Look the parameter name up per tool, and
update the mcp AGENTS.md note on how the runtime is injected.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(clarification): keep each question when a provider reuses a call ID
The clarification ToolMessage ID, which is also the human-input
request_id, was clarification:{tool_call_id}. Providers that number
calls per response reuse call_0 across turns, so a later question took
the earlier one's ID: add_messages replaced the earlier question in
place and left the new call without a result.
Add a digest of the issuing AIMessage ID. A retry of the same call comes
from the same AIMessage and keeps its ID; without that message in state
the ID keeps its previous form.
* docs(changelog): reference #7762 for the clarification call ID fix
* fix(agents): ignore malformed entries in a serialized MCP routing index
`McpRoutingMiddleware._normalize_index` documents that it accepts "arbitrary
serialized routing data, not only the output of the builder", but two shapes
escaped the coercion and raised instead:
- an entry that is not a mapping (`str`, `list`, `None`, a number) reached
`.get` and raised `AttributeError`;
- a non-finite `priority` raised `OverflowError` from `int()` — JSON accepts
`Infinity`/`-Infinity` literals, so a hand-edited or foreign index can carry
one.
The middleware is constructed per run, so either shape fails every model call
in that process instead of the entry being ignored the way an entry with no
keywords already is.
Both now degrade to "not indexable": a non-mapping entry is skipped, and a
priority that cannot be a finite int falls back to 0 (the existing value for
`None`/non-numeric priorities). Valid entries are unchanged.
Tests cover each rejected shape, the control shapes that already degraded, and
the builder-shaped output that must stay identical. Dropping only the mapping
guard fails the non-mapping cases; dropping only `OverflowError` fails the
non-finite cases.
* fix(mcp): handle infinite routing priorities in the builder
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
wait_tool reused the name gate for its pause loop, so every parallel task
returned the last child's result id and its done callback closed that
child's gate. The message reducer then dropped all but one pending result,
and the model was told that a running subagent had been interrupted.
Rename the loop variable so each call keeps its own gate.
* fix(projects): keep referenced originals when the trash sweep walks a symlinked tree
The storage reconciliation protects each row's namespace by its resolved
path (project_document_path resolves symlinks) but walked the unresolved
documents directory. With a symlinked .deer-flow (no DEER_FLOW_HOME),
users/, users/{id}/ or projects/, no walked directory matched a protected
namespace, so the startup sweep and the lazy trash-listing sweep unlinked
live document originals older than 24 hours as orphans.
Walk each project's resolved documents directory so both sides compare
the same paths.
* docs(changelog): reference #7732 for the trash sweep symlink fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(persistence): normalize non-finite floats before every JSON column write
The shared ORM engine serialized JSON columns with the lenient json.dumps
default (allow_nan=True), and the sync agent-storage engine set no
json_serializer at all. A non-finite float reaching a JSON column — run
metadata merges caller-supplied values, goal_verdict and
token_usage_by_model carry model/provider numbers, and json.loads('1e400')
yields inf — was emitted as the bare NaN/Infinity token, which PostgreSQL
jsonb rejects as invalid JSON, failing the whole write with DataError.
SQLite silently accepts and round-trips the invalid token, so the failure
was postgres-only and environment-dependent.
Adds dumps_json_safe/json_safe_value to persistence/json_compat.py (same
None-normalization the canonical display serializer applies since #7670,
which explicitly does not cover persisted state) and wires it into both
engines' json_serializer. Regressions pin: nested non-finite floats
normalize everywhere, finite values/bools pass through unchanged, the
async engine kwargs and the sync agent engine (postgres only; SQLite takes
no json_serializer) both route through it.
* fix(persistence): normalize JSON across synchronous engines
* docs: keep frontend guidance within the CI size budget
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(runtime): add Queue and Steer for active Web runs
* style(frontend): format Queue and Steer controls
* fix(runtime): retain user Steer controls and order active inputs
* docs: add Chinese changelog entry for Queue and Steer
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
serialize_lc_object() passed floats through unchanged, so a non-finite
value (NaN/inf/-inf) survived serialization and broke the two display
paths that share it with opposite JSON policies: the SSE wire
(format_sse / Redis bridge use the lenient default and emit bare
NaN/Infinity tokens the langgraph-sdk/useStream decoder rejects,
breaking the live stream) and the REST surface (Starlette JSONResponse
uses allow_nan=False and raises, turning a thread read into HTTP 500).
Non-finite floats reach the serializer from ordinary runtime data:
json.loads("1e400") yields inf for a tool parsing external JSON, a
provider logprob of -inf for a zero-probability token, or a divide-by-
zero ratio. They round-trip inside Python and checkpoint persistence and
only fail at the JSON boundary.
Normalize non-finite floats to None and recurse into model_dump()/dict()
output so nested values (e.g. a message's response_metadata) are
normalized too. This is a display/wire normalization only; persisted
checkpoint state is never routed through this serializer. Finite floats
are unchanged.
Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
* feat(subagents): add thinking_enabled and reasoning_effort config for sub-agents (#4875)
(cherry picked from commit b1083728b23504cb0f1d1a463f8db70c5ec1c5a7)
* fix: resolve CI failures for subagent thinking config PR
- Fix UnboundLocalError in task_tool.py: initialize parent_thinking_enabled
and parent_reasoning_effort before the if runtime block
- Remove unused imports from test_subagent_thinking_config.py
(SimpleNamespace, pytest, resolve_subagent_model_name)
- Fix import sorting in subagents_config.py and test file
- Add reasoning_effort: None to executor test assertion
(cherry picked from commit 187851e307b871937e48bb67ae1b34b8b69bf92d)
* fix: address review comments - remove parent inheritance, supports_thinking clamp, Literal type
- Remove parent_thinking_enabled/parent_reasoning_effort from metadata
extraction in task_tool.py (backward compatible: subagents only think
when explicitly configured)
- Add supports_thinking clamp in _create_agent to prevent cross-model
mismatch errors
- Change SubagentConfig.reasoning_effort to Literal type
- Fix sandbox_state assignment that was accidentally removed
- Fix syntax errors in subagents_config.py (missing closing parens)
- Update all docstrings/comments from "inherit" to "no override"
- Simplify test helper and remove parent inheritance tests
(cherry picked from commit 74f9c659ef4a98bd1756109929ab0748fab82317)
* feat(subagents): adapt reasoning settings to current native workers
* fix(subagents): retain model context on reasoning rejection
* fix(subagents): preserve model-factory reasoning diagnostics
---------
Co-authored-by: Neon549 <neon549@users.noreply.github.com>
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
* fix(skills): capture package digests without POSIX-only dir_fd
`capture_package` rejected every non-POSIX host, so `SkillMutationRuntime.state`
fell back to `"unsupported-" + uuid4().hex`. That digest differs on every call,
so `repository.observe()` recorded a mutation for an untouched package:
`mutation_seq` kept climbing and ABA detection could never match.
Walk the tree with `scandir` + `lstat` where `dir_fd`/`O_NOFOLLOW` are missing,
comparing the pre-open `lstat` against the opened descriptor before reading,
and leave the POSIX descriptor walk unchanged.
Windows 11 / Python 3.12: test_skill_asset_versions.py 5 failed -> 23 passed;
reverting this file alone brings the 5 failures back.
* docs(gateway): keep guidance below CI size budget
---------
Co-authored-by: Felix Shi <204130665+shi-felix@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(tui): show picker labels as written instead of parsing markup
SelectScreen passed thread titles and model display names to Textual as
plain str, which Textual parses as content markup. A thread title such as
'Fix [/bold] in parser' raised MarkupError and exited the app whenever
/threads opened, and '[a]' vanished from the list. Wrap the title and
option labels in Rich Text so they render literally.
* docs(changelog): reference #7631 for the TUI picker markup fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Declare required thinking, native reasoning effort and image support for both
K3.1 Pro IDs. Normalize fixed parameters at the final Moonshot request boundary
and cover real SDK serialization with offline regression tests.
Fixes#7425
* fix(runtime): present outputs before a loop wrap-up and explain step-limit stops
Loop-detection warnings now ask the agent to present the files it already
created in /mnt/user-data/outputs before its final answer, when
present_files is available.
A run that exhausts recursion_limit now ends with stop_reason
recursion_capped and a plain message that lists outputs it created but
did not present. An active goal stands down without an evaluator call.
The /wait error envelope keeps the GraphRecursionError name, and IM
channels reply with the step-limit message instead of a generic failure.
* docs(changelog): note run-end delivery and step-limit stops (#7588)
* test(runtime): stop pinning LangGraph's step count in the step-limit test
The produced paths and the message already follow the observed model
calls, so the exact count only pinned LangGraph's per-step accounting.
Also note on the step-limit goal branch that its caller passes no
evaluator factory, so it has to stay ahead of the evaluator section.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>