* fix(tlon): truncate approval message preview on UTF-16 boundary
formatApprovalRequest called truncate() with raw .slice() before the
trailing ellipsis. A Tlon ship display name or message preview whose
emoji landed on the 100-character preview cap was split mid-surrogate
pair, emitting a lone surrogate in the owner notification. Use
sliceUtf16Safe so the truncation always falls on a code-point boundary.
* fix(tlon): guard all three messagePreview UTF-16 truncation sites
The prior commit only guarded the formatter (truncate in approval.ts).
The root-cause sites are in monitor/index.ts where PendingApproval is
constructed:
messagePreview: rawText.slice(0, 100), // channel mention path
messagePreview: messageText.slice(0, 100), // DM path
An emoji whose high surrogate falls at position 99 was sliced mid-pair,
storing a lone \uD83D in the persistent PendingApproval before the
formatter ever ran.
Fix: replace both production sites with sliceUtf16Safe(), and also
guard the createPendingApproval() constructor itself so any future
caller that passes an untruncated preview is automatically safe.
Add regression tests that drive the full
createPendingApproval -> messagePreview -> formatApprovalRequest chain
for both the DM and channel paths with an emoji placed exactly at the
100-unit boundary.
* fix(tlon): truncate approval message preview on UTF-16 boundary
* fix(clownfish): repair validation for live-pr-inventory-20260629T032026-001 (2)
---------
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Remove model_not_found from the shouldEscalateRetryLimit exclusion list
so that configured fallback models are attempted when the primary model
is decommissioned by the provider. Previously, model_not_found was
treated as terminal, silently defeating the fallback chain for the
decommission case.
Fixes#97564
* fix(plugins): plugin loggers drop writes after the log level is raised at runtime
The plugin runtime logging facade captured a single tslog child logger per
getChildLogger() call. tslog snapshots a sublogger's min level at creation, so a
long-lived plugin logger (e.g. a channel monitor that runs for the whole gateway
session) kept dropping debug/verbose writes after the log level was raised at
runtime, even though shouldLogVerbose()/isFileLogLevelEnabled() reported the new
level. This made channels like Mattermost go dark while core subsystem loggers
(which re-resolve per emit) kept logging.
Resolve the child logger per call so it always reflects the current level, with a
cheap isFileLogLevelEnabled pre-gate (skipped for explicit overrides) to avoid
building a sublogger when the level is disabled. Fixes every channel that holds a
long-lived monitor logger (mattermost, matrix, msteams, irc, nextcloud-talk) at
the facade boundary with no plugin-code changes.
* test(plugins): type runtime log-level mock
---------
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Exclude models.providers.*.apiKey values that match isNonSecretApiKeyMarker
(e.g., lmstudio-local, ollama-local) from secrets audit plaintext warnings.
Regression test covers marker bypass and real-key flagging.
Closes#89233
* fix(models): don't advise models.providers[] registration for runtime-bound models
When an agent's configured model is bound to an agent runtime (e.g. the
"codex" runtime, whose catalog comes from the OpenAI ChatGPT-account
app-server), and that model id is no longer offered by the runtime, model
resolution fails with "Unknown model: <provider>/<model>". The appended hint
told users to register the model in models.providers[].models[].
For runtime-owned models that advice is misleading: adding the registration
makes resolution "succeed" only for the request to be rejected later by the
provider — e.g. OpenAI returns 400 "model is not supported when using Codex
with a ChatGPT account" once a model id such as gpt-5.3-codex is deprecated.
Detect the agentRuntime binding on the configured agents.defaults.models entry
and instead point the user at the runtime's live catalog (openclaw models list
--provider <id>) and at switching the configured default to an available model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reply): show a clear "model unavailable" reply instead of generic failure
When a configured model is retired/renamed by the provider, model resolution
fails (run.ts throws a typed FailoverError with reason "model_not_found") and
the agent runner falls back to the generic "Something went wrong … use /new"
reply. That copy is actively misleading for this failure: retrying or starting
a new session can never help, because the model id itself must be changed in
config. Users on a deprecated model (e.g. gpt-5.3-codex on a Codex ChatGPT
account) just see the generic message on every message and on /new.
Classify this failure into a dedicated user-facing reply that explains the
model is unavailable and points at the config. Detection is structural — it
keys off the typed FailoverError reason, not provider error text — so it stays
robust as provider wording changes; free-text rejections without a typed reason
remain the responsibility of the failover layer that owns error classification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When Telegram rejects a rich message with RICH_MESSAGE_*_INVALID
(e.g., RICH_MESSAGE_EMAIL_INVALID on /status replies), the delivery
path now falls back to a plain text sendMessage instead of dropping
the reply.
Fixes#96363
The renderNativeTalkSelect function was rendering an extra .agent-chat__talk-select-label
span element for Voice and Sensitivity fields, causing misalignment with the Model field.
Changes:
- Remove selectedLabel parameter from renderNativeTalkSelect function signature
- Remove the conditional rendering of .agent-chat__talk-select-label span
- Clean up the selectedLabel argument in renderRealtimeTalkOptions
This ensures all three fields (Voice, Model, Sensitivity) have identical HTML structure
(label span + input/select), resulting in perfect visual alignment.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a Feishu channel has top-level appId/appSecret using SecretRef
format and sub-accounts with their own inline appSecret, the secrets
resolver marks the top-level SecretRef as inactive because
isBaseFieldActiveForChannelSurface only checks whether any explicit
account inherits the field.
Feishu account listing always creates an implicit default account from
top-level credentials. The fix detects this implicit default account
and keeps the top-level appSecret active accordingly, without changing
the shared channel secret helper semantics for other channels.
Fixes#96929
Signed-off-by: 赵旺0668001248 <0668001248@duomai.com>
Accepted sessions_spawn launches can be persisted as a toolResult with isError:true while the payload carries status=accepted. The compaction safeguard collected every isError toolResult, so normal accepted subagent launches surfaced as repeated '## Tool Failures' lines in user-visible summaries. collectToolFailures now skips results whose toolName is sessions_spawn and whose payload normalizes as an accepted child spawn, mirroring the observer's existing detection; this covers legacy transcripts too. Genuinely failed spawns (status=error/forbidden) and non-spawn tools are still reported.
isCodexToolResultError fail-closes every tool-result status not in its
non-error allowlist, but the allowlist omitted several success statuses
emitted by OpenClaw tools that are exposed to codex agents:
- sessions_spawn accepted launches -> details.status "accepted"
- create_goal / update_goal results -> details.status "created" / "updated"
So a successful accepted spawn (#96833), and successful goal create/update,
were classified as errors: reported to codex as success: false (mapped to a
Failed item status) and persisted on the transcript as isError: true. This
adds those statuses to the allowlist alongside their sibling success statuses
(completed/recorded/started/running). Genuinely failed or forbidden results
(status "error"/"forbidden") stay fail-closed.
Adds regression tests: accepted spawn and created/updated goal results are
reported as successful dynamic tool calls; a forbidden spawn still fails.
Add unit tests for createNonExitingRuntime and writeRuntimeJson
functions in src/runtime.ts to verify runtime behavior.
Tests cover:
- createNonExitingRuntime returns runtime with exit function
- exit function throws error with code
- writeRuntimeJson writes JSON using writeJson when available
- writeRuntimeJson writes JSON using log when writeJson not available
- uses custom space parameter
- handles zero space parameter
* fix(llm): coerce stringified JSON arrays/objects in tool argument validation
When LLMs serialize array or object tool parameters as JSON strings
(e.g. tags: '["test","debug"]' instead of tags: ["test","debug"]),
validateToolArguments now attempts JSON.parse coercion before
rejecting the value. This mirrors the existing numeric string
coercion path and fixes MCP tool calls from providers like MiMo,
Ollama, and others that stringify complex parameters.
Fixes#96916
* fix(llm): bound JSON.parse size for schema-gated array/object coercion
Add MAX_JSON_COERCE_LENGTH (64KB) guard before JSON.parse in the array
and object coercion branches. Oversized stringified arguments are left
for normal validation to reject rather than synchronously parsed.
Addresses Codex review finding: unbounded JSON.parse on model-controlled
tool arguments could block the event loop or spike memory.
vsearch/search are documented as vector/lexical-only modes, but
runQmdSearchViaMcporter omitted `rerank` from the query tool callArgs,
so QMD's query tool (which defaults rerank:true) ran the LLM reranker
anyway. Pass rerank:false for those modes; full "query" mode keeps it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A drained extraction batch is spliced off the queue before the extractor
runs. On a non-terminal failure the batch was never restored, so those
items were silently lost and never retried. Restore the batch to the
front of the queue (original order) on non-terminal errors and rethrow so
the caller still logs; terminal model/auth errors keep the existing
cooldown drop/stop behavior. Also ensure the single-slot debounce
schedules a drain from the overflow branch, so a queue left full by a
restored batch still gets retried instead of being stuck.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>