Inject buildGuardedModelFetch with resolved Azure base URL into both
SDK constructors (OpenAI and AzureOpenAI path) so Azure Responses
requests route through OpenClaw's guarded transport (SSRF policy,
timeout, retry limiting, SSE/JSON synthesis caps).
The non-OK response body cap is applied lazily inside the shared
sanitizeOpenAISdkSseResponse guard via TransformStream — closes the
unbounded response.text() OOM path for hostile 4xx/5xx responses
while preserving the OpenAI SDK's ability to cancel and retry.
- src/llm/providers/azure-openai-responses.ts: pass guardedFetch into
both OpenAI and AzureOpenAI SDK constructors (no per-provider wrapper)
- src/agents/provider-transport-fetch.ts: add capNonOkResponseBodyLazily
helper and wire into sanitizeOpenAISdkSseResponse for !response.ok
- src/agents/provider-transport-fetch.test.ts: 2 inline tests
(cap fires + SDK cancel preserves source)
Co-authored-by: Claude <noreply@anthropic.com>
Wrap the signal outbound sanitizeText hook with sanitizeAssistantVisibleText so assistant internal tool-trace scaffolding is stripped before delivery, matching the sibling channel fixes under #90684 (Telegram #95774, Google Chat #95084, IRC #97214).
Wrap the slack outbound sanitizeText hook with sanitizeAssistantVisibleText so assistant internal tool-trace scaffolding is stripped before delivery, matching the sibling channel fixes under #90684 (Telegram #95774, Google Chat #95084, IRC #97214).
truncateSummary used clean.slice(0, max - 3), which can cut between the
two UTF-16 halves of a surrogate pair (emoji / astral char) straddling
the limit. The serialized card summary then carries a lone high
surrogate that Feishu renders as the replacement char.
Slice with the surrogate-safe sliceUtf16Safe helper instead, matching
the pattern already used in extensions/slack/src/truncate.ts, so a
straddling code point is dropped whole.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
splitTableCells/splitPartialTableCells split on every '|', including a GFM
backslash-escaped pipe ('\|'), which is literal cell content rather than a
column delimiter. A cell containing '\|' was therefore mis-counted as multiple
columns, so the oversized-row fallback (renderTableRowAsFields) rendered the
trailing content under the wrong header.
Split via an escape-aware scan that treats '\|' as a literal '|' (and '\\' as
a literal backslash so a following '|' still delimits). Behavior is byte-for-byte
unchanged for any row without an escaped pipe, so existing chunking is preserved.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16
createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: retry OpenGrep scan (HTTP 502 infra flake)
* test(line): cover grapheme-safe template fields
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Wrap the matrix outbound sanitizeText hook with sanitizeAssistantVisibleText so assistant internal tool-trace scaffolding is stripped before delivery, matching the sibling channel fixes under #90684 (Telegram #95774, Google Chat #95084, IRC #97214).
Align inner compaction retry and compaction-safeguard provider fallback
with the #90908 signal.aborted pattern so undici disconnect AbortErrors
retry or fall back to LLM instead of producing degraded placeholder
summaries. Caller abort remains terminal.
Co-authored-by: NIO <nocodet@mail.com>
Replace bare `await response.json()` in `installSignalCliFromRelease` with
`readProviderJsonResponse` (16 MiB cap, stream cancel on overflow). The
external GitHub Releases endpoint can include a large `body` changelog field;
the error path was already guarded but the success path was unbounded.
The existing inner catch continues to convert overflow errors into the
graceful `{ ok: false, error: "Failed to parse signal-cli release info." }` path.
Adds a regression test verifying the stream is cancelled before all chunks are
read on an oversized 20 MiB streaming response.
Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
When doctor --fix runs legacy OAuth profile id repair, it picks the
single non-legacy OAuth profile as the migration target. If that target
profile already exists as a separate user-configured account, the repair
unconditionally overwrites it, destroying the account's config
(displayName, email) and collapsing two distinct accounts into one.
Add a guard: if the destination profile id already exists in
cfg.auth.profiles, skip the repair entirely to preserve both accounts.
Also pass cfg to resolveAuthProfileMetadata so config-set displayName
is preserved when the repair does proceed.
Fixes#97522
Use surrogate-safe truncation for LINE action labels/data, including card-command, markdown link, quick-reply, and media-control action surfaces, with focused regression coverage.
* fix(telegram): tell models to use <tg-math> for rich-text formulas
The Telegram rich-text capability hint advertised 'formulas' without
specifying syntax. Models (especially fallback models) default to
$...$ or \(...\) LaTeX delimiters, which Telegram Bot API 10.1 rich
messages do not render as math, so formulas appear as literal text.
Telegram Bot API 10.1 renders formulas only via <tg-math> (inline) and
<tg-math-block> (block) tags with raw LaTeX inside. Spell that out in the
hint, matching the existing HTML-tag style used for <sup>/<mark>/<details>.
* test(telegram): assert math rich-text prompt hint
---------
Co-authored-by: wangwllu <wangwllu@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Summary:
- The PR replaces Mattermost draft preview raw UTF-16 slicing with the existing SDK `sliceUtf16Safe` helper and adds a regression test for an emoji that straddles the preview limit.
- PR surface: Source +1, Tests +26. Total +27 across 2 files.
- Reproducibility: yes. Source inspection shows current main raw-slices at `maxChars - 3`; driving `createMatt ... at UTF-16 indices 8-9 reaches the lone-surrogate path, though I did not run tests in this read-only sweep.
Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(mattermost): truncate draft previews on code-point boundaries
Validation:
- ClawSweeper review passed for head 86d0dd2a06.
- Required merge gates passed before the squash merge.
Prepared head SHA: 86d0dd2a06
Review: https://github.com/openclaw/openclaw/pull/97472#issuecomment-4825788514
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Pass the requested agent into tools.effective global-session loading while preserving non-global session ownership guards, with handler and gateway regression coverage.
Route OpenRouter video submit and poll success JSON through the shared bounded provider JSON reader, preserving malformed-response mapping and SSRF request policy coverage.
sanitizeForConsole filtered control characters code-point-aware but then
truncated with sanitized.slice(0, maxChars), which cuts on UTF-16 code units.
When the cap landed between the two code units of an astral character (emoji,
CJK extension, etc.) the output ended in a lone high surrogate before the
ellipsis. Cap on code points instead.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
truncateReplyBody used a raw value.slice(0, MAX_REPLY_BODY_LENGTH - 3)
to shorten long reply bodies. When the cut index fell between the two
UTF-16 code units of a surrogate pair (e.g. an emoji), the slice left a
lone high surrogate before the ellipsis, which renders as a broken glyph
in the reply context shown to the agent.
Replace the raw slice with sliceUtf16Safe from the plugin SDK so the
truncation never cuts inside a surrogate pair. A normal (non-astral)
body is unaffected.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(shared): truncate assistant error text on UTF-16 boundary
Use truncateUtf16Safe from the existing utf16-slice utility instead of
String.prototype.slice(0, 600), which can split a surrogate pair and
produce malformed Unicode when the error message contains characters
outside the Basic Multilingual Plane (emoji, rare CJK, etc.).
* test(shared): add regression test for UTF-16 safe error truncation
Verify formatRawAssistantErrorForUi does not produce dangling surrogates
when truncating a fallback raw error message with a non-BMP character
(emoji) straddling the 600-code-unit boundary.
* fix(agents): truncate assistant error text fallback on UTF-16 boundary
Use truncateUtf16Safe instead of String.prototype.slice(0,600) in
formatAssistantErrorText and its companion comparison in
isRawAssistantErrorPassthrough, preventing dangling surrogates when
non-BMP characters straddle the 600-code-unit truncation boundary.
* fix(models): mark local Ollama rows available
* fix(models): mark local Ollama rows available
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(irc): prevent ghost nick collisions on rejoin after network delay
* test(irc): add regression tests for fallback nick uniqueness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: wendy-chsy <wan.wenyan@xydigit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
serializeJsonlLine returned JSON.stringify(entry) without guarding the
undefined return case. When the root value is undefined, a function, or a
symbol, JSON.stringify returns undefined, which serializeJsonlEntry's template
literal coerced to the literal string "undefined" and wrote to disk. That is
not valid JSON, so parseJsonlEntries silently skipped the line — a fail-silent
loss of a transcript entry with no error log.
Throw a TypeError instead so the caller fixes the bad value before it reaches
the file. Circular references and BigInt already throw via JSON.stringify and
are unaffected. Null, primitives, and plain objects are unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Gates the install hint on the official external provider catalog's
`mediaUnderstandingProviders` contract so only true media-provider
packages (e.g. groq) emit the actionable hint; channel-only ids (e.g.
feishu) and providers without a media-understanding contract (e.g.
amazon-bedrock) fall back to the legacy bare error message verbatim.
Also tightens the gateway recovery wording from generic 'restart the
gateway' to 'stop and start the gateway service' per the canonical
issue reporter's actual recovery flow (full systemd stop/start, not
hot-reload).
Co-authored-by: wangmiao0668000666 <wangmiao0668000666@users.noreply.github.com>
* fix(ui): roll formatTokens over to "M" instead of rendering "1000k"
formatTokens picks the kilo branch on the raw value (< 1_000_000), then renders
Math.round(tokens/1000). For 999_500..999_999 that rounds to 1000, producing the
nonsensical "1000k" instead of rolling over to the M branch. Re-check the rounded
kilo value and fall through to "M" when it reaches 1000, matching the sibling
formatCompactTokenCount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: retry — server-startup-session-migration timeout is main-branch flake, unrelated to formatTokens
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(status): distinguish runtime-loaded plugins from installed inventory
/status plugins merged disk-scan plugin records with the active runtime
registry, so the detailed Loaded: list could include plugins that were
installed/config-enabled but never loaded at runtime. Record the runtime-
loaded plugin ids on the health snapshot, carry them through the merge, and
make Loaded: reflect runtime-confirmed plugins; show installed/discovered
plugins that are not active as a neutral "Installed (not active)" inventory
line rather than an error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(status): include pinned runtime registries in loaded ids
Build runtimeLoadedPluginIds from all live runtime registry surfaces via
collectLivePluginRegistries() (active plus any pinned channel / http-route /
session-extension registry) and render Loaded: from that id set directly, so a
plugin live only through a pinned surface still counts as loaded instead of
being dropped or misreported as "Installed (not active)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>