2584 Commits

Author SHA1 Message Date
jswSpace
62a5b6cc26
fix(auth): preserve OIDC authorization endpoint query parameters (#6831)
* fix(auth): preserve OIDC authorization endpoint query parameters

* docs(auth): keep updated guidance within size budget

* docs(auth): clarify OIDC endpoint query configuration

---------

Co-authored-by: jswSpace <198684035+jswSpace@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 08:19:22 +08:00
jay
eecfebd419
fix(buzz): treat a scalar allowed_users entry as one member pubkey (#6811)
* fix(buzz): treat a scalar allowed_users entry as one member pubkey

BuzzChannel built its allowlist by iterating config.get("allowed_users") directly. A scalar string -- what an allowed_users: $BUZZ_OWNER_PUBKEY reference resolves to -- was iterated character by character, feeding each character to parse_pubkey, whose first non-hex character raised and aborted channel construction; a numeric scalar raised TypeError the same way. The sibling QQ (#6730) and Discord (#6776) connectors already treat a scalar as a single entry.

Parse the value with the shared parse_allowed_users helper via a parse_pubkey coercer: a scalar is one entry, list entries keep their per-entry validation, unusable entries (bad hex, numeric refs, mappings) are dropped with the shared warning instead of crashing, and the deny-by-default contract is unchanged -- an empty or blank value still yields an empty allowlist with start()'s existing deny-all warning.

config.example.yaml states the accepted shape next to the key, matching the mention_free_channels note added by #6776.

* docs(config): split buzz allowed_users note into aligned comment lines

The two-line note on the buzz example entry was emitted with a literal
`/n` where a newline was intended, so the second note never became its
own comment line and the `#`-column alignment shared with the
DENY-BY-DEFAULT lines below was broken. Split it to match the
mention_free_channels sibling shape (review comment on #6811).
2026-10-14 07:55:06 +08:00
Fish
bf40985fa8
fix(skills): require complete frontmatter delimiter lines (#6817) 2026-10-14 07:51:50 +08:00
OUAO-FRANK
12397d2142
fix(sandbox): forward bash command timeout to remote providers (#6782) 2026-10-14 06:47:38 +08:00
RongJie G
8b62537847
fix(sandbox): honor the readiness budget on a monotonic clock (#6823)
wait_for_sandbox_ready polls a sandbox's health endpoint until ready or a
timeout-second budget elapses, but measured that budget on the wall clock
(start_time = time.time(), while time.time() - start_time < timeout). A
backward wall-clock step (NTP correction, VM resume, manual change) while
polling keeps the loop running far past timeout -- and this runs on the
sandbox acquisition path, so it pins the acquiring thread instead of giving
up -- while a forward jump ends the wait early and destroys a sandbox that
was about to become ready.

The loop also only checked the deadline at the top and then blocked on a
fixed 5s request timeout plus a fixed 1s sleep, so a single slow probe could
overshoot timeout by up to ~6s; on the 5s acquisition path (local_backend)
that roughly doubles the budget.

Rework the wait to a monotonic deadline that clamps both the per-probe
request timeout and the inter-probe sleep to the remaining budget
(min(5.0, remaining) / min(1.0, remaining)), making the sync twin a faithful
mirror of wait_for_sandbox_ready_async, which already does this on
loop.time().

Add two deterministic regressions: one advances a scripted monotonic clock
past the deadline while a fake wall clock steps backward (red on the old
wall-clock loop), and one asserts the per-probe timeout and sleep are clamped
to the remaining budget (red on the old fixed 5s/1s values).

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
2026-10-14 06:43:07 +08:00
jay
8ca2910b39
fix(view-image): bound re-injected image context with per-request budgets (#6829)
* fix(view-image): bound re-injected image context with per-request budgets

`viewed_images` only grows (#6825): every model call that follows a
`view_image` round re-encodes and re-sends every entry ever viewed, so a
long PDF-inspection thread pushes the request body past provider limits
(e.g. Anthropic's 32 MB) even though each single image is capped.

Admission now runs newest-first against two knobs on ViewImageMiddleware:

- `max_injected_images` (default 10)
- `max_injected_total_chars` (default 32M characters, so a single
  max-size image stays injectable: 20 MiB source -> ~27 MB base64)

Over-budget entries keep their text bullet with a placeholder ("call
view_image again to re-view this image") and are never read from disk or
sandbox, bounding both the request size and the middleware's per-call
work. Existing threads with small image counts and sizes are unaffected.

Refs #6825

* fix(view-image): make re-viewing an omitted image actually rescue it

The placeholder told the model to "call view_image again to re-view"
an over-budget image, but the recovery advice could not work:
merge_viewed_images merged with `{**existing, **new}`, and updating an
existing key in Python preserves its original insertion position. The
admission pass walks `reversed(viewed_images.items())` purely by
position, so the re-viewed entry stayed at its old (oldest) slot, was
admitted last, and was omitted again — a wasted tool round.

merge_viewed_images now re-inserts updated keys at the end, so dict
order tracks recency and a re-viewed image is admitted first on the
next injection. Also fix the `_create_image_details_message` docstring,
which said entries are "admitted oldest-first" while admission runs
newest-first (content blocks are emitted oldest-first; the admission
decision is not).

Wiring the budget knobs into config.yaml stays a follow-up: both
production instantiation sites construct ViewImageMiddleware() with no
arguments today, and this PR keeps the surface minimal.

Tests: reducer unit tests for re-insertion order plus an end-to-end
regression that re-views an omitted image and asserts it is admitted
on the next injection (#6829 review).
2026-10-14 06:41:06 +08:00
spud
9fd88c5ffc
fix(mcp): avoid leaking credentials in discovery error logs (#6794)
* fix(mcp): keep resolved credentials out of discovery failure logs

A failed per-server MCP discovery logged the raw exception message and traceback. ExtensionsConfig resolves $VAR values before validation, so a discovery error can embed a resolved-credential request URL (for example an httpx error carrying a URL with userinfo). Log only the server name and the exception type instead, at both the per-server and whole-phase handlers, and pin it with a caplog regression test using a simulated credential.

* test(mcp): assert the exact retained server name when redacting logs

The redaction regression test asserted a bare "A" in the captured log, which any unrelated A would satisfy. Assert the full "server 'A'" fragment so the test actually proves the offending server name is preserved while the credential is not.

* test(mcp): assert structured discovery warning arguments

The redaction change made the per-server discovery warning a parameterized log call, so asserting the server name inside the raw format string no longer matches. Assert the structured arguments instead: the server name, the exception type, the fully formatted message, and the absence of exc_info.
2026-10-14 06:40:20 +08:00
Totoro
c5bbaea02f
fix(channels): clean up interrupted Telegram initialization (#6810)
* fix(channels): clean up interrupted Telegram initialization

* fix(channels): preserve Telegram shutdown failures and finalize generators

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-14 06:20:33 +08:00
xiaodu55
9bd0056f3d
test: fix six Windows-host failures across sandbox, read_file, lark shim, skill review, and extension checks (#6787)
* test: fix five Windows-host failures across sandbox, read_file, lark shim, and extension checks

Windows dev hosts currently fail five backend test cases that CI (ubuntu)
never exercises:

- test_local_sandbox_command_timeout: the fixture forces a bare /bin/sh
  shell and the Windows dispatch spawns it directly (WinError 2), and
  make(1) is not a given on Windows. Stub both runners; the contract under
  test is the timeout marker assembly, not the runner.
- test_local_sandbox_provider_mounts: the Popen-capture mock delegated to
  the real spawn, which cannot execute a POSIX shell path on Windows.
  Capture at the runner seam instead and assert the rewritten path
  shape-agnostically (the Windows runners pass the command string, not a
  [shell, -c, command] list).
- test_read_file_tool_binary: text-mode write_text leaks CRLF into the
  fixture on Windows; write the mount fixture as bytes (#6042 follow-up).
- test_lark_shim_atomic: skip the exact 0o755/0o644 assertions on Windows
  hosts, whose stat mode approximates POSIX bits (#6257 pattern).
- test_extension_check_script: zipfile normalizes os.sep in entry names on
  Windows reads, so a writestr-built fixture can never carry a verbatim
  backslash entry there. Inject the hostile name into namelist unless the
  archive already stores it, pinning the guard contract identically.

On a Windows 11 host these five files go from five failed cases to green
(102 passed, ruff check/format clean). Two more Windows-host failures
(skill-review NUL probe, jina shared-deadline classification) need deeper
root-causing and are left for follow-ups.

* test(skill-review): review the committed skill tree, not local residue

The video-generation review test scans the live directory, so untracked
build residue inside skills/public/video-generation fails it: once bytecode
caching drops a scripts/__pycache__ directory into the tree (importing the
skill script writes one), skillscan's package-undecodable-script finding is
correct and the test can never pass again until the residue is deleted by
hand. Reproduced on Windows: a planted scripts/__pycache__ pyc fails the
test; deleting it makes it pass.

- Materialize the tracked skill tree (git ls-files) into a temp dir and
  review that, so the test reviews exactly the committed package.
- tests/skills/skill_loader.py disables bytecode caching around
  exec_module, so the root skill suite stops polluting the committed tree.

Follows up on #6786 (item 6).

* docs(test): state the argv-list contract on both sandbox runners

The Windows dispatch also passes the resolved command as an argv list, so
the string fallback was unreachable on every platform.

---------

Co-authored-by: xiaodu55 <xiaodu55@users.noreply.github.com>
2026-10-14 06:19:43 +08:00
OUAO-FRANK
c645f65b3a
fix(wechat): extend QR-code status request timeout (#6783)
* fix(wechat): extend QR status polling timeout

* fix(wechat): bound QR polling request timeout

* fix(wechat): enforce QR status deadline
2026-10-14 06:18:58 +08:00
Wenchao An
b5bcf64741
fix(extensions): harden team history cursors and cancelled results (#6832)
* fix(extensions): harden team history cursors and cancelled results

* refactor(extensions): deduplicate team history cursor anchors
2026-10-14 06:05:16 +08:00
RongJie G
d970fe8196
fix(agents): measure LLM circuit breaker recovery on a monotonic clock (#6806)
The LLM error-handling middleware's circuit breaker stored its recovery
deadline as time.time() + recovery_timeout_sec and fast-failed while
time.time() < _circuit_open_until. That is a duration deadline measured on
the wall clock, so a backward wall-clock step (NTP correction, VM resume,
manual change) while the circuit is open holds the fast-fail window open far
past recovery_timeout_sec -- shedding every LLM call until the clock catches
up -- while a forward jump ends it early.

Measure the recovery window with time.monotonic() instead, matching the rest
of the harness (task_tool deadlines, batch lease renewal, guardrail TTLs,
secret-file settle windows, journal latency). The clock is held on the
instance so tests can inject a deterministic one. The adjacent Retry-After
math stays on time.time(): it computes target.timestamp() - time.time() for
an absolute HTTP-date, which is genuinely wall-clock.

Add two regression tests (an injected-clock recovery test and a backward
wall-clock-step test) that go red without the fix, and update the two
existing recovery tests to drive the breaker through the same injectable
clock seam instead of patching time.time.

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
2026-10-14 06:04:08 +08:00
huangzz
80a71b0904
fix(loop-detection): let a warned agent save collected results before its final answer (#6804)
* fix(loop-detection): let a warned agent save collected results before its final answer

The warn-level loop messages told the agent to "Stop calling tools and
produce your final answer now." Subagents read this literally: one whose
task names an output file stops all tool calls, including the single
write that delivers its result, and finishes as "completed" with nothing
on disk.

Only the two warn-level texts change. They now ask the agent to stop
repeating the looping call and wrap up, first saving what it has
collected through a file-writing tool if its task asks for a file. The
per-tool warning names the looping tool. Thresholds and the hard-stop
path (which strips tool calls) are unchanged.

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

* fix(loop-detection): bound final-save recovery after warnings

* docs(middleware): keep inherited guidance within CI budget

---------

Co-authored-by: huangzz <110116@nd.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 06:03:45 +08:00
changjing9527
a5e71751d7
fix(jina): classify retry budget expiry reliably on Windows (#6789) 2026-10-13 22:26:28 +08:00
Nan Gao
5841c8e9b7
fix(channels): make Feishu and DingTalk transports stoppable in-process (#6798)
* fix(channels): make Feishu and DingTalk transports stoppable in-process

FeishuChannel.stop() and DingTalkChannel.stop() could not end their SDK
threads. Both SDKs run on a thread-private event loop that only returned
on a fatal error: lark-oapi's ws.Client.start() parks forever in
run_until_complete(_select()) and has no public stop, and the channel
never kept the client handle; dingtalk-stream's start_forever() is a
while-True loop that also catches CancelledError, and the disconnect()
the channel called does not exist on the SDK. So every stop joined for
5s, raised ChannelStopTimeout, and ChannelService retained the instance
while the platform connection stayed alive on the old process. That
burns the full budget on shutdown and on POST /api/channels/{name}/restart,
and it blocks the per-provider leader lease: a demoted leader could not
release the transport before a peer took over.

Both channels now publish the SDK thread's loop and client under a lock;
stop() schedules a shutdown on that loop and then joins. Feishu disarms
lark's _auto_reconnect before closing the socket and stopping the loop,
because start() otherwise swallows the interrupted run_until_complete in
its reconnect branch and dials again. DingTalk drives its own copy of the
SDK connect loop that re-checks the running flag before every dial, runs
the blocking ticket request off the loop, cancels keepalive with the
socket, and logs the endpoint rather than the ticket-bearing payload. A
shared shutdown_private_loop cancels the SDK's leftover tasks with a
bounded drain and closes the loop; a stop requested before the client
exists never dials. ChannelStopTimeout now only fires for a thread that
genuinely wedges inside an SDK call. Telegram already stopped this way.

Tests: a fake lark ws.Client with the SDK's start() shape (including the
reconnect branch that swallows a bare loop stop), a real local websockets
server that observes the DingTalk close, the stop-before-client race for
both, and a blocking-io anchor that stop() ends a live private loop
without blocking the Gateway loop. All eight were red on main.

* docs(changelog): link the stoppable-transports entry to #6798

* fix(channels): release the socket on a stalled close and bound the DingTalk ticket request

Review follow-ups on #6798 (Totoro-qaq):

Feishu: the bounded _disconnect() wait cancelled websockets' close before
its own close-timeout abort ran, and lark clears _conn in its finally, so
a peer that never answers the close frame left the socket to garbage
collection. Worse, a stop landing while _connect() still held lark's
_lock snapshotted _conn as None. _shutdown_ws now settles the in-flight
dial by taking that lock under one shared close deadline, snapshots the
connection, runs _disconnect() under the same deadline, and aborts the
transport itself (abort_connection_transport in base.py) when the
handshake did not finish, yielding once so connection_lost runs before
loop.stop(). DingTalk's websocket.close() path gets the same abort.

DingTalk: the SDK's open_connection() calls requests.post with no
timeout, and running it in an executor let a stalled ticket endpoint
block interpreter exit (executor workers are joined at shutdown). The
channel now sends the same request through httpx on the stream loop
under a 30s deadline, so stop() cancels it and no worker thread lingers.

Tests drive the real SDK clients against loopback peers: a WebSocket
peer that completes the handshake and never ACKs the close (both
channels assert the connection's socket reports fileno() == -1 right
after stop(); red with the abort disabled), a real websockets server for
the clean path, and a loopback ticket server (ok / 500 / never responds)
with a no-lingering-non-daemon-thread check and a request-shape check.
2026-10-13 22:24:46 +08:00
huangzz
2cf7424a80
fix(llm-error): retry streamed 200-OK responses that carry no generation chunk (#6803)
* fix(llm-error): retry streamed 200-OK responses that carry no generation chunk

The non-streaming empty-generations glitch (IndexError) is already
classified as transient and retried. The streaming path reports the same
upstream glitch as ValueError("No generations found in stream.") from
langchain_core's generate_from_stream; it fell through to "generic" and
failed after one attempt regardless of llm_call.retry_max_attempts.

Classify that ValueError as transient so it uses the configured retry
budget. Unrelated ValueErrors stay generic.

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

* style(llm-error): keep comments adjacent to their constants

---------

Co-authored-by: huangzz <110116@nd.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 22:18:17 +08:00
Totoro
a1e3e75b17
fix(channels): retain Slack connect work through shutdown (#6808)
* fix(channels): retain Slack connect work through shutdown

* fix(channels): distinguish Slack cleanup failures from shutdown deadlines

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-13 22:05:40 +08:00
ZJPex
e4c893f2fb
feat(acceptance): verify required CSV header columns (#6714)
* feat(acceptance): verify required CSV header columns

* fix(acceptance): address CSV header review feedback

* fix(acceptance): guard CSV criteria and malformed headers

* test(acceptance): isolate CSV deadline clock simulations
2026-10-13 19:52:22 +08:00
huyengiang101086-spec
6783073a39
fix(image-search): share public result URL screening (#6740)
* fix(image-search): screen DuckDuckGo result URLs

* fix(image-search): unify provider result URL screening

* docs(image-search): scope URL guidance to avoid inherited size limits

---------

Co-authored-by: huyengiang101086-spec <270684046+huyengiang101086-spec@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 15:50:45 +08:00
Grapette.L
9b2151a3a8
fix(buzz): a single mention_free_channels ID was split into characters, a numeric one stopped the connector (#6776)
* fix(buzz): accept a single mention-free channel id

`_mention_free` iterated the configured value directly, so one unquoted
UUID became a set of its characters and a numeric or boolean scalar
raised TypeError while the channel was being constructed. Both shapes are
documented as valid for the sibling Discord connector.

Parse the key through a dedicated helper that follows the Discord
convention: a scalar is a single entry, list entries are kept verbatim,
and an unusable entry never matches so `require_mention` stays in force.

* fix(channels): share scalar-or-list channel ID parsing

* docs(channels): keep guidance within CI size budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 14:55:49 +08:00
Wenchao An
20887e465c
feat(extensions): retrieve retained team history and results (#6793)
* feat(extensions): retrieve retained team history and results

* fix(extensions): correct team task guidance punctuation
2026-10-13 14:54:47 +08:00
IronMurphy
fa9fca740c
fix(browser): publish captured screenshots and fetches atomically (#6731)
* fix(browser): publish captured screenshots and fetches atomically

_write_screenshot and _write_capture_output wrote the final path with a truncating
write and then returned its name, so a failure part-way through left a partial
capture that the caller reported as saved - and the leftovers then made the dedupe
helper pick a new name for the next capture. Publish through a temporary file plus
os.replace.

* fix(browser): preserve permissions on atomic capture publication

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 14:39:23 +08:00
sjr666666
19e0242476
fix(sandbox): decode cmd.exe output with the ANSI code page under UTF-8 mode (#6777)
* fix(sandbox): decode cmd.exe output with the ANSI code page under UTF-8 mode

The cmd.exe branch left its pipe decoder to the `encoding=None` default of
`_run_windows_command`, which follows Python text-mode semantics via
`locale.getpreferredencoding(False)`. Under Python UTF-8 mode
(`PYTHONUTF8=1`, `-X utf8`, the Windows installer's UTF-8 opt-in, and the
default once PEP 686 lands) that call reports `"utf-8"`, while cmd.exe and
the children it spawns keep writing the host ANSI code page - so every
non-ASCII byte in cmd output collapses to U+FFFD, with no fallback path.

Measured on a real zh-CN host under `PYTHONUTF8=1` (`getpreferredencoding`
-> `utf-8`, `getencoding` -> `cp936`):

- through the `encoding=None` default: `'��� �����Z
'`
- pinned to the ANSI code page: `'你好 日本語
'`

This branch pins the cmd.exe pipe decoder to `locale.getencoding()`
(3.11+, repo floor is 3.12), the same selection the MSYS and PowerShell
branches already use for their fallback (#6727, #6548).

## Scope note

The `encoding=None` default itself is deliberately unchanged: it mirrors
Python's own `text=True` semantics and is pinned by
`test_windows_capture_matches_subprocess_text_mode_encoding`. Only the
cmd.exe branch - whose output is known to be the ANSI code page - overrides
it, the same way the MSYS and PowerShell branches already pin their own
decoders.

Follow-up to willem-bd's review note on #6727 (inline comment 4247281255)
flagging this path.

## Testing

- new (POSIX-runnable, mocked): the cmd branch passes
  `encoding=locale.getencoding()` to `_run_windows_command`, verified under
  a simulated UTF-8-mode host (`getpreferredencoding` -> `utf-8`,
  `getencoding` -> `cp936`).
- updated: `test_execute_command_uses_cmd_command_mode_on_windows` now also
  pins the new `encoding` kwarg.
- new (Windows; skipped when the console output CP does not match the ANSI
  page - sibling tests can change it process-wide): real `cmd.exe /c echo`
  roundtrip through `execute_command` returns the text intact (passes on a
  zh-CN host when run standalone).
- full `tests/test_local_sandbox_encoding.py`: 40 passed, 2 skipped under
  both `PYTHONUTF8=0` and `PYTHONUTF8=1` (Windows 11, Python 3.13.12, repo
  venv); the text-mode contract test stays green in all three parametrized
  modes (`locale-code-page`, `PYTHONUTF8`, `-X-utf8`). Red run against
  unmodified main: both new/updated cmd-branch tests fail (`KeyError:
  'encoding'`).
- `ruff check` + `ruff format --check` clean on both files.

* fix(sandbox): use the cmd console output code page

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 14:27:26 +08:00
spud
3343522fa2
refactor(mcp): expose per-server tool discovery results (#6734)
* refactor(mcp): expose per-server tool discovery results

* refactor(mcp): clarify per-server discovery contracts
2026-10-13 08:38:36 +08:00
RongJie G
81f4d7707f
fix(runtime): preserve canonical delta-history order in cache (#6774)
* fix(runtime): preserve canonical delta-history order in cache

CachedHistorySaver composed a parent checkpoint's channel writes from
CheckpointTuple.pending_writes in storage order, but the saver's
authoritative get_delta_channel_history orders writes by (task_id, idx).
On the in-memory backend pending_writes follows task-arrival order, so a
fan-out super-step (several parallel tasks writing one channel in a single
checkpoint) was materialized in a different order with the cache enabled
than with the bare saver, breaking the cache's transparency invariant.

Sort a checkpoint's channel writes by task_id in _channel_writes, restoring
the canonical (task_id, idx) order while preserving each task's internal idx
sequence. The sort is a no-op on sqlite and postgres, whose pending_writes
already arrive ORDER BY task_id, idx.

Add async and sync regression tests over a real InMemorySaver seeded with a
fan-out checkpoint whose writes arrive out of task_id order; both diverge
from the inner saver's native walk without the fix and match with it.

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>

* docs(runtime): clarify cached saver ordering contract

---------

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 08:34:05 +08:00
Eilen Shin
e98179a9d3
feat(models): register managed models with provider presets (#6757)
* feat(models): register managed models with provider presets

* docs: add PR 6757 UI comparison screenshots

* test: fix model management CI checks

* docs: remove PR 6757 screenshots

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 08:14:23 +08:00
FanouZeng-TT
8319e3384b
fix(telegram): retain loop cleanup across bounded stops (#6772)
* fix(telegram): retain loop cleanup across bounded stops

* fix(telegram): release abandoned cleanup generations

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 08:00:41 +08:00
Grapette.L
c53621d769
fix(wechat): a scalar allowed_file_extensions config silently widens back to all 37 defaults (#6737)
* test(wechat): cover allowed_file_extensions coercion boundaries

* fix(wechat): never widen a configured allowed_file_extensions back to defaults

* fix(wechat): warn when a scalar allowed_file_extensions looks like a list

A comma- or space-separated scalar string is still one literal extension, so
`allowed_file_extensions: ".pdf, .zip"` blocks every file with nothing in the
startup log pointing at the config. Mirror the `_parse_allowed_users` guard.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 07:26:59 +08:00
Totoro
5ae95e6a34
feat: add Feishu group notifications and fix mention caret placement (#6762)
* feat(feishu): deliver scheduled-task notifications to bound users

* feat: add Feishu group notifications and fix mention caret placement

* fix(frontend): preserve group targets while capabilities load

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-13 07:17:56 +08:00
Wenchao An
8cec469738
fix(extensions): isolate team workers and rotate admissions (#6754)
* fix(extensions): isolate team workers and rotate admissions

* fix(extensions): index and batch pending team snapshots

* fix(agent-teams): retain unfinished owner rotation rounds

Carry the remaining owner queue across planning passes, including exhausted owners and newcomers. Read legacy rows completely before updating their pending projection and distinguish dispatch reservations from sorted entries.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 23:22:43 +08:00
Nan Gao
d18d2937e7
feat(channels): add channels.leader_lease and gate an explicitly disabled lease under multi-process signals (#6746)
* feat(channels): add channels.leader_lease and gate it under multi-process signals

IM channels other than GitHub connect outbound to their platform. With more
than one Gateway instance each one opens that connection with the same bot
credentials: Telegram rejects the second poller with 409, Discord answers every
message twice and per-process ChannelManager state diverges. The multi-process
startup gate said nothing about channels; the only guard was a comment in the
Helm values.

Add the `channels.leader_lease` section (`enabled: auto|true|false`,
`lease_seconds`, `grace_seconds`; startup-only, registered in the reload
boundary) that the per-provider leader lease will read, validated by a Pydantic
model from the non-schema `channels` mapping. The gate now refuses
`enabled: false` under a multi-process signal while a non-GitHub provider is
enabled under `channels.*` or connectable through `channel_connections`, naming
the providers and the rollback; a malformed block is refused on every topology.
`auto` follows the multi-process signal, so a single instance is unchanged.

ChannelService strips the block before its per-provider loops and exposes it
as `leader_lease`; `ensure_ready_channels` only visits registered providers.
The two-Gateway harness overlay leaves the block alone. config_version 60 in
the example, the Helm chart and its README; docs replace the comment-only
"keep 1 replica" guidance with the gate.

* docs(changelog): link #6746

* fix(channels): stop recommending the unimplemented leader lease as a remedy

Review on #6746: the refusal told operators to switch channels.leader_lease
from false to auto/true "so one instance owns each provider's connection", but
the transport arbitration ships separately, so that change only bypassed the
clause and every worker still started the same bot.

The refusal now offers only the remedies that work today (a single instance, or
disabling the channels) and states that auto/true are reserved for the lease
that ships separately. A regression helper in the gate tests asserts the
remedies start with the rollback and that auto/true are never offered. The
config example, Helm values/README, CONFIGURATION.md, README, the deployment
guide (en/zh), the model docstrings and both changelogs describe auto/true as
reserved settings rather than an ownership guarantee.

* fix(channels): warn on an enabled unknown channel key; reword the changelog entry

Review on #6746: the registry guard in ensure_ready_channels skipped a
misspelled provider key (channels.telegam) silently, where it previously
reached _start_channel and logged 'Unknown channel type'. An enabled dict
block that is neither a registered provider nor leader_lease now logs a
warning that lists the registry.

The changelog entry claimed the chart and docs drop the "keep 1 replica"
guidance; they keep it (auto/true do not make several replicas safe yet) and
only stop relying on the comment alone.

* fix(dev): disable UI channel providers in multi-instance overlay

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 23:05:16 +08:00
哈基米
f4d10ef42d
fix(memory): read the complete scope in the substring search fallback (#6732)
* fix(memory): read the complete scope in the substring search fallback

_search_substring iterated list_facts without paginating, so the
adapter-less fallback only ever saw the default page of 100 facts.
With 150 matching facts in a scope (max_facts allows up to 500),
memory_search returned 100 results for top_k=150 and silently dropped
every match past the first page; DeerMem.search keeps these results
whenever they are non-empty, so its uncapped fallback never ran. Read
the complete scope instead, as the scoped index rebuild already does.

* test(memory): drop in-page fact-100 from the past-page assertion

fact-100 was the last id inside list_facts' old first page
(``matched[0:100]`` in insertion order), so its presence does not
distinguish the fixed search from the pre-fix page; fact-101 and
fact-150 (plus the ``len(results) == 150`` check) carry the regression
signal. Keep a boundary note so the remaining ids are not misread.
2026-10-12 22:54:02 +08:00
Wenchao An
0b78051840
feat(extensions): add example guide and packaging checks (#6755)
* feat(extensions): add example guide and packaging checks

* fix(extensions): align packaging checks with installer

Reject Windows drive-qualified wheel paths before extraction and reuse the installer entry-point validator. Cover unsafe paths, dotted attributes, and valid dotted modules in regression tests.

* fix(extensions): skip unused wheel extraction

Only extract wheels when browser manifests need validation, with a backend-only regression test. Link the three remaining example READMEs in the developer guide.

* docs(frontend): trim guidance intro below size budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 22:51:20 +08:00
哈基米
1b7ca5ecf1
fix(memory): keep non-core fact categories instead of rewriting to other (#6733)
* fix(memory): keep non-core fact categories instead of rewriting to other

_normalize_category rewrote any category outside CORE_CATEGORIES to
"other" and parked the original label in a write-only
categoryExtension field, so memory_add(category="personal") -- an
example label in the tool's own docstring -- persisted as "other" and
memory_search(category="personal") ("Only facts with this exact category
are returned") never matched it. get_memory documents kept reporting
"other" as well.

Keep the caller's label as the stored category so it round-trips
through search, documents, and the retrieval index.

* fix(memory): finish migrations interrupted across the category change

A pre-native-category Gateway committed a per-agent legacy JSON migration
as category="other" + categoryExtension=<label> and could stop before
deleting the legacy JSON (crash / failed unlink). The retained JSON holds
the label natively, so _migrate_locked compares "personal" against its
own committed "other"/"personal" alias, raises MemoryStorageCorruption,
and fails every subsequent read, reload and migration retry for that
agent (HTTP 500 at the Gateway) -- a recoverable state the previous
build recovered from.

Fold the legacy alias into _migration_equivalent's category comparison
("other"/<label> equals <label>/no-extension) so retries recognize and
complete recovery, while a genuinely different label still stops the
migration with the legacy file preserved.

Also drop a stale categoryExtension when a fact is normalized away from
"other": _normalize_fact deep-copies unknown fields through, so a legacy
fact updated to its native category kept surfacing the leftover
extension, contradicting the Gateway contract that the field is only
set "when category is 'other'".

Both regressions are pinned by replaying the historical writer in tests.

---------

Co-authored-by: BlueX888 <BlueX888@users.noreply.github.com>
2026-10-12 22:45:08 +08:00
xiaodu55
22ddb77f83
docs(agents): trim restated rationale to restore AG002 chain headroom (#6759)
* docs(agents): trim restated rationale to restore AG002 chain headroom

The agents/middlewares AGENTS.md chain crossed the 96 KiB AG002 hard
limit at 3f7a0769 (98441 > 98304 bytes); that push produced no lint-check
run, and later pushes compare equal to their base, so main stayed green
while a full-worktree check_agent_guidance run reports an error. Trim
restated rationale:

- backend/AGENTS.md: the Feishu button-card sentence restated semantics
  app/channels/AGENTS.md already owns (originator-validated, one-time,
  fail-closed clicks); keep the flag visibility and point at the module
  guide.
- agents/middlewares/AGENTS.md: drop the membership-probe rationale
  duplicated by the model_response.py paragraph, the ToolOutputBudget
  ordering narration, and the lead-only append note restated by the
  lead-only middlewares heading.

Chain: 98441 -> 98198 bytes (106 under the hard limit).

* docs(channels): own the Feishu card fail-closed invariant in the module guide

Review follow-up on #6759: the backend/AGENTS.md trim dropped two things
app/channels/AGENTS.md item 7 does not own - the fail-closed-across-workers
guarantee and the owning file's name - so the invariant left every
instruction chain and the pointer dead-ended one level above the
implementation. State both in item 7, paid by dropping the therefore-clause
restated by the single-active-owner invariant above it (the partial unique
index and the losing-writer retry already imply deterministic resolution).

channels/AGENTS.md: 49134 -> 49129 bytes (AG001 headroom 18 -> 23).

---------

Co-authored-by: xiaodu55 <xiaodu55@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 22:22:44 +08:00
Zhengxu (Joshua) Jin
47b6755ce9
fix(qq): parse allowed_users with the shared fail-closed allowlist (#6730)
* fix(qq): parse allowed_users with the shared fail-closed allowlist

QQChannel built its allowlist with set(config.get("allowed_users") or []).
A single OpenID, which is what an `allowed_users: $QQ_ALLOWED_USERS`
reference resolves to, became a set of its characters, so the configured
user was silently dropped. A mapping authorized every key, including one
mapped to false, and a numeric scalar raised TypeError while the channel
was being constructed.

Parse it with the shared parse_allowed_users helper that Telegram and
WeChat already use: unset, null, empty collections and a blank string
allow everyone; a scalar OpenID is one entry; entries that are not
non-blank strings (such as numeric QQ account numbers) are dropped with a
warning, and a mapping is rejected as a whole; a configured value with no
valid OpenID denies every sender and logs an error. A scalar containing commas or whitespace warns
that it stays one literal ID. Binding codes still precede the allowlist.

* test(qq): gate group messages by member OpenID in allowlist tests

Every allowlist test sent a C2C frame, and the shared event() helper puts
the sender in both user_openid and member_openid, so a group-path
regression in sender resolution would pass unnoticed. admits() now takes
a kind; group frames carry only member_openid and a group_openid that
differs from the sender. The listed-OpenID test runs for both kinds, and a
new test shows that listing the group's OpenID does not admit its members.

Document that a blank or whitespace-only allowed_users string allows every
sender, like an unset value or an empty list.

* refactor(channels): share scalar allowlist separator warnings

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 22:19:11 +08:00
Lucien
db46250d85
fix(opensandbox): mirror thread outputs before warm reuse (#6699)
* fix(opensandbox): mirror thread outputs before warm reuse

* docs(opensandbox): scope output synchronization guidance locally

* docs(opensandbox): clarify portable output name requirements

* fix(opensandbox): isolate invalid filenames and type lifecycle closure

* fix(opensandbox): mirror complete records after find walk errors

* fix(opensandbox): reject ambiguous output path aliases

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 22:16:33 +08:00
lilei
b346b17fce
fix(client): convert the captured upload source (#6632)
* fix(client): convert the captured upload source

* test(uploads): cover ACL sources and scope capture failures

* fix(uploads): preserve concurrent uploads from one captured source

Combine the embedded client's source capture with no-replace publication for
first-time filenames and Markdown companions. Hold one private capture across
collision retries, retain original descriptor metadata, and keep replacement
behavior for names present when the call starts.

Port concurrent-upload and staging-publication regressions from PR #6687.
Extend source-edit/removal and real XLSX cases to both publication paths, and
cover edits or removal during two filename collisions in ordinary and event-loop
calls. Document capture, retry, publication, and cleanup contracts.

Validation: related suites 469 passed, 24 skipped; full offline suite 28,788
passed, 488 skipped; blocking-I/O suite 245 passed, 1 skipped; Ruff, guidance,
and diff checks passed. Nine race/collision cases failed on the #6632 baseline.

Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>

* docs: keep agent guidance within CI budgets

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
2026-10-12 21:50:19 +08:00
sjr666666
e81fdec459
fix(sandbox): decode PowerShell native-child output with the locale fallback (#6727)
* fix(sandbox): decode PowerShell native-child output with the locale fallback

Fixes #6646

## What

The Windows PowerShell branch of `LocalSandbox.execute_command` passed `encoding="utf-8"` to `_run_windows_command` with no `fallback_encoding`. The guarded console-UTF-8 preamble covers PowerShell's *own* output, but native children it runs (`python.exe`, CLI shims) inherit the pipe and keep writing the host code page, so the pipe decoder's `errors="replace"` turned every such byte into U+FFFD. This passes `fallback_encoding=locale.getpreferredencoding(False)` there, mirroring what the MSYS branch above already applies (#6548).

## Why

On Windows hosts whose ANSI code page is not UTF-8 (GBK on zh-CN hosts, cp932, cp949, cp1251, ...), non-ASCII tool results printed by native children reach the model garbled (#6646). PowerShell's own output stays correct because the preamble pins it to UTF-8, so the fallback only engages when a buffer is not valid UTF-8 — exactly the case where a native child wrote host-code-page bytes. Pure-ASCII and PowerShell-emitted UTF-8 buffers never touch the fallback, matching the tradeoff the MSYS branch already accepts.

## How checked

Reproduced on a real zh-CN Windows host (ANSI code page 936):

- Red on `main` (3f7a0769): a native `python.exe` child printing CJK text through `powershell.exe` / `pwsh.exe` returned `'\ufffd\ufffd\ufffd \ufffd\u0571\ufffd\ufffdZ\n'` — the new regression test failed on both shells:
  ```
  FAILED tests/test_local_sandbox_encoding.py::test_windows_powershell_native_child_cjk_roundtrip[powershell.exe]
  FAILED tests/test_local_sandbox_encoding.py::test_windows_powershell_native_child_cjk_roundtrip[pwsh.exe]
  ```
- Green with the fix: both pass, and the child's text round-trips exactly (`'\u4f60\u597d \u65e5\u672c\u8a9e\n'`).
- `tests/test_local_sandbox_encoding.py`: 34 passed, 1 skipped (Windows 11, CPython 3.13, `uv sync --frozen` environment). The two pre-existing PowerShell unit tests were extended so their `_run_windows_command` fakes accept the new `fallback_encoding` kwarg and assert it is the locale code page.
- `ruff check` and `ruff format --check` pass on both changed files.

* fix(sandbox): decode fallback must survive Python UTF-8 mode and mid-character captures

Follow-up to review feedback on #6727.

- Use ``locale.getencoding()`` (3.11+, repo floor is 3.12) instead of
  ``locale.getpreferredencoding(False)`` for both fallback call sites
  (MSYS and the native-child guard): ``getpreferredencoding`` reports
  ``"utf-8"`` whenever the Gateway runs under Python UTF-8 mode
  (``PYTHONUTF8=1``, ``-X utf8``), silently disabling the fallback and
  leaving #6646 unfixed on those hosts. ``getencoding()`` deliberately
  ignores UTF-8 mode and returns the code page the child's CRT used.
- After a failed strict UTF-8 decode, retry once without a trailing
  incomplete multi-byte sequence, so a capture cut mid-character (the
  10MB capture-cap boundary, or a timeout kill interrupting the child
  mid-write) cannot cascade the whole buffer to the locale fallback.
  Complete non-UTF-8 buffers still fail the retried attempt and reach
  the fallback untouched.
- Tests: the Windows roundtrip probes now run under ``PYTHONUTF8=1``
  with ``locale.getencoding`` pinned, so a green run proves the fallback
  source ignores UTF-8 mode; added POSIX-runnable unit tests for the
  cap-split and timeout-kill partial tails.

Test results (Windows host, ACP=936, repo venv via ``uv sync --frozen``):
``tests/test_local_sandbox_encoding.py`` 36 passed, 1 skipped, both with
``PYTHONUTF8=0`` and ``PYTHONUTF8=1``; ``ruff check`` and
``ruff format --check`` clean on both files.

* fix(sandbox): trim only on cut captures, preserving complete locale tails

Willem-bd's follow-up review pinned a real P2: on a cp1252 host the MSYS
branch receives b'caf\xe9' (a complete e-acute), the trailing E9 is trimmed
as an incomplete UTF-8 lead, and the ASCII prefix is accepted as valid UTF-8
- so the locale fallback never runs and the byte is silently dropped. No
capture cap or timeout is needed to trigger it.

Fix: the strict-retry trim is now authorized only when the capture may
actually have been cut mid-character - a byte-cap cut (signalled by the
capture's own truncated state) or a timeout kill (new mark_split_tail(),
called from _run_windows_command right after the process tree is killed). A
complete buffer, even one ending in a byte shaped like a bare UTF-8 lead,
now goes to the fallback with every byte intact.

Tests:
- new: cp1252 complete-tail roundtrip (red on b62ef75: returned 'caf').
- new: the same partial-shaped tail without a cut signal must fall back
  intact instead of being accepted as a truncated UTF-8 prefix.
- new: POSIX-runnable _run_windows_command timeout integration test - the
  child writes a partial UTF-8 sequence, hangs, is killed at the 1s
  timeout; the trimmed prefix survives (this goes red if the kill path
  forgets to authorize the trim, since the whole buffer cascades to the
  fallback).
- updated: the timeout-kill unit test now marks the capture explicitly,
  mirroring the real call site.

Evidence: Windows 11, Python 3.13.12 (repo venv), pytest
tests/test_local_sandbox_encoding.py -> 39 passed, 1 skipped under both
PYTHONUTF8=0 and PYTHONUTF8=1; ruff check + ruff format --check clean.
Red run against b62ef75: the three new/updated tests fail exactly as
described above.
2026-10-12 21:11:18 +08:00
D41910
62588f3675
fix(sofya): report a format error for a results list with no usable objects (#6719)
* fix(sofya): report a format error for results with no usable objects

`_response_results` skipped non-object entries but returned the filtered
list even when it was empty, so a non-empty `results` array holding no
objects fell through to "No results found" -- telling the agent the query
legitimately matched nothing instead of surfacing the malformed payload.

Treat a non-empty list with no object entries as malformed, matching the
containment rule #6603 documents for this provider family (brave #6550,
groundroute #6603, tavily #6645, webz). Both `web_search` and
`web_fetch` share this helper, so both now report the provider's format
error. Missing, null, and empty `results` keep returning "No results
found", and mixed lists still keep their valid entries in order.

* test(sofya): cover mixed fetch result entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 21:00:24 +08:00
jay
17f774b442
fix(dingtalk): preserve Markdown table cell boundaries (#6741)
* fix(dingtalk): preserve Markdown table cell boundaries

DingTalk's sampleMarkdown fallback re-splits table rows on every pipe, so an
escaped pipe (\|) inside a cell is read as a column separator and every value
after it shifts one column to the left. The strip("|") wrapper also removes an
empty leading or trailing cell, dropping that value's column label.

Split rows on unescaped pipes only, and drop at most one outer border cell per
side, so escaped pipes and empty edge cells stay under the right label.

Adds six parameterized regressions plus one full fallback-adapter regression;
all seven fail on the base commit and pass after the fix.

* docs(dingtalk): document escaped-backslash boundary in table row splitter

Document the residual boundary case flagged in review: the
single-character lookbehind in _split_markdown_table_row cannot
distinguish an escaped pipe (\|) from an escaped backslash followed
by a real separator (\\|), so a cell ending in a literal backslash
absorbs the next cell. Full GFM backslash counting is out of scope
for this helper.
2026-10-12 20:34:42 +08:00
Willem Jiang
e5a72b5009
Revert "fix(uploads): convert the same source version the upload saved (#6621)" (#6763)
This reverts commit 9c071c7053379590b63618498fecd0a626ac95e6.
2026-10-12 20:32:54 +08:00
Yufeng He
9c071c7053
fix(uploads): convert the same source version the upload saved (#6621)
* fix(uploads): convert the same source version the upload saved (#6600)

DeerFlowClient.upload_files copied the caller's file into the uploads
directory, then reopened the original path for Markdown conversion. A
save landing between the two reads left the saved upload at version A
while the companion registered version B, and resolve_companion still
accepted the mismatched Markdown.

Snapshot convertible sources into a private temp dir up front and run
both the upload copy and the conversion from that one read. The snapshot
keeps the #5611 invariant (conversion never reopens a name inside the
sandbox-writable uploads dir), preserves metadata via copy2, and keeps
the re-upload SameFileError rejection through an explicit same-file
probe before the snapshot.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(uploads): pin the snapshot copy to one held source descriptor

Review on #6621 found two gaps in the snapshot step: stat-by-name then
copy2-by-name split identity from content across two path resolutions,
and copy2 drops ACLs so a source readable only via ACL made the private
snapshot unreadable. Open the source once, fstat it for the same-file
rejection, and copyfileobj out of the held fd; restore the mtime from
that same stat so the upload keeps the source's timestamps.

Tests: the version-parity regression's injection now also fires when a
future regression reads the caller's path during conversion, and a real
macOS ACL case (mode 000 + read grant) uploads and converts cleanly.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* test: catch upload conversion source regressions

---------

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 20:12:23 +08:00
Totoro
4b36e680c3
feat(feishu): deliver scheduled-task notifications to bound users (#6753)
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-12 20:08:16 +08:00
PeaceMaker-best
65fa321ec1
feat(deer-flow): configure agent tool access (#6630)
* feat(deer-flow): configure agent tool access

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(frontend): reconcile knowledge defaults with agent tool access

* docs: reduce frontend agent guidance below size limit

* fix: address agent tool-group catalog review findings

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 19:00:54 +08:00
ZJPex
9a0ea6abf7
fix(sandbox): enforce image byte limits during downloads (#6712)
* fix(sandbox): enforce image byte limits during downloads

* docs: keep image download guidance within inherited budget

* docs(sandbox): specify download overflow error contract

* refactor(sandbox): consolidate download budgets and explain AIO locking

Signed-off-by: ZJPex <3258236335@qq.com>

---------

Signed-off-by: ZJPex <3258236335@qq.com>
2026-10-12 18:49:41 +08:00
AochenShen99
5b2a1352cd
fix(agents): allow SQL updates with retained legacy files (#6747) 2026-10-12 17:49:55 +08:00
Hyeonsang Cho
16a4f780c1
fix(gateway): read subagent batch exports in small pages (#6706)
* fix(gateway): read subagent batch exports in small pages

The results.jsonl export loaded 500 full rows per page before streaming
them. A row can hold max_result_chars of report plus as much evidence
JSON (result_artifact), so with the default limit one request peaked
around 280 MB and decoding a page's JSON columns stalled the event loop
for about half a second; at the 1,000,000 ceiling a page was roughly
1.7 GB.

Read 10 rows per page instead. Output and ordering are unchanged; a
full export of 1,000 default-sized rows now peaks at 6 MB with a 46 ms
worst stall and takes the same time.

* docs(changelog): reference #6706

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 17:38:24 +08:00
Storm
3f7a076972
feat(channels): Feishu interactive clarification cards (#6670)
* feat(channels): render Feishu option clarifications as button cards

When the agent's ask_clarification carries options, the Feishu channel
now renders the question as an interactive card with one button per
option instead of plain text. This is the outbound half: card
construction plus a process-local pending registry that a later
card-action callback handler will validate clicks against.

- New feishu_card_interaction module owns request extraction, button
  card rendering, and the pending registry (30-minute TTL, matching the
  typed-reply clarification window)
- Manager attaches the clarification's structured human_input payload to
  the final outbound's metadata so channels can render richer
  affordances without touching the Gateway protocol
- Feishu channel renders the button card only when
  card_interaction_enabled is set (default off); free-text, form, and
  over-four-option clarifications keep the plain-text path
- Button cards bypass the markdown wrapper via a raw_card_content flag
  on the card send/patch helpers; plain-text call signatures unchanged

* feat(channels): close the Feishu button-card click loop

The inbound half of interactive clarification cards: a click on an
option button validates, resolves the clarification, and marks the
card in place.

- Register the card-action callback only when card_interaction_enabled
  is set; clicks arrive as WS EVENT frames through the SDK's callback
  processor map, and the synchronous return value becomes the toast ack
- consume_if_originator atomically validates operator identity against
  the pending entry: non-originators get a refusal toast and the entry
  survives; unknown or expired requests are toast-only and dropped
- A validated click consumes once, updates the card to a terminal
  resolved state, and injects a hidden inbound carrying the
  human_input_response so the run resumes on the originating thread
  with Web-card parity
- Manager translates the inbound's human_input_response metadata into
  the human message's additional_kwargs with hide_from_ui set

* docs(channels): document Feishu interactive clarification cards

- README / README_zh: optional card_interaction_enabled setup, calling
  out that card.action.trigger lives under the console's callback
  configuration tab (separate from event subscriptions) and requires
  publishing a new app version
- config.example.yaml: commented flag with the same console caveat
- backend/AGENTS.md: module ownership note for feishu_card_interaction

* fix(channels): close Feishu card-click routing and ack gaps from review

Address review on #6670:

- Carry the routing identity (topic_id, connection_id, owner_user_id)
  on the pending entry and the synthetic inbound, so clicks resolve to
  the clarified thread in topic-scoped chats and pass bound-identity
  admission instead of opening a new thread or being rejected
- Validate option_value against the registered options at consume time:
  an unknown value is treated like a stale card and the entry is kept
- Return a warning toast when a click cannot be dispatched, and flip
  the card to its resolved state only after the inbound is durably in
  the bus — never display a choice that was not recorded
- Drop the dead mark_sent/consume registry APIs and pass the payload's
  own option ids through to the buttons
- Add an end-to-end test through the manager's thread resolution
  pinning that a card answer reuses the clarified thread

* fix(channels): carry workspace_id on the Feishu card-click inbound

The connections-enabled admission path re-reads the binding with the
message's workspace_id, and Feishu connections are bound with
workspace_id = chat_id. The synthetic inbound set connection_id and
owner_user_id but never workspace_id, so the re-read missed and every
click was rejected as unbound after the entry was consumed.

- Set inbound.workspace_id from the entry's chat_id (Feishu's
  chat-as-workspace binding convention)
- Extend the card-click e2e to require_bound_identity=True, driven
  through the channel's real _resolve_card_click production

* fix(channels): stabilize Feishu clarification delivery and routing

Option cards could fail through the real reply SDK, resume another conversation,
or leave a stale text hint after the answer was accepted.

Unify raw-card delivery and persist trusted thread aliases before showing buttons.
Keep protocol request IDs separate from bounded, one-time callback tokens; validate
originators and option pairs, and retire only the accepted card's text/button state.
Treat valid hidden human answers as turn boundaries so old replies and artifacts
do not leak into the next answer. Cover consecutive clarifications and /new routing
through real SDK request models, stores, and the run journal.

Validation: 147 focused tests and 29,421 offline tests passed (405 skipped);
strict blocking-I/O passed 245 tests (1 skipped). Ruff and guidance checks passed.

* fix(channels): preserve clarification context in Feishu cards

Button cards replaced the full clarification text but omitted its background,
leaving users without the context needed to choose an option.

Carry optional context from request metadata and render its original Markdown
before the question. Keep question-only cards for absent, blank, or invalid context.
Cover rendering and all real SDK delivery paths, and update both user guides.

Validation: 156 focused tests, 245 strict blocking-I/O tests, Ruff, and guidance
checks passed. The offline run recorded 29,381 passed, 49 failed, and 405 skipped;
48 import failures reproduced before the fix. All 49 cases passed on both versions
in isolated environments, and the Feishu-to-export order check passed 13 tests.
2026-10-11 23:58:54 +08:00
CodeAlex
a1383a7953
fix(skillscan): match fork bombs by shape, not spelling (#6689)
* fix(skillscan): match fork bombs by shape, not spelling

`resource-fork-bomb` and the fork-bomb branch of `shell-destructive-command`
only matched the canonical `:(){ :|:& };:` rendering, so renamed or respaced
payloads produced no findings and passed the blocking gate.

Match the general shape instead - a shell function whose body recurses through
a pipe into a background job - keeping the pre-LLM static gate at least as
wide as the runtime sandbox audit middleware it backstops.

Closes #6651.

* fix(skillscan): bound the fork-bomb pattern and require self-recursion

Addresses the three review findings on #6689:

- Bound every quantifier in the shared fork-bomb pattern so a whitespace-free
  blob costs O(n) instead of O(n^2) (measured ~0.15s at 200KB; the previous
  spelling took ~5.5s at just 50KB).
- Require the function name to recur after the pipe (backreference), so a
  benign pipeline such as `backup() { rsync -a src/ | logger & }` no longer
  blocks as a CRITICAL finding.
- Keep both gates byte-identical again: skillscan's _FORK_BOMB_PATTERN and the
  sandbox_audit_middleware fork-bomb entry, pinned by a parity test.
- The code comment now describes exactly what the pattern checks; pipe-less
  self-backgrounding recursion and the spaced `|&` spelling are documented as
  out of scope.
- New regression tests: near-miss pipelines in both suites, the parity
  invariant, and a 200KB single-token scan-time ceiling.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-11 23:27:06 +08:00