* fix(agents): bind read marks to the current tool call
* test(agents): make file and proxy baseline checks portable
* fix(agents): address read-mark review diagnostics and cleanup
* docs(agents): keep read-mark guidance within the chain budget
* fix(memory): drop punctuation tokens from jieba relevance ranking
jieba.cut emits punctuation as standalone tokens and tokenize() only filtered whitespace, so commas and full stops counted as query matches, inflated near-duplicate similarity, and consumed the 128-token budget. Keep only tokens with an alphanumeric character, matching the regex fallback and the FTS5 adapter's query filter.
Document the rule in agents/memory/AGENTS.md.
* docs(changelog): add the DeerMem jieba punctuation fix
* docs(changelog): add the PR reference for the jieba punctuation fix (#6388)
* docs(memory): document jieba relevance token filtering
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skillscan): catch a shell behind a sudo option that takes a value
`shell-curl-pipe-shell` skips a leading `sudo` and its standalone flags, but
`| sudo -u deploy bash` left the matcher parked on the option value, so a
remote script piped into a shell went unreported. Let a value-taking sudo
option consume its value as well.
* fix(skillscan): keep the sudo option branches mutually exclusive
A chain of value-taking options (`curl ... | sudo -u -u -u ... cat`) matched both sudo alternatives inside the lazy repeat, so re.search explored every one-/two-token partition before the non-shell tail failed: a 211-byte input took exponential time and did not return. Excluding value-taking options from the standalone branch lets each token be consumed exactly one way, so a failing chain stays linear while `sudo -u deploy bash` and `sudo -E bash` still report.
* fix(skillscan): treat `sudo -h` as taking a value in the download pipe
The hand-maintained class of sudo options that take a separate value
missed `-h host` (the deprecated remote-host option), so
`curl … | sudo -h host bash` parked the matcher on the host name and
produced no finding — the same miss shape this class exists to prevent.
Add `h`, note that the class is hand-maintained, and pin every letter of
it with a contract test so a dropped letter fails instead of regressing
to a silent miss.
Also build the repeated-option payload in the linearity regression from
the module constant rather than a second inline copy, so the byte-count
pin and the subprocess exercise the same string.
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix(wechat): deny all users when allowed_users has no valid ID
A single iLink user ID was split into characters, and a list with no
usable ID collapsed to an empty set that the inbound gate read as
allow-all.
* fix(wechat): fail closed on numeric and mapping allowed_users
Integer-valued floats now match the sender's integer text. Fractions,
non-finite numbers, and mappings no longer admit anyone, and a denied
sender is dropped before inbound media is downloaded.
* docs(channels): keep WeChat guidance within size budget
* fix(wechat): warn on scalar allowlist separators
* refactor(channels): share fail-closed allowlist parsing
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(middleware): keep the bash exit marker last after budget rewrites
* fix(middleware): keep exit marker at exact-fit fallback budget; harden test cleanup
* docs(changelog): reference #6354 in the middleware entry
* fix(extensions): publish a snapshot restore atomically
_FileSnapshot.restore() rolled a failed extension install back with
Path.write_bytes, which opens with O_TRUNC: if the process died during the
rollback, pyproject.toml / uv.lock / config.yaml were left half written and the
failed install took the checkout with it. Publish through a temporary file plus
os.replace, the pattern the rest of the codebase already uses.
* fix(extensions): preserve snapshot permissions on rollback
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skillscan): gate the directory scan file size before reading
scan_skill_dir() read every candidate file with read_bytes() before any
size check; MAX_FILE_BYTES was only recorded as a finding afterwards.
Measured: one 300MB file (4.7x the 64MB limit) drives a 600MB peak
allocation and a 20s scan, linearly extrapolating to an OOM for larger
planted files - reachable via review/analyzer.py on content that never
passed the archive preflight gates.
Gate with stat() first and bound oversized reads to MAX_FILE_BYTES + 1,
mirroring _read_archive_member so both entry points share the same
discipline. Regression test plants an oversized file and fails if
read_bytes is used on it.
Closes#6358
* fix(skillscan): collapse to a single bounded read per file
Per review: Path.open + read(MAX_FILE_BYTES + 1) returns the whole file
when it is smaller than the limit, identical to read_bytes for regular
files - removing the size-conditional branch and closing the stat-to-read
race where a file growing past the limit between calls would still be
read unbounded. file_size from stat stays the source of truth for the
package-oversized-file evidence.
* test(skills): skip MAX_PATH-exceeding anchors on Windows (ref #6353)
* test(skills): skip MAX_PATH-exceeding anchors on Windows (ref #6353)
* test(skills): skip MAX_PATH-exceeding anchors on Windows (ref #6353)
* style(tests): fix import order in project blocking-io tests
* fix(tests): narrow Windows MAX_PATH skip to only the two failing attach anchors
Per review: module-level skip also skipped ingestion/thread-files
anchors that are not MAX_PATH-bound. Now only the two attach staging
anchors skip on Windows.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(make): name what make clean deletes and refuse under a live gateway
`make help` described `make clean` as cleaning up "temporary files", but it
deletes backend/.deer-flow: the local database, users, threads, uploads,
memory, and secrets. Both Docker stacks mount that directory into the
deer-flow-gateway container, and `make stop` only stops local services, so
the data could be deleted under a running Gateway.
Say what is deleted in the help text and before deleting, and refuse while
the deer-flow-gateway container is running. The check runs before `make
stop`, which would otherwise stop the running stack's sandboxes first; a
missing or unreachable Docker passes.
* docs(changelog): reference #6351 for the make clean guard
* fix(make): share make clean's data list and pin stop before the delete
The help line listed "database, threads, uploads, secrets" while the
deletion notice also named users and memory. Define the list once in
RUNTIME_DATA_CONTENTS and use it in both.
Running sandbox containers left behind by `make down` are already stopped
by `make stop` (serve.sh --stop reaps deer-flow-sandbox*), which runs after
the guard and before the delete. Note that in the guard script, and pin
the ordering: the stub stop now records that the data still exists.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(tui): wait for an interrupted run before sending to its thread
Textual cannot stop a thread worker, so after an interrupt the agent
stream keeps running until its next event. A tool step already in
progress still completes and checkpoints while a new prompt runs on the
same thread; whichever run checkpoints last becomes the thread's state,
so the new turn could drop out of the conversation history.
Track when each worker actually starts and returns (Textual reports a
cancelled thread worker finished at once) and reject sends to a thread
whose interrupted worker is still running. Other threads stay usable,
and a worker cancelled before it started holds nothing.
* docs(changelog): reference #6350 for the interrupted TUI run fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(jina): honor bounded Retry-After recovery
Closes#6237
Honor server-directed waits for 503 and validly hinted 429 responses within the existing attempt and time budgets. Preserve cancellation, default single-attempt behavior and the tool schema. Add deterministic protocol regressions and stabilize mock setup in the real deadline test.
Signed-off-by: 97three <2212371308@qq.com>
* test(jina): pin local backoff for hinted 502 and 504
Signed-off-by: 97three <2212371308@qq.com>
---------
Signed-off-by: 97three <2212371308@qq.com>
* fix(models): preserve structured MindIE tool results
Signed-off-by: lovett55 <xbzz101800@163.com>
* fix(models): redact and bound structured MindIE tool results
* fix(models): keep structured MindIE ToolMessage content visible
_fix_messages flattened list content keeping only string and type=text
blocks, so a JSON tool result became an empty <tool_response> and the
model could not see the tool output on the next turn. Serialize type=json
blocks onto the same text channel; the existing XML escaping applies to
the serialized text unchanged.
Fixes#6010
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(models): harden the MindIE json channel from review
Review follow-ups on the structured-content fix:
- json.dumps can also raise ValueError (circular reference); catch it
alongside TypeError so the payload degrades to str() instead of
failing the whole request normalization.
- A {"type": "json"} block with no json key keeps the old drop
behavior instead of emitting a literal "null" into the frame.
- PiiRedactionMiddleware now redacts json block payloads leaf-wise
(nested dict/list strings scanned, keys and non-string leaves kept),
so the newly serialized channel participates in the configured
redaction policy for user messages and remote tool results alike.
Without it, redaction-enabled deployments would ship structured PII
raw once the provider serializes the block.
Tests: fallback degrade for both exception paths, missing-key drop,
leaf-wise redaction on the model-call and tool boundaries, and a
middleware-to-MindIE regression pinning redaction through provider
serialization. Red/green verified: the 6 new redaction/json tests
fail without the source changes, 155 pass with them.
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(models): close MindIE structured tool redaction gaps
* fix(agents): budget structured json tool output before provider normalization
Willem's P2 on #6208: _message_text() returned None for {"type": "json"}
blocks, so a large structured ToolMessage slipped past both the tool-call
and history budget gates and reached MindIE's serializer at full size,
ignoring the externalize threshold and the fallback cap.
Render those blocks with the same json.dumps(..., ensure_ascii=False)
serialization the MindIE adapter applies (str() fallback included), so
they count toward the budget everywhere _message_text is consulted.
Images and other non-textual blocks still skip enforcement.
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(mindie): render json blocks for tool messages only
InputSanitizationMiddleware neutralizes strings and text blocks, so a
{"type": "json"} block smuggled into a genuine user message reaches
_fix_messages with its payload untouched. Rendering it into the text
channel handed the model an unescaped, unframed injection (e.g. a
literal <system-reminder>), bypassing the guardrail entirely.
Gate the json-block serialization on ToolMessage: structured tool
results keep riding the text channel (and still get html.escape plus
the <tool_response> framing), while user/assistant json blocks go back
to the old drop behavior. Adds unit coverage for the drop paths plus a
chain regression that runs a genuine user message through the
sanitization middleware and then _fix_messages, asserting the smuggled
payload never reaches the model-bound text.
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(models): preserve configured budgets in combined MindIE fix
---------
Signed-off-by: lovett55 <xbzz101800@163.com>
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: lovett55 <xbzz101800@163.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): drain direct release across cancellation
* test(sandbox): retain direct release across cancellation
* fix(sandbox): drain rollback release across cancellation
* test(sandbox): retain rollback release across cancellation
* test(sandbox): format rollback cancellation regression
* fix(deploy): wire the multi-replica prerequisites into the Helm chart and compose
The chart shipped gateway.replicas: 1 with a README citing the long-closed
issue #3948, while its rendered config lacked run_ownership heartbeats and
db run events: raising replicas made every starting Pod write its peers'
lease-less runs off as orphans, and the Gateway's startup gate could not
notice because its worker count only sees one Pod.
Enable heartbeats and db run events in the default config, export
DEER_FLOW_MULTI_INSTANCE from the replica count for the startup gate,
generate and inject a shared AUTH_JWT_SECRET (per-Pod .jwt_secret files
logged users out across replicas), declare a surge-then-drain rollout
strategy, render a PodDisruptionBudget when replicas > 1, and bound
uvicorn's graceful shutdown inside a 90s termination grace period.
docker-compose gets the same uvicorn bound and a 60s stop_grace_period so
the 30s memory flush is no longer SIGKILLed. Docs and comments now list
what actually remains single-instance: IM channels, WeChat QR login and
browser tools.
* fix(deploy): honor existingAppSecret, declare multi-instance explicitly, cover the dev stack's shutdown
Follow-up to the multi-replica prerequisites wiring, addressing review:
- `deer-flow.appSecret` returns `existingAppSecret` when it is set. The
gateway and frontend kept referencing the generated `<release>-app`
Secret, which the chart stopped rendering, so every Pod sat in
CreateContainerConfigError. The value is now declared in values.yaml.
- `gateway.multiInstance` (boolean, default null) overrides the
replica-derived DEER_FLOW_MULTI_INSTANCE, PodDisruptionBudget and
AUTH_JWT_SECRET requirement for deployments scaled with kubectl or an
HPA, which change the replica count without a Helm upgrade.
- AUTH_JWT_SECRET is required, not optional, whenever the gateway is
multi-instance: concurrently booting Pods otherwise race to write
different `.jwt_secret` files on the home volume and sign sessions with
different keys.
- `gateway.uvicornGracefulShutdownSeconds: 0` is honored instead of being
swallowed by `default 10`.
- docker-compose-dev.yaml and dev-entrypoint.sh get the same uvicorn
graceful-shutdown bound and `stop_grace_period: 60s`.
- The chart README documents the one-time sign-out when upgrading a
release that predates AUTH_JWT_SECRET and how to seed the previous key
into the Secret first.
- Tests read the shutdown budget from the Gateway through a shared helper
instead of hard-coding 5/5/30, the raw-template string matches give way
to `helm template` renders, and new renders cover multiInstance,
existingAppSecret, a disabled PDB and a zero graceful-shutdown bound.
* docs(changelog): link the multi-replica deploy entry to #6347
* fix(deploy): budget every bounded teardown hook and flag the multiInstance PDB window
Review follow-ups on #6347:
- The shutdown budget modeled only channel stop + run drain + memory flush
(40s). The lifespan also runs the startup trash sweep, the notification
delivery worker stop, browser-session close and the MCP session-pool
close, each bounded at _SHUTDOWN_HOOK_TIMEOUT_SECONDS, plus a 1s
retrieval-warm wait: about 61s worst case. _gateway_shutdown_budget.py
now reads all of them from the Gateway, a drift guard pins the hook
count against app.py, and both compose files move stop_grace_period
from 60s to 90s (Helm's 90s already had room: 5 + 10 + 61). Comments
and the chart README cite the full arithmetic.
- gateway.multiInstance: true renders the PodDisruptionBudget immediately,
which blocks drains of the single Pod until the scale-up happens; the
README, values comment and both deployment guides say so and name
podDisruptionBudget.enabled: false as the interim escape hatch.
* fix(sandbox): stop pinning a stale agent skill view once host bash is allowed
A restricted Agent run leaves a thread skill view behind. SandboxMiddleware
refreshes that view only while the provider can enforce skill isolation;
with host bash allowed LocalSandboxProvider cannot, so the middleware skips
it and restricted runs are rejected. The provider still mounted the view
whenever it existed, so later unrestricted runs on the thread kept the old
allowlist and could not read other enabled or newly added skills.
Look up the thread view only while isolation is supported, so those runs
get the shared projection the middleware expects. The view stays on disk
and is enforced again once host bash is turned off.
* docs(changelog): reference #6344 for the stale skill view fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): keep line endings in local sandbox reads, writes and str_replace
LocalSandbox opened files in text mode with the default newline=None.
Reads translated CRLF to LF, so str_replace on a CRLF file rewrote every
line as LF and the read-before-write hash missed newline-only changes;
on Windows, writes turned LF content into CRLF (breaking `bash run.sh`).
Read with newline="\n" and write with newline="", keeping line endings
as stored like the remote providers; reads still end lines only at "\n",
the rule count_file_lines and the truncation marker number lines by.
str_replace spells an LF-written old_str/new_str with CRLF when the file
uses it, so multi-line edits still match CRLF files on any provider.
The tool-output budget middleware wrote externalized output the same
way: on Windows the file no longer matched its stamped blob ref and the
next model call deleted it when no blob store was configured. It now
writes with newline="" too.
* docs(changelog): reference #6343 for the line-ending fix
* fix(sandbox): end local grep lines where read_file does
find_grep_matches still opened files with universal newlines, so a bare
"\r" split a line for grep but not for LocalSandbox.read_file, and a hit's
line_number stopped matching the start_line that reads it back. Read with
newline="\n" like read_file, and fold a CRLF terminator back to "\n" so
$-anchored patterns keep matching CRLF lines as before.
* fix(mcp): isolate pooled sessions by ownership domain
Separate deployment and personal stdio sessions even when they share the same runtime server name and thread scope. Preserve deployment call compatibility while carrying personal ownership through Agent wrappers, durable task calls, and disconnect eviction.
Add focused regression coverage for personal discovery wiring and domain-scoped disconnect cleanup.
* test(mcp): address ownership-domain review feedback
Accept the new wrapper keyword in the prefix-overlap test double and reuse MCPPoolDomain for internal task-caller ownership parameters.
* style(mcp): format ownership-domain changes
* style(mcp): apply repository ruff formatting
* fix(middleware): derive the externalized filename from the tool call id
_build_externalized_filename documents that the host-disk and sandbox paths
share it so both produce the identical naming scheme, but it used a random uuid
suffix and ignored tool_call_id: the same call got a different name every time,
so the two paths could not agree and externalizing one output twice left two
files behind. Derive the name from the call id instead, sanitised the same way
tool names are, and drop the now unused uuid import.
* fix(middleware): hash externalized call IDs and content
* fix(middleware): catch sandbox filename encoding failures
* fix(middleware): handle host output encoding failures
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(models): exclude thinking blocks from Claude cache breakpoints
* fix(models): declare Claude test SDK and clarify caching docs
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* test(discord): expose allowlist parsing failures instead of opening the bot
config.example.yaml documents channels.discord.allowed_guilds as a value that
"can also be a single guild ID", but the constructor iterates it: a scalar
string becomes one entry per character, and an unquoted YAML int raises
TypeError. Whatever remains is gated by truthiness, so an allowlist with no
parseable entry leaves the set empty and admits every guild.
* fix(discord): read allowed_guilds as IDs, and fail closed when unreadable
The allowlist was built by iterating the configured value, so the documented
single-guild shorthand ("can also be a single guild ID" in
config.example.yaml) became one entry per character: allowed_guilds: "321"
admitted guilds 1, 2 and 3 while blocking 321, and an unquoted YAML int raised
TypeError in the constructor. The empty result then read as "no allowlist", so
a restriction the operator configured could open the bot to every guild the bot
joined.
Parse both ID lists through helpers that treat a scalar as a one-entry list and
drop unreadable entries with a warning, following the telegram channel: an
allowlist with no parseable entry now denies every guild and logs at ERROR.
Mention exemptions keep their IDs verbatim, where an unreadable entry only
leaves mention_only in force.
* docs(discord): explain allowlist parsing and deny-all behavior
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(gateway): resolve the agent factory off the event loop
start_run() called resolve_agent_factory() inline, which lazily imports
the full lead-agent assembly stack (middlewares, authz, MCP, jsonschema;
measured at ~8s cold on Windows, multi-second on slower filesystems).
start_run is the single choke point every run-creation path flows
through, so the first run after Gateway startup blocked the event loop
for the whole import, stalling every other request, stream and channel
on the same loop - the loop-stall family issue #5172 fixed for agent
construction itself (the checkpoint-state path already resolves through
the assembly pool).
Resolve the factory via asyncio.to_thread: the first call pays one
worker-thread hop for the import, later runs hit the cached module.
Regression test pins the contract with a probe module whose module-level
__getattr__ records the thread that performed the from-import.
* fix(gateway): isolate factory resolution in the assembly pool
Move import regressions into the strict blocking-I/O suite and cover default-executor isolation, request context, and failure-before-admission ordering.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): keep custom-skill edit checks off the event loop
update_custom_skill built the user-scoped storage, ran the editability
probes, validated the frontmatter through a temporary directory and read
the content being replaced on the Gateway loop; only the write and history
append had been offloaded. Run the pre-scan checks in one worker and the
replaced-content read in another, matching rollback_custom_skill, and build
delete_custom_skill's storage off the loop as well.
* fix(skills): build skill storage off the loop in install and skill_manage
The same sweep that moved the custom-skill edit checks off the loop finds
two more storage lookups on it: the archive-install helper behind both
install routes, and the agent's skill_manage tool, whose lookup resolves
and stats config.yaml on every call. Offload both, and pin that the
caller's user context still reaches the storage lookup now that the edit
route resolves it inside a worker.
* docs(changelog): reference #6332 for the skill storage off-loop fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): scan Windows interpreter scripts outside scripts/
CODE_SUFFIXES is the single definition of "this file is code" that
skills/AGENTS.md requires the installer and SkillScan to import, but it
listed .ps1 while omitting .bat/.cmd/.vbs/.wsf/.psm1. Those files
outside scripts/ therefore got is_code_file() == False, so SkillScan
skipped them as binaries (zero findings, not even
package-undecodable-script) and the installer never reached the
executable=True guardrail scan.
Add the five Windows interpreter extensions to the shared table so both
surfaces see them on every platform.
Refs #6320
* fix(skills): scan Windows script host sibling formats
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(gateway): gate declared multi-instance deployments and require the redis stream bridge
GATEWAY_WORKERS / WEB_CONCURRENCY only count the uvicorn workers of one
process tree, so a Kubernetes Deployment with replicas > 1 (one worker
per Pod) passed every multi-worker startup check while each Pod's
startup orphan reconciliation wrote the other Pods' lease-less runs off
as crashed on every rolling update.
Add `deployment.multi_instance` as an explicit declaration that runs the
same gate, plus a DEER_FLOW_MULTI_INSTANCE env override so deploy tooling
can set the declaration from its replica count without editing
config.yaml (wiring the Helm chart to export it is a separate change).
Both the worker count and the declaration now also require a Redis
stream bridge and refuse an explicit memory sandbox ownership store,
since the memory bridge and ownership store are process-local. The
agent-storage divergence warning, the inbound webhook dedupe warning and
the WeChat QR-login guard honor the same declaration.
* docs(changelog): link the multi-instance gate entry to #6328
* fix(ci): bump chart config_version to 53 and keep AGENTS.md under the size limits
config.example.yaml moved to config_version 53 for the new deployment
section, so the Helm chart's embedded config and its README example
follow. The multi-instance notes in backend/AGENTS.md and
backend/app/gateway/AGENTS.md shrink to one line each: main already sits
within 55 bytes of the per-file hard limit for the gateway guide and
within 163 bytes of the chain hard limit under backend/, and the full
contract lives in backend/docs/CONFIGURATION.md.
* fix(gateway): name every active knob in the multi-process rollback
When GATEWAY_WORKERS > 1 and a multi-instance declaration are both
active, the refusal offered only "Set GATEWAY_WORKERS=1", so an operator
who followed it bounced through a second refusal on
DEER_FLOW_MULTI_INSTANCE / deployment.multi_instance at the next start.
The rollback now withdraws both knobs, and the browser-tools refusal
takes the same path instead of the worker-count-only message.
multi_instance_declaration() returns a frozen MultiInstanceDeclaration
(source, value) with knob/rollback properties instead of a display
string, so deps.py no longer branches on message prose. Tests pin the
combined rollback text and that following it actually clears the gate.
* docs(gateway): keep the gateway AGENTS.md under the size hard limit after merging main
main grew backend/app/gateway/AGENTS.md by 30 bytes, which left it 25 bytes
under the 49152-byte hard limit; the one-sentence multi-instance note from
this branch pushed the merged file to 49164. Shorten that sentence by 18
bytes so scripts/check_agent_guidance.py passes on the merge result.
* ci: re-run checks after test_stop_cancels_a_hung_driver_poll flaked on shard 4
tests/test_mcp_task_service.py::test_stop_cancels_a_hung_driver_poll timed
out once on backend-unit-tests (4) for 2c16206ac. The MCP task service and
its test are identical to main, main's run for cfe2023df passed, and 15
consecutive local runs pass; the test gives service.stop() a 1s budget
while cancellation drain allows 5s, so a loaded runner can exceed it.
Fork contributors cannot re-run a job, hence this empty commit.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(knowledge): load the catalog's agent config off the event loop
Both retrieval-catalog handlers called load_agent_config() directly from
async code. The agent store is synchronous: file IO on the file backend,
a sync SQLAlchemy round trip on the db backend, so a slow disk or
database stalled every other Gateway request. Offload the load with
asyncio.to_thread, matching agents.py, scheduled_tasks.py and the
run-admission path in services.py.
The strict Blockbuster anchors drive both handlers end to end against
real file and SQLite agent stores, plus the unknown-agent 404 path.
* docs(changelog): reference #6313 for the knowledge catalog fix
* Added the Chinese changelog entry back
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* perf(summarization): skip the summarizer LLM call for an input already proven no-op
When the exact rendered summarizer prompt (plus candidate models) previously
produced output byte-identical to the current summary_text, reuse that summary
instead of calling the LLM. Compaction proceeds through the normal path, so
messages, hooks and state transitions are unchanged.
- Only proven no-op inputs are cached (bounded LRU, thread-safe); failed
attempts are never cached and any cache error falls back to the LLM call.
- The middleware:summarize event gains changes.llm_call_skipped.
- Applies to both compact_state and acompact_state.
Builds on the per-call telemetry in the P0 branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(summarization): keep fallback no-ops retryable
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* docs(scheduler): document tool_enabled, goal notices and goal-evaluation change
Follow-up to #6229:
- CONFIGURATION.md: add scheduler.tool_enabled to the example and notes.
- CONFIGURATION.md, README (en/zh/pt): unmet goal runs send a goal-unmet
notice instead of a completion notice, and three unmet runs add an
auto-pause notice; the docs still said only success/failed notify.
- CHANGELOG (en/zh): the #6229 entry now states the default-on goal
evaluation change for scheduled/webhook/autonomous goal runs and that
evaluator usage counts toward run usage.
* feat(scheduled-tasks): show goal outcomes and end conditions in run history
Follow-up to #6229. The API already returns these fields; the tasks page
did not show them, so a met goal read as plain "Success", an unmet
reason code rendered as a red execution error, and the goal-unmet IM
notice pointed to a workspace showing neither objective nor verdict.
- Task detail shows the per-run goal, max automatic runs and end time
when a conversation-created task has them.
- Goal runs show "Goal met" (or met relying on stated assumptions);
unmet runs show a localized reason plus the host code in neutral
styling. The run whose agent asked to stop the schedule is marked.
- Runs without a goal, and failed runs, render as before.
Read-only and frontend-only: no API change.
* docs(changelog): add #6326 scheduled-task goal outcome entry
* fix(scheduled-tasks): show readable unmet reasons instead of raw codes
Unmet runs now show only the localized reason; the host code moves to the
tooltip so it can still be matched with the IM notice. Codes without a
label are still shown verbatim.
* fix(scheduler): show readable goal reasons instead of internal codes
Task detail now localizes the host-written last_error values from goal and
stop finalization (agent stop, automatic pause, unmet reason codes) under a
matching label, and Chinese copy is tightened. Goal-unmet and auto-pause IM
notices state the reason in words instead of printing the raw code; unknown
codes are still not forwarded.
* docs(changelog): describe readable goal reasons in #6326 entry
* test(scheduler): pin host goal strings with a cross-language contract
Address review on #6326:
- Add contracts/scheduled_goal_notes_contract.json listing the task
last_error strings and unmet reason codes the tasks page and IM notices
translate. A backend test checks finalization constants, blocker and
stand-down codes against it; a frontend test checks every listed value
has a label.
- Move the two last_error strings in finalization.py into constants.
- Frontend reason lookup strips the blocked: prefix like the IM map and
now covers goal_not_met_yet, with en/zh labels.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(threads): remove a deleted thread's files off the event loop
DELETE /api/threads/{id} called shutil.rmtree over the thread directory
directly on the event loop, so deleting a thread with a large workspace
stalled every other Gateway request and live SSE stream until the whole
tree was gone. Branch copying in the same router already offloads its
filesystem work through run_file_io; deletion did not.
The removal now runs on the file-IO pool. It is wrapped in await_drained
rather than bare-awaited: cancelling the await abandons the worker, not
the removal, so the durable delete reservation would otherwise release
while files are still being deleted, and a newly admitted run could
write into a directory that is about to disappear.
* docs(changelog): reference #6319 in the thread delete off-loop entry
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>