* fix(agent-tools): resolve workspace-scoped tool fs root lazily
Workspace-scoped edit/write tools resolved their fs-safe root eagerly at
construction. Doctor's active-tool schema projection builds the full coding
toolset just to read tool schemas; when an agent workspace dir is absent at
that time (e.g. an unresolved ${ENV} placeholder in the authored config the
legacy-migration path operates on), the eager fsRoot orphaned a rejecting
promise as 'Unhandled promise rejection: FsSafeError: root dir not found'.
Resolve the root lazily and memoized so construction never opens an fs handle;
the root is only opened on the first real read/write/access operation.
* fix(agent-tools): defer fs-safe root start until workspace write validation succeeds
A workspace-only write/edit against an absent root started fsRoot(root) eagerly
(passed as getRoot() into writeWorkspaceFile) before toCanonicalRelativeWorkspacePath
ran. When validation failed (realpath on the missing root), the rejecting root promise
was left unawaited and surfaced as an unhandled rejection — the readFile/access paths
already defer getRoot() the same way.
writeWorkspaceFile now takes a getRoot thunk and calls it only after validation; the
write and edit-write callers pass the thunk instead of a started promise. Adds a
regression that a missing-root write/edit rejects without starting the fs-safe root or
emitting an unhandled rejection.
---------
Co-authored-by: Sasan <sasan.sotoodehfar@gmail.com>
Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
truncateLine could cut a surrogate pair when the maxChars boundary
fell between a high surrogate and its paired low surrogate, producing
a broken unpaired surrogate in grep tool output.
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>