GitHub owner and repository names are case-insensitive, but the agent
registry indexed and looked up bindings by the raw repo string. A
binding for acme/widget never matched webhooks for Acme/Widget, so the
event was dropped without an error. Lowercase the repo on both sides.
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(memory): skip text-less turns when formatting the memory prompt
A content-less message rendered as the literal "None", and a block list
without text blocks rendered as its Python repr, including any base64
data URL. Normalize through the same rules as the sibling content-to-text
helpers and skip turns that carry no text.
* test(memory): cover whitespace-only assistant turns
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(scheduler): pause a task whose schedule no longer computes
Admission validates every schedule, but a stored one can stop computing
later: a zone the installed tz database no longer has, or a cron
expression a newer croniter rejects. The scheduler recomputes next_run_at
inside the shared poll, and that raise escaped:
- dispatch_task recomputed it in its except block after a successful
launch, so run_once aborted the claimed batch and left the occurrence
in "launching" with no last_error;
- once the launch lease expired, recover_expired_launch_claims found the
durable run and raised again for every poll, before claim_due_tasks,
so no other scheduled task was admitted for the rest of the process.
Runtime cursor writes (launch, launch repair, queue timeout, launch
failure, finalization) now use stored_next_run_at, which returns the
reason instead of raising. A live recurring task is paused with
"schedule can no longer be computed: ..." as its last_error and no next
run; a once task keeps its normal outcome. Resume re-validates the
schedule, so it reports the problem until the schedule is fixed.
run_once also isolates the lease-recovery pass, like the terminal-run
reconciliation next to it, so a failed pass no longer skips admission.
* docs(changelog): reference #7993 for the unschedulable task fix
* fix(gateway): keep batch cancellation running when the scheduler store fails
_run_completion_hook awaited scheduled_task_service.handle_run_completion
before batch_continuations.source_finished, so a scheduler store outage
aborted the hook and skipped batch cancellation entirely: a claimed
follow-up of a hard-stopped run stayed in the dispatching state instead
of being cancelled.
The scheduler notification is now best-effort (logged as non-fatal);
batch cancellation always runs.
Fixes#7969.
* fix(gateway): isolate batch cancellation failures and trace completion warnings
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
A stale memory.backend_config (DeerMem-only keys left in place after
switching manager_class to openviking) surfaced only when
MemoryMiddleware constructed the manager mid-conversation, after model
tokens were already spent; make doctor reported Ready because
MemoryConfig accepts backend_config as untyped data.
doctor now runs the backend's own side-effect-free validator
(OpenVikingConfig.from_backend_config) and reports the offending keys
before an agent turn is accepted. Backends without a static validator
are skipped, as is disabled memory.
Fixes#7962
Signed-off-by: Batianxieshen1 <280706568+Batianxieshen1@users.noreply.github.com>
Co-authored-by: Batianxieshen1 <280706568+Batianxieshen1@users.noreply.github.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(telegram): honor retry_after when retrying a new message
A 429 on sendMessage or sendRichMessage slept 2**attempt and dropped
the reply before Telegram's flood wait expired. Wait the longer of
that backoff and the server hint, and leave other channels unchanged.
Fixes#7989
* fix(telegram): bound new reply flood waits
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): exempt expression division after closers from the host-bash guard
A single-segment absolute-path match directly after ')', ']' or '}' is
division in an inline expression (python3 -c, perl, node -e, $((...)) ),
not a host path; #7023 exempted this shape only inside single-quoted awk
programs. Per-closer rules keep unknown-value path builds rejected:
digits only after ')' and '}' ($1/1024 rationale), an identifier after
')' only when the paren closes a name(...) call, any segment after ']'.
Literal tokens ('ls /2') are unchanged.
Fixes#7444
* fix(sandbox): scope division exemptions to parsed expressions
* docs(sandbox): fit inherited guidance within the hard limit
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): reject Windows device names when creating a skill
A skill name is a `custom/<name>` directory component, but the hyphen-case
grammar admits Windows reserved device names (`nul`, `con`, `com1`, ...).
`skill_manage` create and `.skill` archive install accepted them: on Windows
`mkdir("nul")` reports success without producing a directory and the first
write inside it fails with FileNotFoundError; on Linux the install succeeds
but the tree can never be copied, mounted or exported to a Windows host.
Creation now rejects those names through `windows_incompatible_segment()`, the
check uploads, sandbox writes, skill support files, thread IDs and agent names
already use. Archive extraction also rejects member segments that cannot be
created on every host. Reads and deletes stay permissive so a skill an older
release created on a Linux host can still be inspected and removed.
Fixes#7945
* test(skills): pin the extraction guard and the tool-level create wiring
Address review findings on the skill-name portability change:
- Isolate the extraction guard: an archive with a portable folder and a
portable frontmatter name but a reserved support-file segment
(`my-skill/references/nul.md`) now fails with the member-path error, so
deleting the guard turns this suite red again.
- Pin the `skill_manage` wiring: `create` of a device name raises through the
tool, while `edit`/`delete` keep reaching the existing "not found" path.
- Reword the `safe_extract_skill_archive` docstring: members before the
offending one are already written into staging, so what the check
guarantees is that the install never commits under `custom/`.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(community): strip provider API keys before they reach a header
GroundRoute (from GROUNDROUTE_API_KEY), InfoQuest, Tavily, Exa, Jina,
Firecrawl, fastCRW and Crawl4AI put the configured or environment key into
an HTTP header as read. A key read from a file keeps its trailing newline
(a Kubernetes Secret created with --from-file and mounted through envFrom),
and the transport refuses it with an error that quotes the whole header:
h11's "Illegal header value b'Bearer <key>\n'" or requests' "... in header
value: '<key>\n'". The providers returned that message as the tool result,
so the key reached the model and the checkpointed conversation, and
GroundRoute, InfoQuest, Jina and Crawl4AI also wrote it to the error log.
Resolve each key through provider_credentials.resolve_api_key, as the other
bundled providers already strip theirs. Tavily, Exa and Firecrawl now read
their environment fallback themselves instead of leaving it to the SDK,
which would read the raw value again. A blank configured key now falls back
to the environment, as it already does for GroundRoute and the stripping
providers.
* docs(changelog): reference #7981 for the provider API key fix
* fix(community): keep fastCRW from sending the Firecrawl key
fastCRW builds the Firecrawl SDK client, and the SDK reads
FIRECRAWL_API_KEY whenever api_key is None. Once a blank configured key
resolved to None, a fastCRW config with api_key: "" and no CRW_API_KEY sent
the Firecrawl key to the fastCRW endpoint. With no api_key configured at
all, main already did the same.
Pass "" when fastCRW resolves no key of its own; the SDK falls back only on
None and sends no Authorization header for an empty key. A loopback test
pins the empty, blank and absent cases with FIRECRAWL_API_KEY set.
* fix(community): keep a blank environment key away from the SDKs
Tavily, Exa and Firecrawl resolved their environment fallback themselves,
but passed None when it was blank, and each SDK then re-read the same
variable unstripped. Tavily and Firecrawl now pass "", which the SDKs
treat as a missing key; Exa sends "" as is, so it fails before any
request instead.
The fastCRW comment records that only the Firecrawl SDK's v2 client
honours "", and the fastCRW regression test now covers web_fetch as well
as web_search, so a move to the v1 client would fail it.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(docker): keep local config out of the image build context
backend/Dockerfile copies the backend tree wholesale (COPY backend ./backend),
and frontend/Dockerfile does the same for frontend/. .dockerignore excluded
.env files and runtime data, but not the local configuration: config.yaml,
the config.yaml.bak that make config-upgrade writes beside it,
extensions_config.json and legacy mcp_config.json. The config resolvers still
read these from the legacy backend/ location, so on a checkout that keeps them
there every locally built gateway image (dev and prod) carried the model API
keys and MCP credentials in its layers, under the file Compose mounts over at
runtime. frontend/.env.local, the documented place for BETTER_AUTH_SECRET in
local development, was copied into the frontend image the same way, where
next build and next start also read it.
Exclude those files at the root and in backend/, and .env*.local at any depth.
None has tracked content; the build's file list is otherwise unchanged.
* docs(changelog): reference #7958 for the Docker build-context fix
* fix(docker): keep local HTTPS keys and snapshot credentials out of images
frontend/.gitignore also ignores *.pem, the mkcert key pair for local HTTPS,
and COPY frontend ./frontend shipped it into the dev and prod frontend images.
Exclude frontend/*.pem.
The trailing !backend/extensions/sources/** re-inclusion wins over every
earlier rule, so a hand-edited extension snapshot could still carry .env files
or keys into the gateway image. Exclude, after the re-inclusion, exactly the
files the extension manager already refuses to snapshot; a test pins that set
to the manager's own _SENSITIVE_FILENAMES and _SENSITIVE_SUFFIXES.
The test's matcher now translates patterns as Docker does (** across segments,
* within one, character classes, parent-directory matches), so it can model
the mid-path ** rules; its predictions agree with BuildKit on every test path.
The README and changelog now say that a locally built frontend image no longer
inlines NEXT_PUBLIC_* values from frontend/.env.local.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(agent): stop todo reminders after run hard limits
Respect loop_capped and token_capped before scheduling completion reminders, and include the behavior in the assembly policy fingerprint.
Cover sync and async agent graphs, blocked post-stop tools, preserved Todo state, and continuation from the same thread checkpoint. Validation: 316 targeted tests passed.
* docs(agent): explain todo behavior after hard stops
Document that loop and token hard stops suppress Todo completion reminders while retaining unfinished tasks for later runs.
* docs(changelog): record todo hard-stop fix
Add English and Chinese Unreleased entries referencing issue #7953.
* fix(agent): mark safety stops with partial responses
* fix(agent): honor provider stops before todo retries
* docs(agent): clarify todo behavior after provider stops
* test(agent): isolate todo safety-chain runtime dependencies
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
* fix(subagents): build the chat model off the event loop
`SubagentExecutor._create_agent` is awaited from `_aexecute` on the shared
event loop, and it constructed the chat model inline. Provider construction can
read local credentials -- the Codex provider loads the Codex CLI `auth.json`
while it is built -- so every delegated task did synchronous file IO on the loop
before its first model call, which stalls every concurrent turn sharing that
loop.
Build the model through `run_assembly`, the dedicated assembly pool the other
provider-construction sites already use, and leave the rest of the middleware
and graph assembly where it is. The helper preserves request context, and a
cancelled construction still never starts an inference.
`tests/blocking_io/test_subagent_model_offloop.py` drives the real
`_create_agent` with a real `CodexChatModel` under the strict Blockbuster gate:
on `main` the credential load fails with `BlockingError(os.stat)`, and reverting
the `run_assembly` call reproduces the same failure.
* docs(subagents): name the isolated subagent loop in the off-loop notes
The blocked loop is the persistent isolated subagent loop every concurrent
delegated task shares, not the Gateway request loop; say so in the boundary
audit, the regression docstring, and the assertion message.
---------
Co-authored-by: Totoro <msy626836554@gmail.com>
* fix(channels): keep Feishu cards within the card size limit
Card text was unbounded and ASCII-escaped, so an answer of about 4,400 CJK
characters made the card request body exceed Feishu's 30 KB limit (230025).
The final patch and its reply fallback were both rejected, the answer was
lost and the running card stayed on its last streamed text.
Cards are now serialized without ASCII escapes and budgeted by the bytes
their text adds to the request body. A streamed update shows what fits; a
longer final answer continues in up to 10 cards replied to the same source
message, split at line breaks, and a retry resumes after the cards already
delivered.
* docs(changelog): reference #7798 for the Feishu card size fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(skills): expose MiniMax resolution/duration in video-generation
Hailuo 2.3 accepts explicit resolution (768P/1080P) and duration (6/10s)
in the /v1/video_generation body, but the skill sent neither — and its
SKILL.md told the caller --aspect-ratio is ignored for MiniMax without
offering any alternative, leaving cost/quality/length uncontrollable.
Read MINIMAX_VIDEO_RESOLUTION and MINIMAX_VIDEO_DURATION from the
environment and pass them through when set (unset keeps the API defaults
byte-for-byte); values are passed through unvalidated beyond the integer
check so newer tiers (H3 per-second 4-15s) need no script change. Document
both knobs in SKILL.md with a draft/final example. Fixes#7911.
* fix(skills): address MiniMax video override review
---------
Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(sandbox): build the readiness client off the event loop
`wait_for_sandbox_ready_async` exists so sandbox startup waits do not block the
runtime event loop, yet it constructed `httpx.AsyncClient` inline: the
constructor resolves the system trust store synchronously, so the loop stalled
for the whole construction before the first poll was sent (~320 ms in a local
probe on this host).
Move the construction into a worker thread and own it as a task, the way
backend/AGENTS.md now requires for HTTPX clients: a cancellation that lands
while the worker is still inside the constructor waits for it and closes the
client it builds, so the pool is not abandoned. Requests, polling cadence,
timeouts and cleanup stay on the event loop.
The blocking-I/O regression pins both halves. Against the previous code the
trust-store assertion fires on the loop thread; with a plain `asyncio.to_thread`
instead of the owned task, the cancellation test reports the leaked client.
* fix(sandbox): preserve readiness cancellation during cleanup
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
compact_thread_context built the summarization middleware inline on the
Gateway event loop, and with it the run-profile model and any separately
configured summary model. When the provider reads local credential files
(the Codex provider reads the Codex CLI auth.json while it is constructed),
each POST /api/threads/{thread_id}/compact did synchronous file IO on the
shared loop, once for every model it built, so other requests and streams
waited for it. The route passes no app_config, so the call also read the
live config on the loop first, which stats and hashes config.yaml on every
call. #7767 listed this site in the utils guidance as not covered.
Build the middleware through run_assembly, as #7484 and #7767 do for the
one-shot helpers, skill moderation and the goal evaluator, and read the live
config in that pool too when no app_config is passed. The lead agent already
builds the same middleware there through run_agent's agent factory call.
Model resolution, the memory-flush policy, the disabled, failed and
pending-work outcomes and the written checkpoint are unchanged, and
cancellation during construction neither starts the summary call nor writes
a checkpoint.
tests/blocking_io/test_compaction_model_offloop.py builds real
CodexChatModels under the strict Blockbuster gate. On main, compaction
reported itself disabled because every model build hit BlockingError
(os.stat), and the /compact handler answered 500 because the gate rejected
its config read first.
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(memory): restore facts from older backups
Use the current snapshot revision for existing facts during backup import so saved contents can be restored after later edits. Preserve manifest and fact preconditions, scope isolation, and caller-owned input data.
* refactor(memory): share import revision preconditions
* docs(memory): fit import guidance within chain budget
* fix(memory): validate backup revisions before restoration
---------
Co-authored-by: liyinlong <173714821+lilong555@users.noreply.github.com>
* fix(channels): key WeCom group conversations by chatid, not the sender
Group messages used the sender's userid as chat_id and topic_id, the same
key as the sender's private chat. Wherever no group connection kept them
apart (auth-disabled mode, require_bound_identity: false, legacy group
bindings), a member's private chat and every group they wrote in ran in one
DeerFlow thread. Groups are now keyed by chatid with one thread per member.
A group-scoped connection carries its sender-keyed thread over once; a
legacy binding, which spans every group of the bot, does not.
* docs(changelog): reference #7794 for the WeCom group conversation fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(parsing): keep literal think tags inside JSON string values
Structured parsers ran strip_think_blocks over the whole response before
json.loads. A model answer that quotes the tag -- "Explain <think>example
</think>" -- lost the quoted text, and an unclosed literal tag inside a
string cut the JSON document mid-string, so suggestions returned None and
goal evaluation failed to parse.
Strip reasoning only outside JSON string literals: leading blocks are still
removed whole (a JSON example inside reasoning is never taken as the
answer), and blocks after the payload starts are removed only outside
quotes. Suggestions and the goal evaluator both use the shared helper.
* fix(parsing): keep think-tag stripping complete outside the JSON payload
The string-aware walker only covered the payload itself: a reasoning block
after prose and before the payload was copied verbatim, so brackets inside
it became the parsers' find/rfind targets, and an unclosed block in
trailing prose was kept where strip_think_blocks used to truncate.
Find the payload start while stepping over think blocks, strip the head with
strip_think_blocks, keep strip/truncate semantics after the top-level
payload closes, and truncate at an unclosed block outside a string. Adds a
CHANGELOG entry for the user-visible parsing change.
* fix(gateway): strip server-owned metadata from run input todos and goal (#7803 follow-up)
Commit 459fc8c introduced shape validation for caller-written todos in
both strip_server_owned_state_metadata and normalize_input, but only
applied _strip_external_metadata_from_message_like in
strip_server_owned_state_metadata.
External callers submitting POST /runs or POST /threads/{id}/runs with
input.todos or input.goal could supply forged server-owned metadata
(such as untrusted_input: False, original_user_content, or artifact
knowledge_sources) directly into the agent graph.
Apply _strip_external_metadata_from_message_like to both normalized
todos and goal in normalize_input when trusted_internal=False.
* fix(gateway): mirror normalized goal and todos into canonical run record input
* fix(gateway): replace non-dict additional_kwargs and cover edge cases
- Ensure non-dict additional_kwargs in message-like entries is replaced with an empty dict instead of surviving the strip
- Remove redundant None-check on normalized_goal in normalize_input
- Add regression tests covering non-dict additional_kwargs replacement and untrusted_input stamping on human-like entries
* test(gateway): format todo input validation cases
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Tool-call ids are unique per assistant response, not per conversation.
extract_skills built a single id -> path map over the whole message
list, so a later read that reused an id overwrote the earlier one: the
earlier skill silently dropped out of the ledger and its correct
ToolMessage logged "mismatched skill read metadata".
Rebuild the map from each AIMessage's own calls and resolve a
ToolMessage against the nearest preceding response, matching the
pairing rule tool_call_args.pair_tool_call_results already applies.
Fixes#7858
Signed-off-by: Batianxieshen1 <280706568+Batianxieshen1@users.noreply.github.com>
Co-authored-by: Batianxieshen1 <280706568+Batianxieshen1@users.noreply.github.com>
* fix(capabilities): tidy the extension detail layout and narrow-screen overflow
The extension detail view led with a page-level reload button and sat the
back button beside it 3px off: the two flowed inline and aligned by
baseline, which the back button takes from its icon. It also kept a
directory search that filters nothing there, a heading close to the page
title, and status copy that read like body text. On narrow screens the tab
strip widened the whole page, leaving a horizontal scrollbar.
Lead with the back button and move reload to the opposite end, drop the
search from the detail view, and give the detail an icon, package name and
a status notice holding the installation guide. On the directory, reload
now sits beside the hint that explains it. The tab strip scrolls inside
itself and keeps the active tab in view, and reload collapses to an icon
below `sm` with its accessible name intact.
No copy changes.
* fix(capabilities): realign the tab strip on resize without moving the page
Review follow-up. The active tab was only realigned when the tab changed, so
a rotation or window resize could leave it clipped. Realign whenever the strip
resizes, and leave manual swipes alone.
Scroll only the strip. scrollIntoView would also move the page, pulling it back
up when a resize arrives after the user has scrolled past the strip.
Also hide the strip's scrollbar in browsers without scrollbar-width support,
and find the page scroller in the E2E test by computed style rather than by a
class name.
receive_file downloaded each image/file descriptor and replaced the
[image]/[file] placeholders in msg.text, but left msg.files intact. The
generic URL-based _ingest_inbound_files pass then re-walked the residual
{"image_key":...}/{"file_key":...} descriptors, found no url, and logged a
spurious 'reader returned no data' warning per file.
Clear msg.files after materializing the paths, matching DingTalk (msg.files =
[]) and the Slack/Telegram/Discord providers (which materialize them).
Co-authored-by: lisry-1 <249596828+lisry-1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
join_host_path joined POSIX-styled bases with the running OS's native
path module, so a provisioner running on Windows emitted backslash
hostPath values into pod specs — paths a Linux kubelet cannot resolve.
The same leak existed in _host_base_dir_for_extra_mounts,
_is_path_under_base, extra-mount validation, and PVC subPath
computation, all of which used os.path on paths that are POSIX by
contract. Route every non-Windows-styled path through posixpath and
keep the PureWindowsPath branch only for genuinely Windows bases.
Also re-spell the memory guide probe layout with forward slashes so
backend/tests/test_memory_documentation.py compares against the
documented layout instead of the test host's local separator.
A background-task detail or list read issued before the user cancels can
resolve after the mutation's success handler and overwrite the confirmed
"cancelled" snapshot with the pre-cancel "working" one, so the expanded card
shows the task as active again (and polling resumes) until a later response
corrects it.
useCancelBackgroundTask now awaits cancelQueries for the thread's
["background-tasks", threadId] prefix before seeding the confirmed detail and
list entry, so a read that started before the cancel is dropped instead of
landing afterwards. The following list invalidation is unchanged, so a mounted
card still refreshes from the server. This is the same ordering contract as the
memory mutations fixed in #6959.
Regression: frontend/tests/unit/core/background-tasks/hooks.dom.test.tsx uses a
real QueryClient with deferred detail and list reads; it ends on "working"
without the fix and keeps "cancelled" with it.
Co-authored-by: REI-330 <173389910+REI-330@users.noreply.github.com>
Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
* fix(community): initialize InfoQuest TLS clients off the event loop
InfoQuest's fetch, web search and image search constructed
httpx.AsyncClient inline in their coroutines, so the default transport
loaded the TLS trust store synchronously on the agent event loop
(~125 ms per call in a local probe). RAGFlow, LightRAG, You.com,
GroundRoute and Webz already construct their clients with
asyncio.to_thread; InfoQuest was the missed sibling.
Move the three constructions to asyncio.to_thread and cover all three
methods in the blocking-I/O TLS test. Requests, cleanup, timeouts and
responses stay on the event loop.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* docs: qualify the off-loop HTTPX provider list
The list is not exhaustive; several community clients still construct
their client inline. Use non-exhaustive wording until the follow-up
sweep lands.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* docs: correct the off-loop provider list after merging main
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(mcp): degrade a blob without mimeType instead of failing the tool call
A resource link or embedded resource without mimeType made the blob guard
raise, so the whole tool call failed instead of degrading the attachment.
Keep the guard for genuinely unusable payloads and let a missing media type
fall through to the caller's default.
* Modify test for text blocks and mime type validation
Update test to check for text blocks and their mime types.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* test(mcp): remove MIME assertion from text content block
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(extensions): roll back shared state mutations on budget check failure
In deerflow_extension_agent_teams.shared_state, publish() and update()
mutated team['public_state'] in-memory before invoking check_budget().
When check_budget() raised a budget-exceeded ValueError (e.g. 16,000 JSON
bytes or 24,000 shared context bytes), the state mutation was not rolled
back, causing:
1. Permanent state lockout on rejected publish (the oversized entry
remained in state['entries']).
2. False optimistic concurrency conflicts on update retry (the entry
revision was prematurely bumped and content altered).
Wrap check_budget() in try-except in both functions:
- On publish failure: pop the appended entry and decrement revision.
- On update failure: restore original entry dict and decrement revision.
- Added regression test covering both rollback scenarios.
* test(extensions): request plugin fixture for isolated regression run
* fix(extensions): add rollback identity guard and pin retry assertions
* fix(browserless): construct the fetch and screenshot clients off the event loop
* fix(browserless): close the client a cancelled construction still builds
Keeping the constructor off the loop is right, but awaiting
`asyncio.to_thread(httpx.AsyncClient, ...)` directly is not cancellation-safe:
the await can be cancelled while the worker thread is still inside the
constructor. The await is abandoned, the thread runs on and hands back a live
client, and execution never enters the `async with` that would have closed it,
so `is_closed` stays False and the connection pool leaks on that cancellation.
Own the construction as a task instead. When the caller is cancelled, wait for
the task with the shield loop this package already uses for cleanup, close
whatever client it produced, and only then let the cancellation propagate. A
failure while closing is logged rather than raised, so it cannot mask the
cancellation the caller asked for.
The new regression test gates the constructor on a thread event, cancels the
operation while it is parked there, and releases the worker afterwards. Against
the previous commit it fails for both fetch_html_with_status and
capture_screenshot with "the client built after the cancellation was leaked".
---------
Co-authored-by: inchang-ing <197932532+inchang-ing@users.noreply.github.com>
* fix(sandbox): defer fresh AIO skill-policy acquisition
Prepare the authorized thread skill view before the model runs without
acquiring an AIO container for a fresh lazy run. Keep existing sandbox
bindings and upload-based providers on the eager rebind/sync path.
Add sync/async regressions for first-tool authorization, strict rebinding,
filtered read-only AIO mounts, and policy-scoped warm reuse.
AI assistance: Codex; the account owner confirmed human review of the
complete seven-file patch and responsibility for the change.
* docs(sandbox): keep lazy-isolation guidance within inherited budget
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(subagents): continue native batches and honor item dependencies
* docs: link native batch follow-up changelog entries
* test(migrations): pin isolated history-seed upgrade to 0050
* fix(subagents): preserve follow-up scope, blockers and fair scheduling
* test(memory): freeze capacity evaluation time
* fix(subagents): guard follow-up admission and retain run controls
* fix(subagents): enforce runs:cancel on tool cancellation of follow-up batches
* style(tests): format test_batch_followup_host with ruff
* fix(subagents): drop the admission checkpoint check and keep SDK cancel compatible
The checkpoint check inside run admission waited on the checkpoint saver's
lock while holding the SQLite write lock, which could make another chat's
checkpoint write fail. Remove it; dispatch still checks the conversation
before admission.
A follow-up waiting for an unanswered question now keeps its claim until the
lease expires instead of rereading the conversation every poll. Cancel passes
the caller's run authority only to submitters that take it, so existing SDK
submitters keep the two-argument call. Document that retrying a prerequisite
by hand does not requeue its blocked items, and list 0052 as the migration head.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* Keep the Windows SID lookup alive on non-UTF-8 console output
`_resolve_current_user_sid` calls `whoami /user /fo csv /nh` in text mode
without an encoding. `make dev` and `make gateway` start the Gateway with
PYTHONUTF8=1, so on a Chinese Windows host the interpreter decodes as UTF-8
while whoami writes the account name in the console's ANSI code page. The
strict decode raises inside subprocess's reader thread; the exception is not
re-raised, so stdout comes back as None and the credential-tree hardening
walk dies with `AttributeError: 'NoneType' object has no attribute 'strip'`
before any directory is created or hardened.
Pin the encoding and keep an errors policy. The SID is the second CSV field
and is pure ASCII, as are the delimiters, so replacing the undecodable
account name is harmless. #6590 pinned the seven lark-cli/npm captures in
this file; this is the eighth text-mode capture and, unlike those, does not
emit UTF-8, so the errors policy is the part that matters here.
Add a cross-platform regression test next to the #6590 encoding tests.
* docs(lark): correct the SID capture wording
`undecodable` is the spelling used elsewhere in the repository, and the
comment about the console code page reads better as "which does not need to
match". No behaviour change.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): fail closed when the extensions config cannot be read
* test(skills): cover a disabled skill when the extensions config cannot be read
* style(tests): format the fail-closed regression for ruff
* style(tests): fit the docstring and signature inside the 240-column limit
* fix(skills): fail closed in the user-scoped listing when the config cannot be read
* test(skills): cover the user-scoped listing with an unreadable extensions config
* fix(gateway): keep skill read surfaces loud about an unreadable extensions config
Folding the user-scoped degraded mode in (#7704) also stopped the skill
read endpoints from failing on a config that cannot be read: the storage
now answers with an all-disabled listing, which regressed the #7155
contract (a generic 500 with the host path only in the server log) into
a 200 that presents a broken deployment as a legitimately empty one.
Keep both properties: the storage still fails closed for the runtime
gate, and the read surfaces re-validate the config explicitly so the
raised error reaches each handler's generic 500 branch unchanged.
First-run behavior is untouched: no config file resolves to an empty
config without raising, so default-enabled still applies.
* fix(skills): redact config-load failures and keep the read preflight off the event loop
Review follow-ups:
- Log the failure shape instead of the exception text in both degraded
modes: from_file resolves $VAR placeholders before validation, so its
message can embed resolved credentials. Reuse _describe_load_failure,
the loader's own redaction helper.
- Run the gateway read preflight through asyncio.to_thread in the
capability adapter, matching the scan it precedes.
- Cover the user-scoped degraded branch with a CUSTOM skill that has
per-user state on disk, so the second read's guard is directly
regression-tested (verified red with the guard removed).
---------
Co-authored-by: Jay0130-a <309071492+Jay0130-a@users.noreply.github.com>