Keep byte-over-cap visible suffixes at their streamed content indexes when terminal message snapshots are normalized.
Co-authored-by: ZOOWH <ZOOWH@users.noreply.github.com>
In messages.visibleReplies "message_tool" sessions, a successful agent turn that produced a substantive private final without calling message(action=send) previously left the user with silence and only an operator log. The gateway now enqueues one protected front-of-queue retry prompting the model to deliver the reply, and falls back to a sanitized visible diagnostic when the retry cannot be enqueued or also strands. Queue overflow protection is unified with the in-flight-aware drop policy (skip in-flight or protected items, reject when nothing is droppable), rejected overflow no longer refreshes the drain debounce, heartbeat turns are excluded from recovery, and recovery retries no longer share the client turn's queued-turn lifecycle.
Fixes#85714
Thanks to Eva (@100yenadmin) for the contribution.
* feat(channels): narrated progress drafts + activity receipt on the final answer
Progress mode replaces raw tool lines with short utility-model narration of
what the agent is doing (streaming.progress.narration, default on, requires
an explicit utilityModel). On Discord the final answer now carries the
-# activity receipt and the working draft is deleted once the answer lands,
so busy channels keep no orphaned tool log above the reply.
Formatting verified remotely via Testbox oxfmt --check (hook bypassed:
no node_modules in this worktree).
* fix(channels): keep narration toggle independent of channel-default stream mode
Discord resolves its own progress default, so the resolver must not re-derive
mode with the generic partial fallback (narration was off for unset config).
* fix(auto-reply): honor status-only command text in narration model input
streaming.progress.commandText: "status" hides raw exec/bash text from the
channel draft; narration input now mirrors that policy so the utility model
never receives more command detail than the draft shows (Codex review P2).
Formatting verified remotely via Testbox oxfmt --check.
* fix(auto-reply): share the draft's command-tool set for narration and regen channel metadata
Reuse isCommandToolName (exec|shell|bash) so narration's commandText policy
matches the draft formatter exactly, and regenerate bundled channel config
metadata for the new streaming.progress.narration key (Codex review round 2).
Gates verified on Testbox: config:channels:check, oxfmt --check, 4 test shards.
* fix(channels): clear stale narration when the narrator stops mid-turn
An empty narration update now falls the draft back to raw tool lines, and the
narrator emits that clear when it disables after consecutive failures or the
per-turn cap, so drafts never pin stale status text (Codex review round 3).
Gates verified on Testbox: oxfmt --check + 4 test shards.
* chore(config): regen bundled channel metadata after rebase onto main
* chore: CI fixups — lint nits, test harness types, docs map, SDK surface budget
Two deliberate public SDK additions (resolveChannelStreamingProgressNarration,
isCommandToolName via the streaming wildcard re-export) bump the pinned
public-surface budgets to current counts (exports 10488, callable 5235).
Gates verified on Testbox: oxfmt, targeted oxlint, docs:map:check,
config:channels:check, check:test-types, 5 test shards.
* feat(cli,webchat): lobster wild cards - a CLI cousin, moving day, and opt-in sounds
The openclaw banner gains a rare day-seeded ASCII lobster (rich TTYs,
random tagline mode, never CI). The web pet notices gateway upgrades and
arrives carrying a bindle for one load. A new default-off Lobster sounds
quick setting adds tiny WebAudio chirps on pokes and pets. The lobster
docs page learns about petting, sounds, and this batch's field notes.
* chore(webchat): regenerate keyless raw-copy i18n baseline after rebase
* fix(agents): keep model fallback turn-local instead of persisting over user pins
* fix(telegram): use live config snapshots per operation
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* test(telegram): fix config snapshot type coverage
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(agents): recover xAI/Grok "could not decrypt encrypted_content" 400 instead of tripping the circuit breaker
openclaw already strips a stale reasoning replay and retries the Responses call, but the
recovery is gated on isInvalidEncryptedContentError(), which only recognizes the
`invalid_encrypted_content` / `thinking_signature_invalid` codes/messages. xAI/Grok returns
a prose 400 with no error code — "Could not decrypt the provided encrypted_content. Ensure
the value is the unmodified encrypted_content from a previous response." — so the matcher
returns false, the call fails, and the per-model circuit breaker trips, blocking ALL
grok-4.3 traffic through the gateway until manual intervention.
Match that message (contains `encrypted_content` and a decrypt-failure phrase) so the
existing strip-and-retry path handles it too. Narrow enough to avoid unrelated
"could not decrypt" messages (e.g. the OAuth sidecar warning), which do not mention
`encrypted_content`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): narrow xAI decrypt retry detection
Co-authored-by: rvdlaar <rvdlaar@gmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): re-check session runtime model against current agent defaults after hot-reload
Agent model hot-reload silently did not take effect because session entries
cached modelProvider/model from agent defaults during reset, and the resolver
returned these cached values before checking current config.
Fix (three-pronged):
1. Reset-side: only cache modelProvider/model when the resolved model came
from a user override — default-derived values are no longer persisted.
2. Resolver-side: when runtime metadata exists without overrides and an
agentId is available, the values are default-derived and may be stale —
skip them in the persisted-model fallback so current config defaults win.
3. Inheritance: only inherit runtime model metadata from parent when it
carries explicit user overrides (align with reset-side contract).
Reset response includes resolvedModel so API and TUI consumers always get
the effective model identity.
Fixes#102269
* fix(gateway): restore truncateUtf16Safe and emoji-boundary title test per ClawSweeper review
* fix(gateway): only skip stale session runtime model metadata when it actually differs from current defaults
The previous change unconditionally skipped cached modelProvider/model when no
user overrides were present and an agentId was available, assuming it was always
stale. This broke sessions that legitimately had non-default models set through
normal session creation (e.g. custom vision models).
Now the resolver resolves the current agent default first and compares: if the
cached runtime metadata matches the current default it is returned directly
(not stale); only when it differs is it treated as stale and re-resolved.
Also updates tests that set modelProvider/model without overrides to configure
their agent defaults so the expected model matches the resolution result.
* fix(session-model-ref): add stale-metadata detection for config hot-reload
* fix(test): remove strict timeoutMs assertion in provider catalog live-runtime test
The remainingTimeoutMs calculation can be off by 1ms depending on timing
(Date.now() - startedAt = 1ms on fast CI runners), causing a flaky failure.
This assertion is not the test's focus — dedicated timeout behavior is already
covered by 'uses one timeout budget across paginated live catalog discovery'.
* fix: restore AVATAR_MAX_BYTES to 2MB (revert accidental merge contamination)
* fix(gateway): resolve session models from current config
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mcp): redact secrets in /mcp show replies
Prevent owner /mcp show from dumping raw headers/env credentials into
group chats. Mark MCP env values sensitive, restore redaction sentinels
on set so show→set cannot overwrite real secrets.
Fixes#103053
* test(mcp): e2e redaction of /mcp show secrets on live gateway
Spin up an ephemeral gateway process, chat.send /mcp show with live
credential-bearing config, and prove secrets never leave the reply path.
Also prove show→set sentinel restore on disk via chat and CLI.
* fix(queue): prevent applyQueueDropPolicy from selecting in-flight items as overflow victims
When a burst of inbound messages hits the followup queue cap while the
head item is mid-delivery, applyQueueDropPolicy can select that same
in-flight item as an overflow victim. With the default
dropPolicy: "summarize", the in-flight item ends up recorded in the
overflow summary as dropped even though it is still being delivered,
producing a contradictory record where the same message is both
answered and reported as unanswered-due-to-overflow.
The fix introduces an optional `inFlight` Set parameter to
applyQueueDropPolicy and drainNextQueueItem. The followup queue state
now owns a shared inFlight set that is:
- Populated by drainNextQueueItem during the await run(next) window
- Populated by the collect-merge drain path for activeGroupItems
- Passed to applyQueueDropPolicy in enqueueFollowupRun
The drop policy now computes an effective queue length that excludes
in-flight items, and skips them when selecting splice victims.
Fixes#103246
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(queue): align pending depth with active deliveries
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(opencode-go): remove deprecated mimo-v2-omni and mimo-v2-pro model aliases
These deprecated aliases reject agent requests from the OpenCode Go gateway.
Remove them from the provider catalog and clean up all references in probe
skip lists, CI workflows, and tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(opencode-go): complete deprecated MiMo cleanup
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(config): use shared dedupe cache for legacy toolsBySender warnings
Replace unbounded Set with createDedupeCache({ ttlMs: 0, maxSize: 4096 })
from src/infra/dedupe.ts. The shared helper provides check() with
touch-on-read and max-size pruning — no custom LRU stack needed.
Tests verify eviction and hot-key retention through the public
resolveToolsBySender API without a testing-only cache export.
* test(config): tighten legacy warning cache proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): use Buffer.byteLength for bash output rolling buffer accounting
The outputBytes variable tracks the rolling output buffer size for
bash command execution, but it used string .length (UTF-16 code units)
instead of Buffer.byteLength (UTF-8 bytes). When command output
contains multi-byte UTF-8 characters (emoji, CJK, etc.), the .length
undercount causes the rolling buffer to exceed maxOutputBytes.
Replace .length with Buffer.byteLength() at both increment and
decrement sites to correctly track byte-level buffer size.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(agents): unify bash output accumulation
* refactor(agents): unify bash output accumulation
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
renderTerminalBufferText filters C0 control characters (0x00-0x1f)
and DEL (0x7f) but did not filter C1 controls (0x80-0x9f). The C1
range includes the CSI (Control Sequence Introducer) at 0x9b, an
alternative ANSI escape prefix that stripAnsiSequences() may miss.
Add the C1 range to CONTROL_BYTES_REGEX to strip all C1 bytes.
Same pattern as the C1 fix in sanitizeForConsole (#103226).
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* fix(cli): bound node camera URL downloads
* refactor(cli): enforce camera download policy at guard
* refactor(cli): enforce camera download policy at guard
* refactor(cli): enforce camera download policy at guard
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* feat(mattermost): add opt-in DM threading via dmReplyToMode
Resolves#93203.
Mattermost direct messages are hardcoded non-threaded: resolveMattermostReplyToMode
returns "off" for direct chats before consulting config, and the reply-root /
effective-reply resolvers short-circuit for direct chats. So every DM accretes into
one ever-growing session with unbounded per-session token usage, with no way to
isolate each DM topic the way groups/channels (and Slack) can.
Add channels.mattermost.dmReplyToMode (same enum as replyToMode), default "off" so
existing flat-DM deployments are unchanged. When set to "first"/"all", a DM @mention
starts an independent, thread-scoped session:
- resolveMattermostReplyToMode: direct chats return config.dmReplyToMode ?? "off".
- resolveMattermostEffectiveReplyToId: direct early-return gated on replyToMode "off"
(the value already reflects dmReplyToMode for direct chats).
- resolveMattermostReplyRootId: direct early-return gated on the absence of a thread
root (a DM carries one only when DM threading is enabled).
- New DM threads start fresh (no parent-session inheritance) so each topic is isolated;
a threaded DM keeps its own history key while a flat DM root keeps none.
Default-off preserves the documented flat-DM contract, so this changes no existing
behavior. Adds unit coverage and documents the opt-in. The only open question is the
knob shape (dmReplyToMode enum vs a boolean dmThreading) — happy to adjust.
Refs #65729.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mattermost): register dmReplyToMode in config schema
dmReplyToMode was wired into the account config type and reply
resolution but never added to MattermostConfigSchema, so setting it in
openclaw.json failed config validation ("must not have additional
properties: dmReplyToMode") and the gateway refused to start — the knob
could not actually be enabled. Register it in the schema, regenerate the
bundled channel config metadata and doc baseline, and add a
config-schema test.
* fix(mattermost): thread direct reply transport when dmReplyToMode is on
resolveReplyTransport nulled the direct reply root/thread id
unconditionally, so routed replies and message-tool follow-ups in an
opted-in DM thread were still sent flat even when the effective
replyToMode was first/all. Keep direct transport flat only when the mode
is off (the historical contract) and preserve the thread root otherwise,
matching the monitor-level DM threading gates. Adds channel.test.ts
coverage for resolveReplyTransport.
* fix(mattermost): use per-chat reply mode overrides
* fix(mattermost): bound direct thread history
* chore(config): refresh docs baseline
---------
Co-authored-by: leon <leon@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(openai): preserve native tool calls from clean streams without finish_reason
OpenAI-compatible providers that emit delta.tool_calls during streaming but
terminate with data: [DONE] without a final finish_reason chunk (e.g. Evolink
DeepSeek V4) have their tool calls silently stripped.
Introduce sawNativeToolCallDelta (structured provider intent) and
sawStreamDONE (exact SSE data: [DONE] detection via TransformStream).
Pass sawStreamDONE as a getter so the live value is read after stream
consumption. Promotion requires (sawStopFinishReason || (sawNativeToolCallDelta
&& sawStreamDONE?.())). SSE parsing uses line-boundary-aware regex; [DONE]
inside tool arguments or content does not match.
EOF without [DONE] remains fail-closed. DSML still requires sawStopFinishReason.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes#97994
* [AI] test(openai): add fetch-wrapper loopback tests for [DONE] detection proof
Prove sawStreamDONE works through the full transport chain:
- Local HTTP server → TransformStream → OpenAI SDK → processOpenAICompletionsStream
- [DONE] without finish_reason → promoted to toolUse
- EOF without [DONE] → fail-closed (tool calls stripped)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden clean SSE terminal detection
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>