* fix(minimax): bound image/video success response reads
MiniMax image generation and video generation (task submit + status poll)
read their success responses through unbounded `await response.json()`, so
a misbehaving or hostile endpoint could stream an arbitrarily large body
into memory before parsing and exhaust the process. Read those success
bodies through the shared bounded reader (16 MiB cap, the same limit other
bundled providers and the sibling MiniMax web-search provider already use)
and cancel the stream on overflow. The error-body path is already bounded
via assertOkOrThrowHttpError; this closes the matching success-JSON gap.
MiniMax TTS is already bounded and is left unchanged.
AI-assisted.
* fix(minimax): bound video metadata response reads
* fix(minimax): leave image response sizing to image hardening
* fix(minimax): bound image/video success response reads
MiniMax image generation and video generation (task submit + status poll)
read their success responses through unbounded `await response.json()`, so
a misbehaving or hostile endpoint could stream an arbitrarily large body
into memory before parsing and exhaust the process. Read those success
bodies through the shared bounded reader (16 MiB cap, the same limit other
bundled providers and the sibling MiniMax web-search provider already use)
and cancel the stream on overflow. The error-body path is already bounded
via assertOkOrThrowHttpError; this closes the matching success-JSON gap.
MiniMax TTS is already bounded and is left unchanged.
AI-assisted.
* fix(minimax): bound video metadata response reads
* fix(mattermost): bound successful REST JSON/text response reads
The Mattermost REST client already bounds error bodies
(readResponseTextLimited) and streams guarded responses without buffering,
but the success path still called `await res.json()` / `await res.text()`,
reading the whole body into memory before parsing. A self-hosted or
compromised Mattermost server can return an arbitrarily large (or
never-terminating, content-length-less) JSON/text body and force the plugin
to buffer it unbounded.
Read successful JSON through the shared readProviderJsonResponse (16 MiB cap,
cancels the stream and throws a bounded error on overflow, same as the
provider HTTP path) and cap non-JSON success bodies with readResponseTextLimited.
uploadMattermostFile's file-info JSON is bounded the same way.
Symmetric follow-up to the #95103 / #95108 response-limit campaign.
AI-assisted.
* fix(mattermost): bound probe success JSON reads
* fix(mattermost): reject oversized success text bodies
* fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM
Nextcloud Talk talks to self-hosted servers whose HTTP responses are not
trusted to be small. The send and reaction paths buffered three external
bodies without any byte cap:
- success JSON via await response.json()
- send error text via await response.text()
- reaction error text via await response.text()
A hostile or misbehaving Nextcloud endpoint could stream an unbounded body
(no content-length) into memory, pressuring or hanging the plugin/provider
path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error
bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow.
The 'message sent but receipt JSON unreadable -> unknown' fallback is
preserved (an over-limit body now also routes through the existing catch).
This is the symmetric counterpart to the #95103/#95108 response-limit
campaign, reusing the shared @openclaw/media-core helpers (newly re-exported
from plugin-sdk/response-limit-runtime for plugin consumers).
* fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface)
Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime
pushed the public plugin-SDK export count past its surface budget, failing
plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the
Nextcloud Talk send/reaction error bodies through the already-public
readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the
bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is
unchanged for callers; no new plugin-SDK surface is introduced.
Success JSON still reads through readResponseWithLimit (16 MiB cap). The
committed bounded-response-reads Vitest suite continues to prove the caps
hold against 17 MiB streamed bodies with no content-length.
* fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON
The send success receipt parsed JSON by hand via readResponseWithLimit + a
local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)),
duplicating the shared provider-http helper that the sibling room-info.ts and
bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing
shared helpers locally.
Swap the hand-rolled block for the one-stop
readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which
reads through the same bounded reader and throws on overflow/malformed JSON, so
the outer try/catch still keeps the "unknown" receipt and behavior is
equivalent. The error path keeps readResponseTextLimited (text, not JSON).
The Discord REST main response path read the body with an unbounded
await response.text() before JSON-parsing it. A controlled or hijacked
endpoint could stream an arbitrarily large body and exhaust memory (OOM).
Wrap the read in the canonical readResponseWithLimit helper with an 8 MiB
cap (well above any legitimate Discord JSON payload) plus an idle timeout
tied to the request timeout, so the stream is cancelled at the cap or on
stall instead of buffering unbounded. Normal payloads still parse fully.
This mirrors PR #95108 which bounded the analogous Anthropic Messages
error-response read with the same helper.
* fix(plugin-sdk): follow paginated live model catalogs
* fix(plugin-sdk): bound paginated live catalog discovery
* fix(plugin-sdk): reject incomplete live catalog pagination
* fix(plugin-sdk): resolve live catalog next links after redirects
* fix(plugin-sdk): preserve redirect auth boundary in catalog pagination
* [AI] fix(plugins): recognize document-extractors as a capability kind in inspect-shape
PluginCapabilityKind did not include "document-extractors", causing
plugins that declare contracts.documentExtractors (like document-extract)
to show capabilityCount=0 and shape="non-capability" in plugins inspect.
Add "document-extractors" to PluginCapabilityKind and read from
plugin.contracts.documentExtractors in buildPluginCapabilityEntries().
Related to #91539
* [AI] test(plugins): add document-extractors shape contract coverage
Add a test case verifying that plugins declaring
contracts.documentExtractors are classified as
plain-capability shape with capabilityCount=1
and capabilities including the document-extractors kind.
Addresses ClawSweeper P2 review finding on PR #91597.
* [AI] chore: rebase on main to refresh CI
* test(plugins): fold extractor into shape matrix
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* fix(cron): propagate cleanupCliLiveSessionOnRunEnd to isolated cron CLI branch
* test(cron): add CLI interim retry coverage for isolated cron cleanup flag
Verify cleanupCliLiveSessionOnRunEnd is passed on both the initial and
retry CLI runs during isolated cron interim-ack retry loops. Proves the
inner boundary is safe: each runCliAgent call creates a fresh context,
so cleanup cannot affect the retry's live session.
* fix(cron): remove unused variable in interim retry test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cron): trim redundant cleanup retry coverage
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
Summary:
- The PR imports `truncateUtf16Safe` and uses it for the Codex usage-limit preview and verbose working-label truncation paths in auto-reply.
- PR surface: Source +2. Total +2 across 2 files.
- Reproducibility: yes. Current main uses raw `slice(0, N)` at both reported user-facing truncation sites, and ... ludes terminal before/after output showing dangling surrogates before the fix and safe truncation after it.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head 74a0a32ed9.
- Required merge gates passed before the squash merge.
Prepared head SHA: 74a0a32ed9
Review: https://github.com/openclaw/openclaw/pull/97299#issuecomment-4820038635
Co-authored-by: zenglingbiao <zeng.lingbiao@xydigit.com>
Approved-by: takhoffman
* fix(model-fallback): don't rethrow provider-side AbortErrors as user cancellations
When the LLM API closes the connection mid-stream, the fetch layer
surfaces AbortError("This operation was aborted") with no external
abort signal triggered. The old guard `shouldRethrowAbort()` returned
false for these errors (because isTimeoutError matched the message),
so they fell through to the fallback loop but were never retried —
the error propagated up and produced SILENT_REPLY_TOKEN in group
sessions, permanently silencing the topic.
Replace the guard with a direct check: only rethrow AbortError when
the external abort signal is actually set (user/gateway cancellation).
Provider-side AbortErrors without an external signal now fall through
to the next fallback candidate, giving the system a chance to recover.
* fix(cron): forward abort signal into runWithModelFallback
Thread the cron executor's abort signal into the shared
runWithModelFallback call so that cron timeouts and cancellations
stop the fallback chain instead of retrying with the next candidate.
Previously, the run callback checked params.abortSignal?.aborted and
threw, but runWithModelFallback itself had no signal — so the new
guard in model-fallback.ts could not distinguish a caller abort from
a provider-side AbortError and would retry silently.
Also adds a focused regression test verifying the signal is forwarded.
---------
Co-authored-by: Shengting Xie <shengting@openclaw.ai>
Co-authored-by: yayu <yayu@yayuMacStudio.local>
decodeHtmlEntities decoded numeric entities with String.fromCodePoint(parseInt(...)) without a range check, so an out-of-range entity such as � or � threw RangeError and made the whole results page fail to parse. Validate the code point is within 0..0x10FFFF and keep the original entity text otherwise. Add a regression covering decimal and hex out-of-range entities plus a valid astral entity.
reactMessageTelegram and deleteMessageTelegram passed context: "send" to
isRecoverableTelegramNetworkError, which disables message-snippet matching
(allowMessageMatch defaults to false only for "send"). Both operations are
idempotent (setMessageReaction / deleteMessage are safe to repeat), yet a
transient snippet-only network error (e.g. "socket hang up", "undici network
error" with no error code) was not retried — stricter than polling/webhook/
unknown, which all default allowMessageMatch to true. Users saw spurious
reaction/delete failures on transient network errors.
Add delete | react to TelegramNetworkErrorContext (additive) and use them at
the two callers. The helper default (context !== "send") is unchanged, so
delete/react now match polling/webhook/unknown. sendMessage keeps "send".
Co-authored-by: Claude <noreply@anthropic.com>
sanitizeInput truncated long messages with String.slice(0, 4000), which
can cut through an astral character's surrogate pair (e.g. an emoji at
the 4000-char boundary), leaving a lone surrogate in the sanitized text
passed downstream.
Use truncateUtf16Safe so truncation never splits a surrogate pair,
keeping the existing 4000-char budget and '... [truncated]' suffix.
Adds tests asserting the truncated output stays UTF-16 well formed and
that a supplementary-plane character is preserved when it fits.
truncateText sliced the approval card text paragraph with String.slice,
which can cut through an astral character's surrogate pair (e.g. an emoji
straddling the 1797-char limit), leaving a lone surrogate in the card
text sent to Google Chat.
Use truncateUtf16Safe from the plugin SDK so truncation never splits a
surrogate pair, keeping the '...' suffix and the existing length budget.
Adds tests asserting the truncated Command card text stays UTF-16 well
formed and that an astral character is preserved when it fits.
truncateSlackMrkdwn cut approval Block Kit mrkdwn (Command/Request/
plugin description) with String.slice(0, maxChars - 1), which can split
an astral character's surrogate pair at the 2600-char preview limit,
leaving a lone surrogate in the chat.postMessage/chat.update payload.
Slice with sliceUtf16Safe so truncation never splits a surrogate pair,
keeping the existing ellipsis suffix and length budget.
Adds tests asserting exec command and plugin request mrkdwn stay free of
lone surrogates, plus a BMP regression keeping the existing limit.
elide truncated text with String.slice(0, limit) on a UTF-16 code-unit
index, so an astral character straddling the limit was cut into a lone
surrogate; the truncated-char count was also computed from the fixed
limit rather than the actual kept length.
Truncate with truncateUtf16Safe so a surrogate pair is never split, and
derive the truncated-char count from the kept length so the annotation
stays accurate.
Adds tests asserting no lone surrogate when the limit lands inside an
emoji and that a complete astral character is kept when it fits.