* fix(channels): honor the configured recursion limit in IM runs
Every IM channel run asked the Gateway for recursion_limit 100, because
the channel manager seeded each run config with it. The Gateway honors a
request value, so the top-level recursion_limit never reached IM runs,
and a budget of 100 could stop a run after a handful of tool-calling
turns (six in one measurement with the default lead-agent middleware;
the count changes with the middleware chain).
IM runs now send no recursion_limit of their own and get the Gateway's
configured default, clamped by max_recursion_limit, like Gateway API
runs. A session config value still takes precedence, and GitHub channel
runs keep their floor of 250. The README and config.example.yaml IM
examples no longer pin session.config.recursion_limit to 100.
* docs(changelog): reference #7521 for the IM recursion-limit fix
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
A cross-process stream bridge (RedisStreamBridge) returns False from
start_producer_lease only after a transient backend error whose partial
lease registration was safely rolled back, leaving nothing tracked. The
admission guard treated that as fatal and raised, surfacing a 500 for an
otherwise healthy run whenever Redis had a momentary blip.
Degrade instead of rejecting: admit the run and log a warning. An
untracked producer falls back to the historical conservative recovery
path (wait for the real END or the retained-stream TTL), so the run is
no less safe than it was before producer leases existed. The lease is
never transferred or finished because none was taken; the worker
re-attempts its own lease independently.
Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Move checkpoint modes, replay, module maps and terminal-ownership details intact into the existing runtime reference. Keep concise invariants and explicit links in AGENTS.md, reducing the effective chain from 99,004 to 80,817 bytes. The existing guidance regression now passes without changing budget thresholds.
* fix(projects): delete read-only documents when purging trash
A permanently deleted project document could never be purged on Windows:
the read-only file attribute makes `Path.unlink` fail with `PermissionError`
(`WinError 5`), where POSIX needs only a writable parent. A restored archive
or a mode-preserving sync sets that attribute, and because the purge re-raises,
the row rolls back and the trashed document keeps coming back with the same
error on every retry.
Clear the attribute and retry the unlink once, mirroring the retry that
`config/paths.py::_clear_read_only_tree` already performs for thread
directories. Any other unlink error still propagates, so the row stays
retryable.
* fix(projects): harden trash purge permission retries
---------
Co-authored-by: Aling33 <along308@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): count code points when previewing table cells and memory facts
artifact-table-preview cut a cell at 120 UTF-16 code units and
memory-settings-page's truncateFactPreview measured and cut fact previews
at 140, so an emoji spanning two units could be split at the cut — the
lone half renders as U+FFFD — and a preview that already fit the
documented code-point budget was still shortened. Both previews now share
truncateToCodePoints in core/utils/text, which measures and cuts by code
points, the same rule briefReason in core/scheduled-tasks/format.ts
already follows.
Add unit tests for the helper and a DOM regression test covering both the
fits-exactly and the cut-inside-an-emoji cases.
* fix(frontend): count code points in the artifact-cell expand gate too
The expandable gate still measured value.length (UTF-16 units), so a
120-code-point cell ending in an astral character opened the expand
control even though the inline preview already showed the whole cell —
the same units-vs-code-points mismatch the preview cut fixed.
The gate now counts Array.from(value), and the boundary test asserts
the fits cell renders as a plain cell with no expand control while the
overflows cell keeps its button and whole-emoji cut.
Suggested-by: gdut4140 (port of the #7509 expandable gate)
* fix(sandbox): derive the AIO sandbox ID off the event loop in async acquire
_acquire_internal_async offloads every acquire layer except the sandbox ID
derivation, which ran inline. _sandbox_id_for_thread checks the thread's
skills_view directory with exists(), and resolving it goes through
Paths.base_dir, so each async thread acquire made filesystem syscalls on the
event loop. The strict blocking-I/O gate raises on it.
Run the derivation through the same owned-worker helper as the reuse and
warm-pool layers around it, and anchor it in the blocking-I/O suite for both
the default and the policy-scoped ID.
* docs(changelog): reference #7507 for the AIO async acquire sandbox ID fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(runtime): backfill run.terminal on confirmed recovery takeover
* fix(runtime): isolate recovery terminal publication failures
* fix(runtime): fence recovery terminal publication lifecycle
* fix(runtime): unify recovery receipt publication
* docs(runtime): document lifecycle guard lock constraints
* fix(runtime): reuse event transaction for recovery guards
Read Run lifecycle through the active event session when SQL stores share an engine, preserving deletion fencing without a second pool checkout. Keep custom lookups and different engines on their ordinary read path. Scope memory recovery guards and deletions to per-thread locks. Add SQLite/PostgreSQL pool pressure, stale identity, fallback, and independent-thread regressions, and document the contracts.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(projects): make read_file_chunks cancellation-safe for worker handoffs
Cancelling a project document read had three unsafe windows around the
dedicated file-I/O worker: (1) a cancellation landing between the open
worker completing and the result reaching the coroutine leaked the
handle (the cleanup path never saw it); (2) the cleanup close could
start while an admitted read was still running; (3) a repeated
cancellation let the caller escape before the close worker settled
(#7491).
Gate every admitted operation with the existing await_drained
primitive: cancellation is delivered only after the operation settles,
the open result is retained in a list cell the moment the worker
returns it (so the cleanup close always has the handle), and waiting
for the next chunk via the yield stays immediately cancellable.
Regression tests gate the real executor with asyncio events at the
worker boundary (real files, no mocks): all three windows are red on
unmodified main and green with the fix; the happy path is unchanged.
The dispatch-spy promotion tests that import app.gateway.routers need
the full backend dependency set and could not run in the minimal local
env; they fail identically on unmodified main (verified).
Fixes#7491
Co-authored-by: Jay Feng <jayfeng77@outlook.com>
* test: fix _gated_offload return annotation to the unpacked tuple
Review nit: the helper returns (calls, gated) and every call site
unpacks it as a pair, but the annotation claimed list[str]. Type it as
the tuple it actually returns.
Co-authored-by: Jay Feng <jayfeng77@outlook.com>
* test: pin missing-path FileNotFoundError in read_file_chunks
Convergence item from the duplicate-pair verdict (#7493): the
open-failure path must surface FileNotFoundError as-is — the cleanup
finally skips the close because retained is empty (no handle was ever
opened), rather than raising AttributeError from closing None.
Co-authored-by: Jay Feng <jayfeng77@outlook.com>
---------
Co-authored-by: Jay Feng <jayfeng77@outlook.com>
Two Gateways booting the same missing database both reach CREATE DATABASE and
only one wins; the loser aborted startup with duplicate_database (42P04) even
though the winner's catalog row was already committed and the target was
usable. Follow the SQLSTATE, like the missing-database check beside it does, so
the loser falls through to its retry. Every other failure -- permission denied
included -- still propagates untouched.
* fix(scheduler): keep manual-trial launch origins stable across retries
A manual trial's synthetic queued row omitted scheduled_for, and _launch_provenance used the current now for manual triggers. A retry at a later time therefore produced a different origin, which is part of start_run's per-occurrence idempotent input: the retry either conflicted with the first attempt or submitted a different request. Include the admitted scheduled_for in the synthetic row and use the occurrence time for every trigger, falling back to now only when a legacy caller supplies none.
Tests: a manual queued row retried at two later clocks produces the same origin and keeps the admitted scheduled_for; existing manual/scheduled provenance and title tests stay green. Full scheduler suite: 1108 passed / 130 skipped; guidance 0 errors / 0 warnings; ruff clean.
* test(scheduler): drive manual retry through requeue and drain
The unit provenance test pins the stable-origin function directly. Add a driven case that admits a manual occurrence, hits a busy-thread ConflictError so the row requeues, then drains it at a later clock and asserts both launches carry the admitted scheduled_for and the same full origin.
* test(scheduler): pin the immediate queue row in the retry test
Capture the queued mappings passed to _attempt_queued_run so the driven test asserts both halves explicitly: the synthetic immediate-launch row and the durable row drained later both retain the admitted scheduled_for.
---------
Co-authored-by: Tong-bit-art <Tong-bit-art@users.noreply.github.com>
* fix(gateway,integrations): keep temp cleanup from replacing the caller's outcome
* fix(gateway): adopt the family helper so a published edit stays silent
* fix(frontend): allow non-ASCII local parts in auth password forms (#7488)
The sign-in, register, admin-setup and change-password forms relied on the
browser's type="email" constraint validation, which rejects any non-ASCII
local part before the submit handler runs. The backend identity schema
deliberately accepts such addresses (see emailSchema in @/core/auth/types),
so an account created via the API, an administrator or OIDC could never sign
in through the password form.
Forms are now noValidate, and the permissive isValidEmail helper (one @,
non-empty local part and domain, no whitespace) runs in the handlers instead,
staying strictly weaker than what the backend accepts.
* fix(auth): normalize emails and restore form password checks
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(uploads): include markdown modification time in ownership records
* test(uploads): reject same-size edited companions on Windows
* fix(uploads): distinguish legacy companion records
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(checkpoints): isolate cached history payloads from state readers
Copy nested snapshot seeds and pending-write payloads at both cache boundaries.
Keep user copy callbacks outside the registry lock so readers cannot poison
later checkpoint materialization or deadlock by re-entering the cache.
Cover sync/async state accessor reads, nested message payloads, disabled-cache
controls and callback lock ownership.
* docs(checkpoints): document synchronous history copy costs
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): make docs search work on desktop and mobile
Docs search on deerflow.tech was broken twice over: no build step ever
generated the Pagefind index that Nextra's <Search> loads, and the custom
docs header that replaces Nextra's navbar never rendered a search box.
- `pnpm build` now runs `search:index` (pagefind 1.5.2, pinned) to write
public/_pagefind/ (gitignored).
- The landing Header and its mobile sheet take an optional `search` slot;
only the docs layout fills it, with localized labels and an aria-label.
- Each docs page carries a Pagefind `lang` filter and queries are scoped to
it, since the root <html lang> is fixed and would mix en/zh results.
- globals.css restyles the results panel with app tokens because Nextra's
<Head> variables are unset, lifts it above the mobile sheet, and keeps it
tappable; the sheet no longer closes before a result navigates and closes
after navigation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(frontend): add DESIGN.md describing the shipped design system
Records the existing tokens, typography, components, docs (Nextra)
overrides, motion, accessibility and copy rules, plus the landing-only
decorative exceptions, so UI changes have something to be checked
against. Linked from frontend/AGENTS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(frontend): address Codex review of docs search
- Chinese search: the root <html lang> is fixed, so Pagefind indexed zh
pages with the English tokenizer and multi-character words (e.g. 偏好)
never matched. scripts/build-search-index.mjs now sets each page's lang
from its route before indexing, producing separate en-US and zh-CN
indexes, and DocsHtmlLang syncs <html lang> at runtime (resetting
Pagefind on a language switch) so queries hit the right index.
- Mobile menu: reset the open state when the route changes instead of
comparing against the saved path, so Back no longer reopens the sheet.
- Shortcut: stop mounting a second <Search> in Nextra's unused mobile menu,
which captured Cmd/Ctrl+K and focused an offscreen input at 640-767px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(frontend): keep guidance within size budget
* fix(frontend): address docs search review feedback
Keep Nextra results within the mobile sheet scroll boundary and dismiss same-hash selections by pointer or keyboard. Rebuild Pagefind output without stale fragments and replace UI source-string checks with behavioral coverage.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): reject write_file payloads that are the elision placeholder
The tool-output budget middleware replaces a superseded write_file payload
with a deterministic placeholder in the model-bound request only. That
placeholder is text the model sees in its own history, so it can imitate it
and submit it back as real write_file content: #7319 reports a delivered
file that was 194 bytes of placeholder.
write_file now rejects a payload that is the placeholder (whole payload,
allowing only surrounding whitespace) with an actionable error telling the
model to read_file the target path and write the real content; nothing is
written. The match is whole-payload on purpose: a substring guard would
block legitimate edits to this repository, whose constants.py and
persistence docs quote the template verbatim.
The placeholder wording moves to one shared constant,
WRITE_ELISION_PLACEHOLDER_TEMPLATE in deerflow.constants, which the
middleware renders from and the guard matches on, so the two cannot drift.
Regression tests fail on base (7 failed, 6 passed) and pass with the fix;
near-miss payloads that merely quote the placeholder still write.
Refs #7319
* fix(sandbox): match complete write elision placeholders
* docs: clarify write placeholder validation scope
* chore: re-trigger CLA status check
* fix(sandbox): preflight async writes and trim guidance
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(models): preserve Moonshot cache_write_tokens in usage metadata
langchain-openai 1.2.1's _create_usage_metadata reads
prompt_tokens_details.cached_tokens but not cache_write_tokens, so the
PatchedChatDeepSeek conversion dropped the cache-creation count that Kimi K3.1
reports, and DeerFlow's usage journal never saw it (#7426).
PatchedChatDeepSeek now overrides _create_chat_result to restore the raw SDK
cache_write_tokens as input_token_details["cache_creation"].
Offline regression: a real OpenAI ChatCompletion carrying
prompt_tokens_details {cached_tokens: 100, cache_write_tokens: 256} converts to
input_token_details {cache_read: 100, cache_creation: 256} (red before, green
after); the existing 14 patched_deepseek tests and the managed-provider suites
stay green.
* fix(models): restore cache_write_tokens on the streaming path too
Streamed runs (web UI messages-tuple) build usage metadata in
_convert_chunk_to_generation_chunk -> _create_usage_metadata, which ignores
cache_write_tokens, so the non-streaming-only override still lost the count on
the path streamed runs actually take (review follow-up on #7428).
_convert_chunk_to_generation_chunk now mirrors _create_chat_result's restore
for the final usage chunk. Note chunk["usage"] is JSON null on non-final
chunks, so the lookup uses `or {}` instead of a .get default (a None usage
would otherwise raise AttributeError inside every chunk and break
test_model's connection probe).
* fix(harness): keep timestamped output-name fallbacks distinct
`output_name_candidates()` sampled `datetime.now(UTC)` inside the yield
loop, so its five timestamped fallbacks were only distinct when the host
clock advanced between two generator steps. On a coarse clock (Windows
advances in sub-millisecond steps) all five produced the same name, and
the capture tools lost retries before failing with
`could not reserve an output filename`.
Sample the clock once when the first fallback is reached and offset each
attempt by one microsecond, so distinctness no longer depends on the host
clock. Add a regression test that freezes the clock, and update the
exhaustion test to hold all five distinct stamps.
* docs: describe the coarse clock accurately
---------
Co-authored-by: PerryLink <255665900+PerryLink@users.noreply.github.com>
* fix(memory): drop unusable numbers in stale extensions and consolidation confidence
* fix(memory): log defaulted consolidation confidence and pin the int passthrough
* Track DingTalk stream message tasks to avoid dangling tasks
The stream loop handed each frame to client.background_task() via a
bare asyncio.create_task(...) whose result was dropped. asyncio only
keeps a weak reference to a task once it awaits, so the task could be
garbage-collected mid-execution (silently dropping a message) and any
exception it raised was never observed.
Keep a strong reference to each handler task and discard it via a done
callback, matching the background-task tracking used elsewhere (e.g.
the subagent executor and the MCP session pool).
* fix(channels): observe DingTalk stream task failures
---------
Co-authored-by: lisry-1 <lisry-1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
briefReason() measured and sliced UTF-16 code units, so a cut inside an
emoji's surrogate pair left a lone high surrogate that the outcome notice
renders as U+FFFD. Measuring code points also stops shortening a summary
that fits the documented budget.
Add a regression test for both the fits-exactly and truncating cases.
Co-authored-by: 一片一片 <192845873+gdut4140@users.noreply.github.com>
* fix(skills): support basename data-analysis exports
Use the current directory when export output has no parent directory.
* test(skills): cover data-analysis export paths
Exercise real DuckDB queries, filesystem exports and cached CLI execution across CSV, JSON and Markdown.
* docs(skills): record export path regression guidance
Document basename and nested output expectations and the regression test command.
* docs(skills): explain data-analysis export paths
Clarify bare filenames, relative parent directories and absolute output paths.
* docs: note supported data-analysis export paths
Add a concise README explanation while retaining existing upstream documentation.
* docs(skills): clarify export overwrite behavior
Document that existing export targets are overwritten without warning, as suggested in PR #7132. Verified real CSV, JSON and Markdown overwrites; 27 export regression tests pass.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): honor skills.container_path in user-scoped skill storage
The user-scoped factory never passed container_path, so
UserScopedSkillStorage kept the default /mnt/skills while public
storage honored the configured root, and consumers reading the root
from the storage (container registry, skill activation) built paths
that do not exist once an operator moves the mount root.
Seed container_path from the resolved app config, matching the
public-storage factory; an explicit caller override still wins.
Fixes#6767
* fix(skills): seed container_path in the legacy-visibility kwargs branch
willem-bd review nit on #6770: user_should_see_legacy_skills constructs
a one-off UserScopedSkillStorage when callers pass kwargs, and that
branch skipped the app_config container_path seeding, so a future caller
combining kwargs with a custom skills root would read the default
/mnt/skills instead. Seed it the same way the factory does; an explicit
caller override still wins.
* fix(scripts): keep sync_labels.py from crashing on non-UTF-8 stdio
The ✓ / ✗ status glyphs and the em dash in the Done line crashed the sync
with UnicodeEncodeError whenever stdout was redirected on a host whose
stdio encoding cannot represent them (Windows code pages, POSIX C locale,
PYTHONIOENCODING=ascii) — after `gh label create --force` had already run,
leaving a half-applied sync and a traceback. gh's UTF-8 diagnostics were
additionally decoded with the host locale, mangling failure messages.
Closes#7389.
- Reconfigure stdout/stderr in main(): UTF-8 with backslashreplace when
redirected, backslashreplace only on consoles.
- Decode the gh subprocess as UTF-8 with errors="replace", matching
scripts/support_bundle.py's _run_command.
- Add backend/tests/test_sync_labels_script.py: hermetic offline cases
(stub yaml, stub gh) running the real script under PYTHONIOENCODING=ascii
for the success path, a UTF-8 gh failure, and --dry-run. All three go
red on the unfixed script.
* style(tests): fix sync-labels regression lint
* docs(scripts): document label sync encoding policy
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* test(memory): pin the memory guides to the layout DeerMem resolves
The en and zh harness guides still document the pre-abstraction storage
shape, and nothing in CI reads prose, so the drift stayed invisible.
Derive the per-user manifest and per-agent facts paths from
memory_file_path()/agent_facts_directory() and take the allowed memory:
top-level keys from the public MemoryConfig model.
* docs(harness): scope memory docs per user and nest DeerMem knobs
Both guides claimed a shared {base_dir}/memory.json plus one memory.json
per agent; DeerMem keeps one manifest per user and puts agent state in a
facts directory beside it. The zh Configuration example also still showed
DeerMem-private keys at the memory top level, which config.example.yaml
and the en twin already place under backend_config.
* docs(memory): align per-agent facts overviews in both languages
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): keep document bytes when write_file translates paths
LocalSandbox.write_file rewrote container paths inside every file it stored,
so a dictated README.md ended up holding a host path while read_file masked
the rewrite for agent-written files: the read-back showed the virtual path,
and the agent confirmed content the file did not contain (#7461).
Translation now applies only to executable content, classified with the
shared is_code_file rule from deerflow.skills.package_files that the skill
installer, the export guard, and skill_manage_tool already use. Code suffixes
and extensionless files that open with a shebang keep the host-path rewrite
they need to run under a host interpreter (PR #1935); documents, data files,
and configs are stored verbatim and read back as stored.
* fix(sandbox): translate paths for interpreter scripts and stop reading pipes
The classifier added for #7461 asked only the skill-package rule, which
does not list `.awk` (nor `.sed`, `.lua`, `.r`, `.tcl`), so a written AWK
program kept container paths that the host awk cannot open when it runs
`awk -f`. Those suffixes are now the sandbox's own set: the shared rule
also decides what SkillScan and the installer treat as package code, and
widening it there is a different call.
Classification no longer touches the destination unless the path is
extensionless. A suffix answers the question by itself, so an append to
`events.py` no longer opens the file first, and the extensionless
shebang sniff goes through `os.open(O_NONBLOCK)` plus an `fstat` regular
file check, which is what keeps a FIFO append from blocking.
* test(sandbox): pin the non-blocking sniff with a suffixless pipe append
Both chat pages capped the "conversation finished" notification body with
String#length / substring, which count UTF-16 code units: a cut inside an
astral character's surrogate pair rendered U+FFFD, and a body that fits 200
code points was shortened early.
Extract notificationBody() so both pages share the code-point bound and the
"Conversation finished" fallback, and add boundary regressions.
Co-authored-by: 一片一片 <192845873+gdut4140@users.noreply.github.com>
Use the shared ScrollArea so long artifact lists remain reachable in desktop panels and mobile drawers. Add browser coverage for short and long lists before clicking the final file.
Adapt the ScrollArea approach from LeonSGP43 in bytedance/deer-flow#2076 and verify it against current main for issue #2074.
* fix(sandbox): classify speculative prewarm acquires by event state, not coarse clock ties
* test(sandbox): pin the prewarm tie classification with a quantized monotonic clock
* fix(threads): reject an assistant_id that names no agent at thread creation
POST /api/threads stored assistant_id unchecked. Every state read then built
a run config from it, and build_run_config's ValueError answered 500 on GET
thread, state and history until the thread was deleted.
Validate it at creation with the rule run creation already applies, now
shared as custom_agent_name(), and answer 422. Records written before this
change are read with the lead agent's schema instead of failing.
* docs(changelog): reference #7445 for the thread-creation assistant_id fix
* fix(threads): clear invalid legacy assistant ids in branches
* docs(changelog): note the branch assistant_id behavior for #7445
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(memory): rank capacity eviction per record, not per fact_id
select_facts_for_capacity keyed every score by fact_id, so an imported
batch that repeats an id collapsed into a single dict entry: the last
record's score won, and both the ranking pass and the evicted audit
read that one value for every position. A 0.9-confidence record could
therefore be evicted while its own 0.1-confidence duplicate survived.
The import path keeps caller-supplied ids without checking them for
uniqueness, and any over-cap import lands here.
Score by position instead: a parallel list next to the indexed facts,
so ranking and the audit read each record's own score. The public
``scores`` dict stays keyed by fact_id with last-write-wins
semantics, keeping the DeerMem benchmark contract and the callers
that never read it unchanged.
Red on main: test_confidence_policy_scores_duplicate_fact_ids_per_record
fails with the highest-confidence record evicted; green on this branch.
* fix(utils): keep share-less UNC roots out of the extended-length prefix
extended_length_path prefixed every absolute UNC path with
\\?\UNC\, but Win32 only accepts \\?\UNC\server\share\... — a
share-less root (\\server, \\server\, or a bare \) is not a valid
device path, so every Win32 call handed the prefixed spelling fails.
Only prefix when both the server and the share are present;
otherwise return the ordinary spelling unchanged.
Red on main: the \\server, \\server\, //, and //server cases of
test_windows_extended_spelling assert the forged prefix; green on
this branch.
* fix(sandbox): normalize mount container_path trailing slashes at config load
A container_path declared with a trailing slash (e.g. /mnt/reports/) was
accepted by VolumeMountConfig and normalized by LocalSandboxProvider when
building path mappings, but the sandbox tool path gates compare the raw
configured value, so every ls/read/write/glob/grep/bash access to the
mount failed with a confusing PermissionError. Normalize trailing
slashes in the config model so both layers agree, and reject the
container root '/' which the gates cannot express.
* fix(sandbox): normalize skills.container_path and pin empty-value rejection
Review follow-up on #7393:
- Share the container-path normalizer with SkillsConfig, whose prefix
feeds the same as-is comparison in _is_skills_path and had the same
trailing-slash dead-path failure mode.
- Pin the empty-string rejection: on main an empty container_path made
the gates match every absolute path, applying one mount's read_only
flag to all paths.
- Cover both config fields in CONFIGURATION.md.
---------
Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
* fix(channels): retry the runtime config file lock instead of failing after ~10s on Windows
The runtime channel config sidecar lock (#6844) used Windows' blocking msvcrt LK_LOCK mode, which gives up after roughly ten seconds and surfaces cross-worker contention as Permission denied instead of serializing the read-modify-write cycles it exists to serialize, while fcntl.flock(LOCK_EX) waits on POSIX. Retry the non-blocking acquisition (LK_NBLCK + 0.2s) on the Windows fallback, mirroring the extensions manager's lock (#6020), and bind fcntl/msvcrt at module level so the fallback branch is testable.
Tests: new backend/tests/test_channel_runtime_config_store_lock.py (5 tests: retry cadence via fake msvcrt, sidecar byte creation, POSIX flock preservation, direct Windows regression through the sys.modules seam, real cross-thread contention). 3 of them fail on the unmodified base (one with OSError 36 raised by the blocking call itself) and all pass with the fix. Related suites: store peers + connections router 86 passed / 1 skipped; blocking_io + encoding + channels 498 passed / 2 skipped. Ruff format and check clean on both files. Windows 11, Python 3.12 (uv-managed venv).
* fix(channels): share contention retries and diagnose prolonged lock waits
* docs(channels): shorten guidance to satisfy lint budget
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
`Paths.delete_thread_dir()` called `shutil.rmtree()` directly. Windows
refuses to unlink a file that carries the read-only attribute
(`WinError 5`) where POSIX only needs a writable parent, so one read-only
file under `user-data/` made the whole thread undeletable: `DELETE
/api/threads/{id}` answered 500 `Failed to delete local thread data`,
`rmtree` had already removed everything it walked before that entry, and
every later retry failed on the same file.
Retry at the call site: clear the read-only attribute tree-wide and call
`rmtree()` once more. Only `PermissionError` is retried, the retry still
raises when it cannot help, so a file another process holds open stays a
real error instead of becoming a silent skip. A `onexc` handler cannot do
this portably: POSIX `_rmtree_safe_fd()` catches the handler's own
`PermissionError`, re-dispatches it as a `scandir` error and leaves the
deletion to fail later as `ENOTEMPTY`.
Co-authored-by: Aling33 <along308@users.noreply.github.com>