762 Commits

Author SHA1 Message Date
ZephyrWang
42da64a25c
fix(dingtalk): verify recipient status for file sends (#8089) 2026-11-10 17:50:40 +08:00
yetuge
d49e0f3d7c
fix(gateway): reject unusable skill_context entries at the input boundary (#8091)
* fix(gateway): reject unusable skill_context entries at the input boundary

merge_skill_context indexes entry["path"] and calls entry.get(), so a
caller-supplied non-object entry or one without a usable path raised during
the state merge and failed the run with a 500. Validate the list shape at the
run-input and thread-state boundaries, mirroring the todos and delegations
channels.

* Normalize entry by ensuring 'name' is a string

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-10 15:36:01 +08:00
kongxin
f17e0504d0
fix(feedback): return 409 instead of 500 when feedback is POSTed twice (#8064)
* fix(feedback): return 409 instead of 500 when feedback is POSTed twice

POST /api/threads/{id}/runs/{rid}/feedback did a plain INSERT. A second
POST by the same user for the same run (a retry, or feedback on another
message of that run) hit uq_feedback_thread_run_user and the uncaught
IntegrityError surfaced as HTTP 500.

FeedbackRepository.create now rolls back and raises
FeedbackAlreadyExistsError on that collision, and the route maps it to
409 pointing callers at PUT, which already upserts.

* fix(feedback): only map the run/user unique key to 409; cover concurrent POSTs

Address review on #8064:
- create() now re-reads the (thread, run, user) row after an
  IntegrityError and raises FeedbackAlreadyExistsError only when it
  exists; any other violation (e.g. a feedback_id collision) propagates.
- Add a commit-barrier test where two first POSTs race: one 200, one
  409, and the winner's row is kept.
- Add CHANGELOG.md / CHANGELOG_zh.md Fixed entries.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 07:47:01 +08:00
inchang-ing
1647320ad1
fix(gateway): return 404 on cross-owner thread_id instead of 500 (#8057)
* fix(gateway): return 404 on cross-owner thread_id instead of 500

When a caller-supplied thread_id already belongs to another user, the SQL-backed store rejects the duplicate primary key with IntegrityError. The route's recovery path resolved the surviving row through the requesting owner's scope, so a foreign row could not be read back and the route surfaced a 500 instead of the documented 404 ownership-conflict response ("Thread not found").

Read the row back bypassing the owner scope before collapsing to a 500 so the cross-owner case mirrors ThreadOwnershipConflictError.

Fix #8052

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>

* test(gateway): pin thread-ownership 404 and real-failure 500

Address review feedback on the cross-owner 404 recovery:
- Update test_insert_race_concurrent_trusted_claims_have_one_winner: the
  losing request now resolves the surviving unscoped row as a 404 ownership
  conflict instead of a 500, while the single-winner assertions and the
  non-disclosing detail are preserved.
- Add test_create_thread_keeps_real_integrity_failure_as_500 to pin the
  fall-through branch where a duplicate-key IntegrityError coincides with no
  surviving row, which must still surface as a 500 rather than collapse into
  the ownership-conflict 404.
- Document the user-facing error-surface change in the Unreleased Fixed
  section.

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>

---------

Signed-off-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
Co-authored-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-10 06:22:24 +08:00
yu1128-L
7a8051c0d9
fix(artifacts): bind ranged reads to validated generation (#7978)
* fix(artifacts): bind ranged reads to validated generation

* fix(artifacts): drain pinned reads without Windows hard links

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 21:18:57 +08:00
Severus
7adb2062eb
fix(gateway): keep retryable thread inputs ahead of terminal receipts (#7972) (#8008)
* fix(gateway): keep retryable thread inputs ahead of terminal receipts (#7972)

* fix(gateway): document thread-input ranking policy (#7972)

* test(gateway): cover thread-input ranking and receipt starvation (#7972)

* test(gateway): format thread-input regression tests with repo ruff config (#7972)
2026-11-09 20:28:09 +08:00
一片一片
3ebd4de993
fix(scheduler): isolate a malformed stored schedule from the poll batch (#7999)
remain from its review:

- `stored_next_run_at` caught the ValueErrors a dropped zone or a newer cron
  parser raises, but a stored `schedule_spec` that is JSON null, a list or a
  string fails on `.get` with AttributeError. The launch-failure handler
  recomputes the schedule, so that escaped `run_once`'s claimed-task loop and
  skipped the rest of the batch. Catch it too; the task is paused with the
  reason like the other unusable schedules.
- `run_once` dispatched its claimed batch with a bare loop, so any per-record
  raise in `dispatch_task` re-aborted the batch. Isolate each task's dispatch
  and keep claim-level store failures loud above the loop.

Tests: `test_scheduler_poll_batch_isolation.py` (non-object specs, end-to-end
pause, per-task isolation). Red on `91f20a7`, green at head.

Refs #7998
2026-11-09 20:25:32 +08:00
2SinceJuly
4e09e63a68
fix(threads): evict in-memory runs when a thread is deleted (#8036)
* fix(threads): evict in-memory runs when a thread is deleted

RunManager.get() and list_by_thread() answer from the process-local _runs
registry before consulting the RunStore. DELETE /api/threads/{id} only
removed the durable rows, so a worker that had admitted the thread's runs
kept serving them: after the delete returned 200 and the thread itself
answered 404, GET /api/threads/{id}/runs still listed the run and
GET /api/threads/{id}/runs/{run_id} still returned 200 with the original
prompt intact in kwargs. The residue disappeared only on restart, which
is what made it look like a cache rather than a leak.

Add RunManager.discard_thread_runs() to drop this worker's copies for the
thread and call it from the delete path, next to the durable run
deletion. Internal thread-operation rows are left alone: the reservation
protecting the deletion is one of them and its own release path owns it.
Historical runs, the thread row, run events and checkpoints are unchanged.

* fix(threads): keep a live finalize tail attached when evicting runs

discard_thread_runs() popped every record it found, but worker.py clears
finalizing before the tail that publishes the terminal frame, closes the
journal and releases the sandbox lease. Popping mid-tail turns the tail's
manager-mediated writes into warnings on a record that no longer exists.

Skip records whose task is still running, mirroring what cleanup() already
does through shield(), and pin the guard with a unit test that seeds both
a finished run and a delete reservation. Document that a DELETE served by
one Gateway bounds, rather than removes, a peer's copies: they survive
until that peer's deferred cleanup(run_id) fires.

* fix(threads): hide a deleted run from reads during its finalize tail

A DELETE that arrives after worker.py clears `finalizing` but before the
worker task finishes cannot drop the record: the tail still publishes the
terminal frame, closes the journal and releases the sandbox lease through it,
so popping turns those writes into `set_status called for unknown run`.
Leaving the record readable instead keeps the run and its prompt reachable
through `GET /threads/{id}/runs` until `cleanup()`'s 300s delay fires, which
is exactly the window #8034 reports.

Records that a deletion reaches mid-tail are now flagged instead of skipped.
`get()`, `list_by_thread()`, `get_many_by_thread()`, the regenerate-source and
edit-replay reads skip a flagged record (and never rebuild it from a store row,
since the durable rows were deleted first), and the tail's own completion
evicts it — no second delete request and no timer.

* fix(threads): keep the tail eviction task referenced until it settles

The done-callback eviction is a separate task because it cannot await the
registry lock inline. The loop holds only weak references to tasks, so an
unreferenced eviction could be collected before it drops the record and the
deleted run would come back within `cleanup()`'s 300s window. Track it in a
manager-owned set and discard on completion.

* fix(threads): keep hidden runs hidden on every read path

The deletion flag set by discard_thread_runs is authoritative for "this run
is deleted", but two reads still treated it as a hint.

get() narrowed its store-less guard to a missing record, so a flagged
record on a store-less RunManager fell through to self._store.get() on
None; the AttributeError was swallowed by the broad except below while
every read in the flag window logged a hydration warning with a full
traceback. The record now settles the read before any store call, which
also keeps the read side-effect free as the comment intends. aget()
shares the path.

get_many_by_thread() rebuilt flagged runs from store rows. list_by_thread
already refuses that, because the durable row is deleted before the flag
is set and anything left is a stale or partial read; the batch read now
applies the same guard so the invariant is uniform.

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
2026-11-09 20:21:07 +08:00
王一帆
463361c5ba
feat(weknora): allow opt-in shared retrieval for authenticated users (#7944)
* feat(weknora): allow opt-in shared authenticated retrieval

* docs(weknora): clarify shared retrieval identity requirements

* docs(weknora): clarify durable batch authentication limits

* docs(gateway): keep agent guidance within hard size limit

---------

Co-authored-by: Totoro <msy626836554@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 17:06:05 +08:00
Hyeonsang Cho
7bb92cea10
fix(threads): keep the owner row when deleting checkpoints fails (#7992)
* fix(threads): keep the owner row when deleting checkpoints fails

DELETE /api/threads/{id} treated the checkpoint delete as best-effort: a
failure was logged at DEBUG, the threads_meta row was removed anyway and
the request returned success. Read routes allow a thread without a meta
row as an untracked legacy thread, so the surviving checkpoints (title,
messages, goal) became readable by every signed-in user through
GET /threads/{id}, /state, /history and /goal, and the owner could not
retry because DELETE requires the row.

A failed checkpoint delete now logs the exception and returns a generic
500 before the meta row is touched, so the thread stays owned and the
owner can retry. The remaining cleanup steps stay best-effort; their
reads are owner-scoped.

* docs(changelog): reference #7992 for the thread delete checkpoint fix

* fix(threads): require checkpoint deletion before dropping ownership

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 11:53:56 +08:00
Zeren Wang
0d04322d41
feat(hitl): add human-in-the-loop approval for high-risk tool calls (#7979)
* feat(hitl): add human-in-the-loop approval for high-risk tool calls

* fix(hitl): address approval review findings

* docs(hitl): keep agent guidance under size limit

* fix(hitl): guard approval pause recovery

* docs(hitl): shrink inherited guidance

* fix(hitl): harden approval admission and recovery
2026-11-09 09:52:35 +08:00
hdu_zhou
4c00e8d4a9
fix(memory): reject Windows reserved device names in an agent memory scope (#7941)
* fix(memory): reject Windows reserved device names in an agent memory scope

`_management_agent_name_or_501()` validated the `agent_name` query parameter
with `AGENT_NAME_PATTERN` (`^[A-Za-z0-9-]+$`) only, which admits `NUL`, `CON`,
`COM1` and the rest of the Windows reserved device names.

An agent scope is not only a label: DeerMem resolves it to
`{storage}/users/{uid}/agents/{name}/facts` and creates that directory on the
first write. On Windows the path never materialises, so `create_fact` raised
`FileNotFoundError`, which the endpoint's `OSError` branch turned into
`500 Failed to create memory fact`.

Run `windows_incompatible_segment()` at the same boundary -- the check agent
directories, thread IDs, uploads, sandbox writes and skill support files
already go through -- and answer `422`. The `for_creation` flag scopes the rule
to the two routes that create the scope, `POST /memory/facts` and
`POST /memory/import`; read and delete routes stay permissive so a scope that an
older release stored under a reserved name can still be exported and cleared.

Closes #7940

* fix(memory): address review feedback for Windows reserved agent names

Cite PR #7941 in CHANGELOG instead of issue #7940; pin the clear route's permissive behavior for a legacy reserved-name scope with a regression test; cross-reference the shared validate_agent_name() reserved-name check so the two copies cannot drift.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-09 09:09:00 +08:00
fuduoduo
2a9e7255a7
fix(channels): keep Gateway calls off the environment proxy (#7877)
* fix(channels): keep Gateway calls off the environment proxy

* fix(channels): keep the aio_sandbox policy import lazy

Importing deerflow.community.aio_sandbox.backend at module scope executes
the package __init__, which eagerly imports AioSandbox, AioSandboxProvider
and both backends. That makes every ChannelManager import load the entire
sandbox provider stack even when a different sandbox is configured.

Mirror app/gateway/health.py::_make_provisioner_client: move the import into
a small _make_gateway_client helper and route both Gateway call sites through
it, so the URL policy stays lazy.

* fix(channels): keep the run client off the environment proxy

langgraph_sdk.get_client() builds httpx.AsyncHTTPTransport(retries=5) on an
httpx.AsyncClient that leaves trust_env at its default True and exposes no
transport/trust_env override, so ChannelManager._get_client() still sent
runs.wait submissions to a loopback Gateway through HTTP_PROXY. That is the hop
#7876 reports: the control-plane calls are fixed, the run submission is not.

Route the SDK client through _make_langgraph_client: loopback and private
Gateways get an explicit trust_env=False httpx.AsyncClient carrying the SDK's
own retries, timeout and User-Agent and handed to LangGraphClient, while
external Gateways keep the stock get_client() path.

Also move the URL policy out of the aio_sandbox package into
deerflow.utils.http_policy (control_plane_trust_env). Keeping the import lazy
did not help on its own: aio_sandbox/__init__ eagerly imports the provider
stack, so importing aio_sandbox.backend still loaded all of it. The neutral
module has no httpx or sandbox dependency, so manager.py and gateway/health.py
import it at module scope, and aio_sandbox.backend re-exports it as
sandbox_http_trust_env for the existing sandbox call sites.

* fix(channels): keep AGENTS.md under budget and align legacy CSRF test with new client

* style(sandbox): remove unused urlparse import

---------

Co-authored-by: jack12178 <jack12178@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 22:10:10 +08:00
Eilen Shin
aaca3eeaeb
refactor(agent): ground runtime decisions in structured evidence (#7986)
* refactor(agent): ground runtime decisions in structured evidence

Use producer status and structured errors for tool recovery, preserve memory context without lexical confirmation, and remove predetermined receipt and skill conclusions.

Require explicit MCP discovery and authenticated schedule trial actions. Preserve authorization, ownership, isolation, and bounded execution; update documentation and regression coverage.

* test(sandbox): expect read_file initialization error callback

* fix(agent): address evidence review follow-ups

* fix(agent): preserve retry budgets for OpenAI transport errors

* fix(agent): expose tool execution status in model requests

Project validated execution metadata and exact tool text into request-only JSON so status survives provider serialization. Preserve stored results and receipt hashes, cover SDK and graph boundaries, and migrate replay input hashes without changing recorded responses.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 22:00:23 +08:00
Eric
3920169fb6
fix(subagents): reject Windows reserved device names in managed subagent creation (#7959)
* fix(subagents): reject Windows reserved device names in managed subagent creation

* Reject Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
  in normalize_managed_subagent_name when for_creation=True via
  windows_incompatible_segment.
* Validate subagent names on creation in FileManagedSubagentStore.create
  and /api/subagents POST gateway router before attempting disk writes.
* Keep read, update, and delete paths permissive for backward compatibility.
* Add unit tests for store validation and router endpoints.
* Update CHANGELOG.md under Fixed.

* fix(subagents): reject reserved names in SQL managed-subagent create

Apply the same for_creation check used by the file store so both
backends refuse Windows device names. Pin mixed-case Nul in tests and
import the shared normalizer at module level.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 21:55:46 +08:00
aduoge
91f20a782f
fix(github): match repository bindings case-insensitively (#7982)
GitHub owner and repository names are case-insensitive, but the agent
registry indexed and looked up bindings by the raw repo string. A
binding for acme/widget never matched webhooks for Acme/Widget, so the
event was dropped without an error. Lowercase the repo on both sides.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 13:45:59 +08:00
Hyeonsang Cho
8924c5844a
fix(scheduler): pause a task whose schedule no longer computes (#7993)
* fix(scheduler): pause a task whose schedule no longer computes

Admission validates every schedule, but a stored one can stop computing
later: a zone the installed tz database no longer has, or a cron
expression a newer croniter rejects. The scheduler recomputes next_run_at
inside the shared poll, and that raise escaped:

- dispatch_task recomputed it in its except block after a successful
  launch, so run_once aborted the claimed batch and left the occurrence
  in "launching" with no last_error;
- once the launch lease expired, recover_expired_launch_claims found the
  durable run and raised again for every poll, before claim_due_tasks,
  so no other scheduled task was admitted for the rest of the process.

Runtime cursor writes (launch, launch repair, queue timeout, launch
failure, finalization) now use stored_next_run_at, which returns the
reason instead of raising. A live recurring task is paused with
"schedule can no longer be computed: ..." as its last_error and no next
run; a once task keeps its normal outcome. Resume re-validates the
schedule, so it reports the problem until the schedule is fixed.

run_once also isolates the lease-recovery pass, like the terminal-run
reconciliation next to it, so a failed pass no longer skips admission.

* docs(changelog): reference #7993 for the unschedulable task fix
2026-11-08 13:39:34 +08:00
Harvey DU
3aefc56f5f
fix(gateway): keep batch cancellation running when the scheduler store fails (#7971)
* fix(gateway): keep batch cancellation running when the scheduler store fails

_run_completion_hook awaited scheduled_task_service.handle_run_completion
before batch_continuations.source_finished, so a scheduler store outage
aborted the hook and skipped batch cancellation entirely: a claimed
follow-up of a hard-stopped run stayed in the dispatching state instead
of being cancelled.

The scheduler notification is now best-effort (logged as non-fatal);
batch cancellation always runs.

Fixes #7969.

* fix(gateway): isolate batch cancellation failures and trace completion warnings

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 12:52:05 +08:00
alexcastgonz
b315cf2c9e
fix(telegram): wait for retry_after before retrying a new message (#7990)
* fix(telegram): honor retry_after when retrying a new message

A 429 on sendMessage or sendRichMessage slept 2**attempt and dropped
the reply before Telegram's flood wait expired. Wait the longer of
that backoff and the server hint, and leave other channels unchanged.

Fixes #7989

* fix(telegram): bound new reply flood waits

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-08 09:51:43 +08:00
Totoro
3f9a78e02e
fix(goal,artifacts): verify completion and artifact handoff (#7961)
* fix(goal,artifacts): verify completion and artifact handoff

* fix(goal,artifacts): harden shared artifact handoff

* docs(agents): shrink inherited guidance to fit chain budget

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-11-07 18:21:49 +08:00
Hyeonsang Cho
d658b13c79
fix(channels): keep Feishu cards within the card size limit (#7798)
* fix(channels): keep Feishu cards within the card size limit

Card text was unbounded and ASCII-escaped, so an answer of about 4,400 CJK
characters made the card request body exceed Feishu's 30 KB limit (230025).
The final patch and its reply fallback were both rejected, the answer was
lost and the running card stayed on its last streamed text.

Cards are now serialized without ASCII escapes and budgeted by the bytes
their text adds to the request body. A streamed update shows what fits; a
longer final answer continues in up to 10 cards replied to the same source
message, split at line breaks, and a retry resumes after the cards already
delivered.

* docs(changelog): reference #7798 for the Feishu card size fix

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-07 15:52:59 +08:00
Hyeonsang Cho
61d76d9a6a
fix(channels): key WeCom group conversations by chatid, not the sender (#7794)
* fix(channels): key WeCom group conversations by chatid, not the sender

Group messages used the sender's userid as chat_id and topic_id, the same
key as the sender's private chat. Wherever no group connection kept them
apart (auth-disabled mode, require_bound_identity: false, legacy group
bindings), a member's private chat and every group they wrote in ran in one
DeerFlow thread. Groups are now keyed by chatid with one thread per member.
A group-scoped connection carries its sender-keyed thread over once; a
legacy binding, which spans every group of the bot, does not.

* docs(changelog): reference #7794 for the WeCom group conversation fix

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-06 21:36:19 +08:00
yetuge
a41d0fcec1
fix(parsing): keep literal think tags inside JSON string values (#7907)
* fix(parsing): keep literal think tags inside JSON string values

Structured parsers ran strip_think_blocks over the whole response before
json.loads. A model answer that quotes the tag -- "Explain <think>example
</think>" -- lost the quoted text, and an unclosed literal tag inside a
string cut the JSON document mid-string, so suggestions returned None and
goal evaluation failed to parse.

Strip reasoning only outside JSON string literals: leading blocks are still
removed whole (a JSON example inside reasoning is never taken as the
answer), and blocks after the payload starts are removed only outside
quotes. Suggestions and the goal evaluator both use the shared helper.

* fix(parsing): keep think-tag stripping complete outside the JSON payload

The string-aware walker only covered the payload itself: a reasoning block
after prose and before the payload was copied verbatim, so brackets inside
it became the parsers' find/rfind targets, and an unclosed block in
trailing prose was kept where strip_think_blocks used to truncate.

Find the payload start while stepping over think blocks, strip the head with
strip_think_blocks, keep strip/truncate semantics after the top-level
payload closes, and truncate at an unclosed block outside a string. Adds a
CHANGELOG entry for the user-visible parsing change.
2026-11-06 21:19:52 +08:00
Chau Vu
856cde25b1
fix(gateway): strip server-owned metadata from run input todos and goal (#7803 follow-up) (#7904)
* fix(gateway): strip server-owned metadata from run input todos and goal (#7803 follow-up)

Commit 459fc8c introduced shape validation for caller-written todos in
both strip_server_owned_state_metadata and normalize_input, but only
applied _strip_external_metadata_from_message_like in
strip_server_owned_state_metadata.

External callers submitting POST /runs or POST /threads/{id}/runs with
input.todos or input.goal could supply forged server-owned metadata
(such as untrusted_input: False, original_user_content, or artifact
knowledge_sources) directly into the agent graph.

Apply _strip_external_metadata_from_message_like to both normalized
todos and goal in normalize_input when trusted_internal=False.

* fix(gateway): mirror normalized goal and todos into canonical run record input

* fix(gateway): replace non-dict additional_kwargs and cover edge cases

- Ensure non-dict additional_kwargs in message-like entries is replaced with an empty dict instead of surviving the strip
- Remove redundant None-check on normalized_goal in normalize_input
- Add regression tests covering non-dict additional_kwargs replacement and untrusted_input stamping on human-like entries

* test(gateway): format todo input validation cases

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-06 21:14:28 +08:00
lisry-1
c0b0ac3489
fix(feishu): clear msg.files after receive_file downloads them (#7804)
receive_file downloaded each image/file descriptor and replaced the
[image]/[file] placeholders in msg.text, but left msg.files intact. The
generic URL-based _ingest_inbound_files pass then re-walked the residual
{"image_key":...}/{"file_key":...} descriptors, found no url, and logged a
spurious 'reader returned no data' warning per file.

Clear msg.files after materializing the paths, matching DingTalk (msg.files =
[]) and the Slack/Telegram/Discord providers (which materialize them).

Co-authored-by: lisry-1 <249596828+lisry-1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-06 11:27:36 +08:00
Totoro
593ba49458
feat(subagents): continue requested batch work and schedule explicit prerequisites (#7839)
* feat(subagents): continue native batches and honor item dependencies

* docs: link native batch follow-up changelog entries

* test(migrations): pin isolated history-seed upgrade to 0050

* fix(subagents): preserve follow-up scope, blockers and fair scheduling

* test(memory): freeze capacity evaluation time

* fix(subagents): guard follow-up admission and retain run controls

* fix(subagents): enforce runs:cancel on tool cancellation of follow-up batches

* style(tests): format test_batch_followup_host with ruff

* fix(subagents): drop the admission checkpoint check and keep SDK cancel compatible

The checkpoint check inside run admission waited on the checkpoint saver's
lock while holding the SQLite write lock, which could make another chat's
checkpoint write fail. Remove it; dispatch still checks the conversation
before admission.

A follow-up waiting for an unanswered question now keeps its claim until the
lease expires instead of rereading the conversation every poll. Cancel passes
the caller's run authority only to submitters that take it, so existing SDK
submitters keep the two-argument call. Document that retrying a prerequisite
by hand does not requeue its blocked items, and list 0052 as the migration head.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 15:10:21 +08:00
lukyturnflow
bef8c9c2e3
fix(skills): keep disabled skills disabled when the extensions config cannot be read (#7705)
* fix(skills): fail closed when the extensions config cannot be read

* test(skills): cover a disabled skill when the extensions config cannot be read

* style(tests): format the fail-closed regression for ruff

* style(tests): fit the docstring and signature inside the 240-column limit

* fix(skills): fail closed in the user-scoped listing when the config cannot be read

* test(skills): cover the user-scoped listing with an unreadable extensions config

* fix(gateway): keep skill read surfaces loud about an unreadable extensions config

Folding the user-scoped degraded mode in (#7704) also stopped the skill
read endpoints from failing on a config that cannot be read: the storage
now answers with an all-disabled listing, which regressed the #7155
contract (a generic 500 with the host path only in the server log) into
a 200 that presents a broken deployment as a legitimately empty one.

Keep both properties: the storage still fails closed for the runtime
gate, and the read surfaces re-validate the config explicitly so the
raised error reaches each handler's generic 500 branch unchanged.
First-run behavior is untouched: no config file resolves to an empty
config without raising, so default-enabled still applies.

* fix(skills): redact config-load failures and keep the read preflight off the event loop

Review follow-ups:

- Log the failure shape instead of the exception text in both degraded
  modes: from_file resolves $VAR placeholders before validation, so its
  message can embed resolved credentials. Reuse _describe_load_failure,
  the loader's own redaction helper.
- Run the gateway read preflight through asyncio.to_thread in the
  capability adapter, matching the scan it precedes.
- Cover the user-scoped degraded branch with a CUSTOM skill that has
  per-user state on disk, so the second read's guard is directly
  regression-tested (verified red with the guard removed).

---------

Co-authored-by: Jay0130-a <309071492+Jay0130-a@users.noreply.github.com>
2026-11-05 15:07:07 +08:00
Wenchao An
b6ed47778d
feat(extensions): add versioned team file handoffs (#7817)
* feat(extensions): add versioned team file handoffs

* fix(extensions): fence team artifact execution and snapshots

* fix(agent-teams): retain resumed keys and bound artifact transfers

* ci: reclaim runner space before sandbox image smoke

* ci: budget sandbox image preparation separately

* fix(extensions): version artifact contracts and document snapshots
2026-11-05 14:50:09 +08:00
哈基米
713a861e75
fix(channels): ignore non-finite WeChat server longpoll timeouts (#7898)
* fix(channels): ignore non-finite WeChat server longpoll timeouts

The getupdates response's longpolling_timeout_ms was passed through
float() with only TypeError/ValueError handled. A huge JSON integer
raised OverflowError before the message loop and cursor write, so the
same batch was re-polled forever, while Infinity/NaN were stored and
turned the next request timeout into inf/nan.

Apply the same positive-finite rule the channel already uses for
configured timing values: catch OverflowError and reject non-finite
results, keeping the previous valid timeout.

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

* fix(channels): bound WeChat server long-poll hints

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 12:07:31 +08:00
一片一片
459fc8ca43
fix(todos): keep an unusable stored todo list from failing every model call (#7803)
* fix(todos): keep an unusable stored todo list from failing every model call

`todos` is a reducer channel that `POST /threads/{id}/state` and run input can
Overwrite directly, but neither boundary validated its shape. The todo
middleware rendered it with `entry.get(...)` in both `before_model` and
`after_model`, so a non-object entry — or a value that is not a list — raised
AttributeError on every model call, permanently for that thread.

Validate caller-written todos beside `goal` in `_normalize_external_todos`: a
list of objects with string content and a known status, or HTTP 400. The
middleware now reads the channel through `_usable_todos` and ignores entries of
any other stored shape, so checkpoints and direct integrations written before
this boundary still run.

* fix(todos): normalize stored todos in token-usage attribution

TokenUsageMiddleware reads the same stored `todos` channel through
`_build_attribution` whenever the last AIMessage calls write_todos, and
`token_usage.enabled` defaults on, so a value the todo middleware now tolerates
still aborted the step here. Route the stored list through `_normalize_todos`
before reading entries.

Document that a caller-written todo without a status is accepted as pending and
cover that shape at both external boundaries.

* docs: keep backend agent guidance within size budgets

* fix(todos): keep the channel metadata strip on caller-written todos

The dedicated todos branch in strip_server_owned_state_metadata replaced the
generic list branch, which stripped server-owned metadata from every entry. A
caller could therefore persist additional_kwargs.original_user_content,
untrusted_input, or artifact.knowledge_sources on a todo entry. Re-apply the
per-entry strip after normalization, keeping None and empty lists untouched.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 07:28:33 +08:00
Wenchao An
2145c9ab1f
feat(models): discover provider catalogs and search chat models (#7871)
* feat(models): discover provider catalogs and search chat models

* docs(models): scope catalog guidance to owning directories

* test(guidance): register model catalog scoped instructions

* test(memory): pin capacity scenarios to a stable clock

* fix(models): guard catalog imports and align picker interactions

* test(memory): share the fixed clock with updater capacity tests

* fix(models): clear provider filters removed by catalog refresh

* refactor(models): share connection fields across model forms

* fix(models): keep active provider filters clearable

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 06:43:01 +08:00
aduoge
26bf78f2b9
fix(github): apply allow_authors to the webhook sender (#7868)
_author_login returned the issue/PR creator for issues and pull_request
events. On edited/labeled/reopened actions the actor is sender, so an
allow_authors user editing someone else's issue was gated on mention,
while anyone editing an allow_authors user's issue bypassed it. Prefer
sender.login and keep the object author as a fallback.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-04 21:35:55 +08:00
golemon
5371343f69
fix(gateway): continue dispatching queued inputs after cancellation (#7746)
* fix(gateway): continue queued inputs after cancellation

* docs(runtime): clarify queued input cancellation handling

* fix(gateway): clarify cancelled-input error and log race-window skips

* docs(changelog): document queued input cancellation fix

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-04 10:39:20 +08:00
spud
21175ba46f
fix(runtime): persistently fence stale run event producers (#7619)
* fix(runtime): fence revoked run event producers

* fix(runtime): reduce off-mode producer guard locking

* docs: keep inherited guidance within size budget

* chore(runtime): remove development phase labels

* fix(persistence): normalize libpq SSL options for asyncpg connections

* docs(runtime): clarify producer fencing behavior in off mode

* test(persistence): align migration TLS probes with channel-binding rejection

* fix(runtime): authorize fenced Gateway history seeding

* fix(scheduler): preserve takeover identity in recovery publication

* fix(scheduler): report unconfirmed recovery publication

* fix(runtime): preserve history messages before the first human turn

* fix(runtime): clarify run creation failure diagnostic

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 15:26:42 +08:00
Willem Jiang
01e1ed778b
docs: restore agent guidance size budgets (#7810) 2026-11-02 15:20:54 +08:00
王一帆
d0ec8dcc5d
feat(knowledge): add optional ingestion and status center (#7518)
* feat(knowledge): add optional ingestion and status center

* docs(knowledge): record upstream CI budget fix and regression results

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 10:42:21 +08:00
Hyeonsang Cho
2c8892c810
fix(channels): deliver the DingTalk reply when an AI card request fails (#7781)
* fix(channels): deliver the DingTalk reply when an AI card request fails

dingtalk-stream's AICardReplier logs a failed card request and returns
as if it succeeded; its create call returns the card ID it generated
before the request. The channel stored that ID, so neither the running
reply's text fallback nor send()'s sampleMarkdown fallback could run,
every streamed update failed silently, and the final answer never
reached DingTalk.

Send createAndDeliver and the streaming update through the channel's
httpx client with its token, using the bodies the SDK sends, and raise
on HTTP and transport failures so both fallbacks run. The SDK replier,
its per-card map and the token shim it needed are removed.

* docs(changelog): reference #7781 for the DingTalk AI card fallback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 10:34:03 +08:00
Hyeonsang Cho
7fd0c0cb3e
fix(harness): keep a cancelled batch cancelled when retrying a failed item (#7792)
* 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
2026-11-02 10:05:05 +08:00
N
98b72af732
fix(auth): allow demoting disabled administrators (#7769) 2026-11-02 09:26:00 +08:00
Daoyuan Li
80902c52b7
fix(channels): explain in-flight goal write rejections (#7741)
* fix(channels): explain in-flight goal write rejections

* test(channels): pin Gateway busy-goal feedback contract

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 09:16:19 +08:00
lisry-1
08f343f5d7
fix(wecom): report False when send_file cannot upload instead of True (#7768)
send_file short-circuited with `return True` for non-final messages, which
reported a successful upload that never happened. Per the Channel.send_file
contract ("Returns True if the upload succeeded, False otherwise"), a
message with no upload path must report False so the attachment lands in the
'failed' feedback the user relies on, instead of being silently dropped.

Drop the non-final short-circuit so the method reaches its normal
connectivity checks (no WS client / no thread frame -> False), matching every
other channel.

Co-authored-by: lisry-1 <249596828+lisry-1@users.noreply.github.com>
2026-11-01 23:59:00 +08:00
Hyeonsang Cho
17b05718e0
fix(agents): store an explicit null description as empty (#7638)
* 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>
2026-11-01 23:34:59 +08:00
Xiao Hou
830c8ff6f1
fix(runs): trim event_types filter entries before querying (#7758)
* fix(runs): trim event_types filter entries before querying

* style(runs): compute the trimmed event type once per entry

Addresses the review nit on #7758: use the walrus pattern already present
in this file (lines 657 and 1658) instead of stripping each entry twice.

No behavior change.

---------

Co-authored-by: gouyu-hou <gouyu-hou@users.noreply.github.com>
Co-authored-by: gouyu-hou <213296172+gouyu-hou@users.noreply.github.com>
2026-11-01 23:27:00 +08:00
lisry-1
e0f5483fdf
fix(goal): fall back gracefully for an unsupported reply locale (#7745)
* fix(goal): fall back gracefully for an unsupported reply locale

goal_reply_text and goal_status_text indexed _TEXT directly with the
caller's locale, so a locale outside NotificationLocale (en-US/zh-CN)
raised KeyError. The /connect handshake (connect_reply_text) normalizes
with supported_locale and falls back to en-US; the /goal replies must do
the same instead of surfacing an internal error to the user.

Normalize the locale in both public entry points. Adds tests pinning the
fallback for an unknown locale and the zh-CN wording for a supported one.

* style(tests): format goal status locale assertion

---------

Co-authored-by: lisry-1 <249596828+lisry-1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-31 19:19:42 +08:00
aduoge
12b2a9429c
fix(channels): surface Feishu upload log_id and missing keys (#7731)
* fix(channels): surface Feishu upload log_id and missing keys

* test(channels): cover Feishu upload key and log_id checks

* test(channels): cover non-str Feishu upload keys
2026-10-31 19:16:29 +08:00
Liu Hao
762b7188ab
fix(conversation): tolerate malformed event metadata (#7721)
* fix(conversation): tolerate malformed event metadata

* fix(gateway): guard malformed event metadata

* fix(gateway): guard malformed message kwargs

* fix(gateway): log malformed transcript metadata

* fix(gateway): ignore malformed message roles
2026-10-31 18:25:31 +08:00
jswSpace
bee97022b1
fix(projects): defer trash cleanup when retention config is unavailable (#7683)
* fix(projects): preserve trash when retention config is unavailable

* test(projects): address trash retention review feedback

---------

Co-authored-by: jswSpace <198684035+jswSpace@users.noreply.github.com>
2026-10-31 18:08:11 +08:00
gouyu-hou
0f2f199b3d
fix(console): tolerate malformed usage counters and pricing (#7736)
* fix(console): tolerate malformed usage counters and pricing

* docs(routers): record console counter tolerance in the router contract

* fix(console): reject stored counters beyond the float range

A JSON integer is not bounded by the float range: 10**400 parses as an
exact int, so _token_count passed it through and _token_cost then raised
OverflowError on the division back to float, taking /usage, /stats and
/runs down with a 500 - the same failure mode the rest of this PR
removes for every other junk shape.

Reject counts whose float conversion is not finite inside the existing
try, so they coerce to 0 and the run falls back to its legacy totals.
10**30 stays usable.

---------

Co-authored-by: gouyu-hou <gouyu-hou@users.noreply.github.com>
Co-authored-by: gouyu-hou <213296172+gouyu-hou@users.noreply.github.com>
2026-10-31 18:05:16 +08:00
Totoro
44e372dd7f
feat(runtime): support Queue and Steer during active Web runs (#7729)
* 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>
2026-10-31 11:35:36 +08:00
Dragon ze
5d2749b05b
fix(gateway): read the live config off the loop for thread token usage (#7727)
* fix(gateway): read the live config off the loop for thread token usage

build_context_usage resolves the model capacity through deps.get_config,
which revalidates config.yaml and extensions_config.json on every call, so
each /threads/{id}/token-usage request paid that file IO on the shared
Gateway loop.

* docs(changelog): record the thread token-usage config read move

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-31 08:28:01 +08:00