1645 Commits

Author SHA1 Message Date
Guo-Yixin
d8bdd7552c
fix(models): tolerate null Codex response usage (#6335) 2026-10-06 08:11:05 +08:00
Daoyuan Li
2a81f913a5
fix(projects): synchronize document character-count cache (#6315) 2026-10-06 08:09:15 +08:00
Grapette.L
2523be658b
fix(discord): read allowed_guilds as IDs and deny every guild on an unreadable allowlist (#6338)
* 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>
2026-10-06 07:56:10 +08:00
XuRuuuy
a9c8f79dfe
fix(gateway): resolve the agent factory off the event loop (#6206)
* 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>
2026-10-06 07:52:47 +08:00
haochen
8d5654363e
fix(wechat): enforce outbound limits while reading attachments (#6309)
* fix(wechat): enforce outbound limits while reading attachments

* fix(wechat): centralize outbound read overflow rejection

---------

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-06 07:45:18 +08:00
Hyeonsang Cho
b9ee8af328
fix(skills): keep custom-skill edit and storage lookups off the event loop (#6332)
* 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>
2026-10-06 07:20:04 +08:00
sxh
a72c6489da
fix(skills): windows interpreter scripts outside scripts/ escape security scanning (#6321)
* 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>
2026-10-06 07:17:22 +08:00
haotianqi666
aa7846823d
fix(uploads): accept uppercase markdown companions (#6308)
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
2026-10-06 07:10:55 +08:00
NanPan
c5b9964c25
fix(aio): keep renewal shutdown retryable (#6317)
* fix(aio): keep renewal shutdown retryable

* test(aio): cover renewal shutdown retry

* test(aio): stabilize renewal shutdown retry liveness

* fix(aio): preserve signal forwarding on shutdown timeout

* test(aio): match renewal shutdown timeout diagnostic
2026-10-06 07:08:33 +08:00
NanPan
9c5e5253ea
fix(telegram): retain polling worker on stop timeout (#6333)
* fix(telegram): retain polling ownership on stop timeout

* test(telegram): cover stop timeout ownership retry

* docs(channels): pin Telegram stop ownership
2026-10-06 07:00:09 +08:00
Nan Gao
5872d07e9a
fix(gateway): gate declared multi-instance deployments and require the redis stream bridge (#6328)
* 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>
2026-10-05 22:34:57 +08:00
Hyeonsang Cho
c2f31e440e
fix(knowledge): load the catalog's agent config off the event loop (#6313)
* 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>
2026-10-05 22:17:09 +08:00
subhash7476
a558884c3a
perf(summarization): skip the summarizer LLM call for an input already proven no-op (#6318)
* 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>
2026-10-05 22:04:36 +08:00
Totoro
cdbcc0d9e9
feat(scheduled-tasks): show goal outcomes and end conditions in run history (#6326)
* 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>
2026-10-05 21:56:04 +08:00
NanPan
78150e9b30
fix(subagents): keep isolated-loop shutdown ownership retryable (#6316)
* fix(subagents): retain isolated-loop shutdown ownership

* fix(subagents): reap stopped isolated loop before replacement

* test(subagents): cover pending-loop recovery after worker exit

* docs(subagents): document pending-loop recovery

* fix(subagents): revalidate pending loop recovery
2026-10-05 21:55:01 +08:00
siye566
d8ca2c9358
test(community): make URL offload regressions DNS-independent (#6314)
* test(community): make URL offload regressions DNS-independent

* test(community): reject unsupported DNS fixture families

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 21:53:55 +08:00
Willem Jiang
cfe2023df3
test(backend): run make test in four parallel shards (#6324) 2026-10-05 21:28:08 +08:00
Hyeonsang Cho
0223d949bf
fix(threads): remove a deleted thread's files off the event loop (#6319)
* 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>
2026-10-05 21:27:46 +08:00
Guo-Yixin
9f8a59da80
fix(models): skip undecodable Claude descriptor handoffs (#6323) 2026-10-05 20:21:31 +08:00
jay
7abb973f72
fix(harness): detect shell pipe variants with sudo, full paths and other shells (#6312)
* fix(harness): detect shell pipe variants with sudo, full paths and other shells

The shell-curl-pipe-shell rule only matched the bare 'curl ... | bash'
spelling. The variants that real install scripts actually ship —
'sudo bash', 'sudo -E bash', '/bin/bash', 'zsh'/'dash'/'fish', and
line-continued pipes — produced no finding at all, so a HIGH-severity
remote-code-execution pattern went unreported (#6310 lists the table).

The pattern now tolerates an optional sudo/doas-style flag prefix, full
interpreter paths, and line continuations, and covers the common shell
names. Non-shell pipes (jq, tee) stay unreported.

Fixes #6310

* test(skillscan): restore lost parametrize decorator in merge resolution

* test(skillscan): fix indentation of the http_host parametrize block

* style: ruff-format the skillscan test file

* fix(skillscan): bound pipe matching and handle real continuations

* fix(skillscan): tolerate continuations between sudo, its flags and the shell

Verified undetected until now: `curl ... | sudo \
  bash` and the -E
variant (LF and CRLF). Whitespace runs inside the sudo segment use the same
disjoint (?:\s|\\r?\n)+ shape as the pipe side; value-bearing flags such as
sudo -u root stay intentionally out of scope and are documented in the skills
AGENTS.md note. 17 positive / 6 negative variants verified red-to-green.
2026-10-05 19:53:01 +08:00
Grapette.L
d67a9e4d74
fix(extensions): measure the team response size gate in UTF-8 bytes (#6274)
* fix(extensions): measure the team response size gate in UTF-8 bytes

json.dumps escapes non-ASCII by default, so a CJK clarification answer
counted ~6 escaped bytes per character and an emoji 12, while the plugin
documents 4,000 characters / 8,000 UTF-8 bytes and its own UI validates
exactly those two bounds. A 漢 answer of 2,000 characters (6,000 UTF-8
bytes, inside the bound) was refused by the gate.

Serialize with ensure_ascii=False so the gate measures the UTF-8 bytes it
names, matching the budget walkers in the same module.

* fix(agent-teams): honor the raw UTF-8 response boundary

Measure string answers without JSON quotes or escapes, retain the structured JSON byte limit, and reject malformed Unicode with the normal response validation error. Add boundary and state-preservation regressions and document the response budgets.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 19:45:54 +08:00
wgwfn3
7d50f3fc7b
fix(sandbox): keep the exit marker last when appending audit warnings (#6307)
* fix(sandbox): keep the exit marker last when appending audit warnings

The medium-risk warning was appended after the bash exit marker and the ToolMessage was rebuilt from four fields. The subagent executor anchors its exit-status regex on the trailing 'Exit Code: N' (and fullmatches 'Command exited with code N'), so a failed command was harvested as success; the rebuild also dropped deerflow_tool_meta, artifact and id. Insert the warning before a trailing marker, leave a bare remote marker untouched, and copy the message instead of rebuilding it.

Document the warning placement in agents/middlewares/AGENTS.md.

* docs(changelog): reference #6307 in the sandbox audit entry

* fix(sandbox): keep a trailing remote exit marker last as well

Match the full sandbox.tools._BASH_EXIT_MARKER_TAIL_RE alternation when
choosing where to insert the medium-risk warning, so output that ends in
`Command exited with code N` after other text keeps the marker last too.

* test(sandbox): import the middleware tail regex inside its test

A module-level import of the new name made the whole file fail to collect
against main, hiding which tests the fix turns green.
2026-10-05 19:36:05 +08:00
PeaceMaker-best
fcee85b822
feat(storage): persist viewed images as shared blobs (#6160)
Store validated image bytes in the configured content-addressed blob backend and resolve checkpointed refs before local compatibility paths.

Refs: #4189

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-10-05 19:06:16 +08:00
JasonH
22623d1b00
fix(projects): validate explicit document names before staging (#6287) 2026-10-05 18:38:22 +08:00
LingYi
3a2f768d4c
fix(tui): isolate callbacks from interrupted runs (#6302)
* fix(tui): isolate callbacks from interrupted runs

* fix(tui): recover when agent worker creation fails

---------

Co-authored-by: LingYi-Liang <270316896+LingYi-Liang@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 16:47:55 +08:00
Daoyuan Li
4d9f1d7aa0
fix(extensions): exclude unadmitted team waits from active budget (#6298)
* fix(extensions): exclude unadmitted team waits from active budget

* docs(extensions): explain frozen team input reservation
2026-10-05 16:10:07 +08:00
Guo-Yixin
2970aa4323
fix(scripts): reject invalid Claude Keychain credential containers (#6281)
* fix(scripts): validate Claude Keychain export credentials

* fix(scripts): reuse validated Keychain access token
2026-10-05 15:39:32 +08:00
NanPan
6cdc600895
fix(e2b): keep shutdown retryable while maintenance workers are live (#6244)
* fix(e2b): retain teardown ownership across maintenance timeout

* fix(e2b): fence acquisition and preserve signal forwarding

* test(e2b): cover shutdown admission and signal forwarding

* docs(e2b): pin shutdown admission and signal invariants

* fix(e2b): scope deferred-cleanup signal handling

* test(e2b): exercise dedicated deferred-cleanup signal error

* style(tests): format E2B shutdown regression tests

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 15:37:33 +08:00
Hyeonsang Cho
7f2c0fbe0c
fix(persistence): wait out a peer's Postgres bootstrap past command_timeout (#6306)
* fix(persistence): wait out a peer's Postgres bootstrap past command_timeout

The bootstrap advisory lock was taken with a blocking pg_advisory_lock on
the app engine, whose asyncpg command_timeout (30s by default) also bounds
that statement. A second Gateway instance therefore failed startup with
TimeoutError whenever another instance's migration outlasted the timeout.

Poll the non-blocking pg_try_advisory_lock instead: each attempt returns
immediately and stays bounded by command_timeout, while the wait lasts as
long as the holder's migration. The wait is logged once.

* docs(changelog): reference #6306 in the bootstrap lock entry

* docs(persistence): describe polled bootstrap lock in the migrations guide

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 15:33:29 +08:00
jay
fff9d185c5
fix(harness): normalize local HTTP host case in skillscan (#6280)
* fix(harness): normalize local HTTP host case in skillscan

_LOCAL_HTTP_HOSTS holds lowercase names and every membership test
compared the extracted host verbatim, so an endpoint written with any
uppercase letter (e.g. http://LOCALHOST:8080) was classified as an
external endpoint: non-root deployments failed with EACCES while root
deployments silently wrote plans outside the workspace.

Normalize the host to lowercase in _http_host and in the
network-cleartext/local scan branch so all three call sites agree.

Fixes #6278

* test(skillscan): cover local host case and external endpoint controls

* fix(skillscan): parse IPv6 hosts and detect metadata case variants

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 15:17:03 +08:00
Guo-Yixin
57a2d7fa70
fix(models): decode CLI credential files as UTF-8 (#6277)
* fix(models): decode CLI credential files as UTF-8

* fix(doctor): accept BOM CLI credentials consistently
2026-10-05 14:47:14 +08:00
quifox
420b39ed90
fix(acp): apply invocation timeout to initialization and session creation (#6292)
* fix(acp): apply invocation timeout to initialization and session creation

* fix(acp): preserve SDK timeout errors before the invocation deadline
2026-10-05 14:39:59 +08:00
Shxiao
b85e4ae74b
fix(channels): serialize lifecycle mutations under per-channel lock (#6304)
* fix(channels): serialize lifecycle mutations under per-channel lock

* fix(channels): invalidate stale config snapshots across lifecycle mutations under per-channel lock
2026-10-05 14:35:51 +08:00
NanPan
8237dd75b3
fix(boxlite): keep private-loop shutdown retryable (#6283)
* fix(boxlite): keep private-loop shutdown retryable

* test(boxlite): cover private-loop shutdown retry

* docs(sandbox): pin BoxLite loop shutdown ownership

* fix(ci): format BoxLite shutdown changes and trim guidance

* fix(boxlite): drain acquisitions before shutdown teardown

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 13:38:17 +08:00
Hyeonsang Cho
52046aa82e
fix(projects): convert shelf documents outside the SQLite write lock (#6305)
* fix(projects): convert shelf documents outside the SQLite write lock

Lazy conversion ran pymupdf/markitdown inside the BEGIN IMMEDIATE
transaction that serializes the derived/converted.md publish against
trash and purge. On SQLite that lock is database-wide, so every unrelated
writer (run status, thread metadata, the scheduler) waited for the whole
conversion and failed with "database is locked" past busy_timeout (30s).

Conversion now writes into .staging/ outside any transaction. The
document row lock is taken only to revalidate the live row and atomically
rename the output into place (publish_under_live_lock, renamed from
convert_under_live_lock); a row trashed or purged meanwhile discards the
staged output, so nothing is published after purge. A read-only probe
skips converting rows that are already gone, a failed conversion is
still revalidated so a purged row reports content_missing, and the
conversion is drained on cancellation so its staging file is always
removed.

* docs(changelog): reference #6305 in the shelf conversion lock entry

* fix(projects): share one conversion across concurrent first reads

A first-read conversion holds a bounded file-IO pool thread for its
whole duration. With the conversion moved out of the row lock,
concurrent first reads of one document each converted on their own
thread, so a burst of reads could starve unrelated offloaded file work.

Route the conversion through a single-flight keyed by (user, document,
project): the first reader leads and the rest wait on its result
without holding a pool thread. The map holds concurrent.futures.Future
values under a threading.Lock because the read tool also runs on the
isolated subagent loop. Followers wait through asyncio.shield, so a
cancelled follower cannot cancel the shared future, and a leader that
is cancelled or raises hands the work to a follower instead of failing
it.
2026-10-05 13:34:19 +08:00
siye566
e8f1e269e5
fix(uploads): exclude punctuated numeric columns from document outlines (#6266)
* fix(uploads): exclude punctuated numeric columns from outlines

* fix(uploads): filter currency and later numeric outline columns

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 13:20:00 +08:00
sxh
ef76f085a3
fix(skills): stop reporting an env path, shebang or flag as an env dump (#6297)
* fix(skills): stop reporting an env path, shebang or flag as an env dump

* fix(skills): report shell env dumps only at a command position

`shell-env-dump` matched the word `env` anywhere it was not preceded by a path, dot, dash or word character, so ordinary argument text (`echo env`) and comment text (`# env`, `# export -p`) were still reported as environment dumps. Anchor the match to a shell command position -- the start of a line or right after a `;`, `&`, `|`, `(`, `)` or `{` separator, optionally behind `NAME=value` assignment prefixes -- and add regression cases for argument and comment text.

* fix(skills): ignore an env dump inside a comment or heredoc body

`shell-env-dump` matches `env`, `printenv` and `export -p` at a command
position, but the text it sees is still raw shell. A `;` inside a comment
(`# documentation; env is only an example`) and a bare `env` line inside
heredoc data both sit at a command position on paper while the shell runs
neither, so both were reported as environment dumps.

Reduce the text to shell code before matching: a quote-aware pass strips
comments (only a `#` at a word start opens one) and blanks heredoc bodies,
covering `<<-` tab stripping and quoted delimiters, while keeping one output
line per input line so reported line numbers stay correct. Real commands
after a separator (`true; env`) and on the line that opens a heredoc
(`env <<EOF`) still report.

* fix(skills): report env dumps after a keyword and stop masking ${#...}

The command-position anchor for `shell-env-dump` accepted only a line start or a
`;`, `&`, `|`, `(`, `)` or `{` separator, so real dumps were dropped: after a
reserved word that introduces a command (`if true; then env; fi`,
`while true; do printenv; done`), after `exec` (`exec env`) and inside a backtick
substitution (``V=`env` ``). Accept those positions too, with a guard so a path
component (`./then env`) does not arm the anchor, and add the backtick to the
separator set.

A `{` directly before a `#` is not a word start -- bash reads `${#HOME}` as the
length operator and `}#` as part of a word, while a brace group needs the space
of `{ # ...`. The comment heuristic no longer treats `{` and `}` as comment
openers, so `n=${#HOME}; env` keeps its `; env` and is reported again, while
`${env}` (a variable read, not a command) stays silent.

---------

Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
2026-10-05 12:25:41 +08:00
Daoyuan Li
49dcd49f91
fix(agents): keep malformed todo statuses recoverable (#6299)
* fix(agents): keep malformed todo statuses recoverable

* docs(agents): fit todo guidance within inherited size limit

* docs(agents): clarify todo attribution guidance

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 12:21:39 +08:00
Jun
57d946b730
fix(browser): drain live cleanup before releasing session (#6284)
* fix(browser): drain live cleanup before releasing session

* test(browser): cover cancellation precedence during cleanup

* style(browser): separate cancellation regressions
2026-10-05 12:15:21 +08:00
siye566
335c383147
fix(paths): reject Windows console device aliases (#6279) 2026-10-05 10:25:51 +08:00
Hyeonsang Cho
c540399a5e
fix(gateway): scope per-run reads to the run's data identity (#6282)
* fix(gateway): scope per-run message and event reads to the run's data identity

start_run stamps run-event rows with the raw trusted owner id, but
GET /runs/{rid}/messages and /runs/{rid}/events called the event store with
user_id=AUTO, which resolves to the internal caller's make_safe_user_id form.
On the database event store, owners whose id contains characters outside
[A-Za-z0-9_-] (IM channel ids such as "feishu:owner-777") therefore got an
empty list. The regenerate source-run scan had the same mismatch and fell
back or returned 409.

Pass _run_scope_user_id() explicitly, as the thread message routes already
do (#5448), and add user_id to RunEventStore.list_messages_by_run() so every
backend accepts it like list_messages()/list_events(). Browser and API
sessions keep their per-user filter.

* fix(gateway): scope workspace-changes and archive reads to the run's data identity

The same raw-owner mismatch reached two more run-scoped routes. On the SQL
stores, GET /runs/{rid}/workspace-changes reported no changes because its
event lookup used user_id=AUTO, and GET /runs/{rid}/artifacts/archive
answered 404 because both its run-row lookup and its run.delivery event
lookup did.

Pass _run_scope_user_id() to both, and let get_workspace_changes_response()
forward an explicit user_id to the event store.

* docs(changelog): reference #6282 in the run-scope read fix entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 10:23:55 +08:00
Hyeonsang Cho
56aa71c583
fix(runtime): keep renewing the lease while a staged terminal run finalizes (#6263)
* fix(runtime): keep renewing the lease while a staged terminal run finalizes

With an event store, run_agent stages the terminal status in memory and
commits it to the run store only after the journal flush, delivery receipt,
workspace scan and duration checkpoint. _renew_leases selected runs by local
status, so it stopped renewing as soon as the status was staged. A
finalization longer than lease_seconds + grace_seconds let a peer, or this
worker's own reconciler, claim the still-active row and record the
successful run as an orphan error.

The worker now marks RunRecord.terminal_commit_pending until the deferred
commit is attempted, and lease renewal treats it like the existing
scheduled-goal barrier: keep renewing, release on a confirmed own commit,
and fence when a peer claims the row. A fenced run stops renewing.

* docs(changelog): reference #6263 for the staged terminal lease renewal fix

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-05 07:07:04 +08:00
quifox
cc2ccaa523
fix(channels): handle streamed run errors before dedupe cleanup (#6272)
* fix(channels): handle streamed run error frames

* fix(channels): log streamed run error details
2026-10-05 06:54:32 +08:00
sxh
ee44d1ebc7
fix(skills): report secret assignments whose key is quoted (#6276)
* fix(skills): report secret assignments whose key is quoted

The line-oriented secret sweep required the credential name to be followed
directly by ':' or '=', so a JSON config ('{"api_key": "..."}') never matched
while the same binding in YAML, .env, .ini or shell did. Allow one optional
quote between the key and the separator.

* fix(skills): match secret assignments whose key has a separator prefix

The line-oriented sweep anchored the credential word with `\b`, which cannot
land after `_` because `_` is a word character. So `access_token`,
`client_secret` and `MY_API_KEY` — the common spellings in OAuth and
service-account config — matched nothing, and `_SECRET_TOKEN_PATTERNS` does
not backstop JWT- or `GOCSPX-`-style values, so such a package produced no
finding at all.

Guard the word with "not preceded by an alphanumeric" instead: a separator
(`_`, `.`, `-`) may now introduce it, while a run of letters (`tokenizer`,
`secretive`, `credentials_file`) still stays quiet.

---------

Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
2026-10-04 21:15:18 +08:00
quifox
e8de7584b7
fix: include completed child usage in lead budget checks (#6218)
* fix: include completed child usage in lead budget checks

* test: require completed usage middleware in lead budget regressions

* docs: fit middleware guidance within the size budget

Condense the subagent, loop, and token budget guard descriptions while retaining their limits, lifecycle rules, and completed child usage ordering.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-04 21:14:35 +08:00
sxh
4abba38734
fix(skills): recognise eval fixture paths behind an earlier evals segment (#6271)
is_eval_fixture_path returned on the first 'evals' segment, so a path whose
'evals/fixtures/' came after a plain 'evals' segment read as a non-fixture.
Move the 'fixtures' test into the loop condition so every segment is checked.

Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
2026-10-04 20:47:15 +08:00
Hyeonsang Cho
12fb53e5de
fix(channels): follow Buzz threads mapped in the connection repository (#6232)
* fix(channels): follow Buzz threads mapped in the connection repository

With channel_connections enabled, ChannelManager stores a bound author's
thread mapping only in the connection repository, but Buzz's thread-follow
gate read only the JSON ChannelStore. engaged_thread was therefore always
False for authors bound with /connect, and every unmentioned reply in a
thread the bot was already answering was silently dropped.

Buzz now resolves the connection identity before the mention gate and looks
the thread up through lookup_thread_id, a helper extracted from
ChannelManager._lookup_thread_id that both sides now share, so the gate and
the manager can no longer disagree about which store owns a mapping.

* fix(channels): drop the manager's own JSON-store read in the skill whitelist

_resolve_available_skill_names fell back to reading the JSON ChannelStore
when the caller's lookup_thread_id found nothing. For a bound message that
lookup is repo-only, so a legacy unbound mapping for the same chat decided
which custom agent's skill whitelist applied. The parameter is now required
and the caller passes "" when nothing is mapped.

Also pin the Buzz thread-follow fix against the real SQL connection
repository (bound through a real /connect) instead of a fake.

* docs(changelog): reference #6232 for the Buzz thread-follow fix

* fix(channels): log Buzz drops of unmentioned replies with no engaged thread

The thread-follow gate was the one drop in _handle_chat_event without a
log line. Log it at DEBUG with the thread root and whether the author was
bound, matching the allowlist drop, so a report that the bot stopped
following replies can be told apart from a lost bind or missing mapping.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-04 20:38:43 +08:00
Daoyuan Li
a9046508cf
fix(artifacts): bound reads before saving edited outputs (#6250) 2026-10-04 19:39:50 +08:00
Daoyuan Li
4dad94dcc0
fix(tui): guard conversation switching and invalid resume references (#6249)
* fix(tui): guard conversation switching and invalid resume references

* fix(tui): discard late stream actions after conversation switches

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-04 19:28:32 +08:00
siye566
a6c2e913b8
test(plugins): guard symlink-dependent browser asset cases (#6267) 2026-10-04 19:27:54 +08:00