* fix(minimax): use dedicated image generation endpoint
MiniMax image generation uses a dedicated API endpoint
(api.minimax.io/v1/image_generation) that is separate from the
text/chat API endpoint (api.minimax.io/anthropic).
Previously, the resolveMinimaxImageBaseUrl function would extract
the origin from the provider's configured baseUrl. If a user had
configured their baseUrl to the chat endpoint (e.g.,
api.minimax.chat/anthropic), the image generation would incorrectly
use that endpoint, resulting in "invalid api key" errors.
This fix always uses the dedicated image generation endpoint,
ignoring the provider's baseUrl configuration for image generation.
Fixes#61149
* fix(minimax): support CN endpoint for image generation
Respect MINIMAX_API_HOST environment variable to determine whether
to use the global (api.minimax.io) or CN (api.minimaxi.com) endpoint
for image generation.
This ensures that CN users who configure MINIMAX_API_HOST to use
api.minimaxi.com will continue to use the CN endpoint for image
generation, while global users continue to use api.minimax.io.
The original bug was caused by the code extracting the origin from
the provider's configured baseUrl, which could be set to incorrect
endpoints like api.minimax.chat. This fix uses the dedicated image
generation endpoints instead.
Fixes#61149
* fix(minimax): infer CN endpoint from provider config when env is unset
When MINIMAX_API_HOST is not set, fall back to checking the provider's
configured baseUrl to determine whether to use the CN or global image
endpoint. This ensures CN users who went through onboarding (which sets
models.providers.minimax.baseUrl to https://api.minimaxi.com/anthropic)
are correctly routed to the CN image endpoint.
The isMinimaxCnHost check ensures we only use the baseUrl origin for
CN detection - invalid endpoints like api.minimax.chat would not match
minimaxi.com and would correctly fall through to the global default.
Fixes#61149
* test(minimax): cover dedicated image endpoints
* fix(logging): handle context assembly diagnostics
* Revert "fix(logging): handle context assembly diagnostics"
This reverts commit f51d2f7d67f8193268dd37553ac77e80a0423390.
* test(minimax): isolate image endpoint env
* docs(changelog): credit minimax image fix
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(browser): note tilde expansion also covers per-profile paths
The 95a2c9b fix expanded "~" for both `browser.executablePath` and
per-profile `profiles.<name>.executablePath` (config.ts:382 calls
`normalizeExecutablePath` for profile overrides). Per-profile
`userDataDir` on existing-session profiles is also tilde-expanded
(config.ts:391 via `resolveUserPath`). The configuration reference
only mentioned the top-level `browser.executablePath` case.
* docs(browser): align tilde path config help
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(browser): document local startup timeout bounds
The new browser.localLaunchTimeoutMs and browser.localCdpReadyTimeoutMs
options are clamped to MAX_BROWSER_STARTUP_TIMEOUT_MS (120000 ms) by
normalizeStartupTimeoutMs in extensions/browser/src/browser/config.ts,
and zero/negative/non-finite values fall back to the defaults. Without
this in the configuration reference, users setting a higher value see
no error and silently get the 120 s ceiling, or set 0 expecting 'no
timeout' and silently get the default.
* docs(browser): clarify startup timeout validation
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fix the bundled Bonjour gateway discovery crash-loop caused by ciao probe cancellation rejections after the Bonjour plugin migration.
The plugin entry now wires the existing rejection handler into the advertiser, and the unhandled-rejection handler registry is anchored on globalThis so staged plugin SDK module copies register into the same process-level handler set used by the host.
Verification:
- pnpm test:serial extensions/bonjour/src/advertiser.test.ts src/infra/unhandled-rejections.fatal-detection.test.ts
- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed partially completed: conflict markers plus core/core-test/extensions/extension-test typecheck passed; local lint lane hit a self-lock and was stopped.
Five recent diagnostics-otel feat commits added user-facing OpenTelemetry
surfaces but did not update docs/logging.md, so the listed metrics and
spans drifted out of sync with what the plugin actually exports:
- 7bbd47349e adds gen_ai.client.token.usage histogram (GenAI semconv)
- b8a41739d5 adds memory heap/rss histograms, pressure counter and span
- d6ef1fcf24 adds openclaw.tool.loop counters and span
- ff172f46a5 adds openclaw.context.assembled span
- 44114328b4 adds openclaw.provider.request_id_hash attr on
openclaw.model.call spans
Append the new metrics under existing model-usage and exec sections,
add a 'Diagnostics internals' subsection for memory + tool-loop
metrics, and add the three new spans (context.assembled, tool.loop,
memory.pressure) plus the request-id-hash attribute to the spans
listing.
Fixes context usage display regressions and prevents active runs from being interrupted by channel reloads. Adds persisted tool-result detail bounds so large tool metadata stays out of model/session payloads.
* feat(litellm): add image generation provider
Registers litellm as an image-generation provider so model refs like
litellm/gpt-image-2 route through the LiteLLM proxy, and
agents.defaults.imageGenerationModel.fallbacks entries of the form
litellm/... resolve without "No image-generation provider registered
for litellm" errors.
Implementation uses the OpenAI-compatible /images/generations and
/images/edits endpoints that LiteLLM proxies for. BaseUrl resolves from
models.providers.litellm.baseUrl (default http://localhost:4000). Private
network is auto-allowed when baseUrl is a loopback/RFC1918 address, which
covers the common self-hosted LiteLLM proxy case without needing
OPENCLAW_PROVIDER_ALLOW_PRIVATE_NETWORK. Public baseUrls keep normal SSRF
defaults.
Default model is gpt-image-2 (matching upstream 4.21+ OpenAI default).
Advertises the same 2K/4K sizes OpenAI now exposes, plus legacy
256/512/1024 for dall-e-3. Supports both generate and edit.
Local patch. LiteLLM has no upstream image-generation support yet; revisit
if upstream adds one.
* ci: rerun after upstream main hot-fix
* fix(litellm): harden image generation provider
---------
Co-authored-by: Chris Zhang <chris@ChrisdeMac-mini.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Val Alexander's c65aa1d2a6 (#71639) changed assistant avatar uploads
from gateway config persistence to localStorage, mirroring the existing
user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md
'Personal identity (browser-local)' section only documented the user
identity. Add a paragraph noting the assistant avatar override follows
the same browser-local pattern, while keeping the ui.assistant.avatar
config field reachable for non-UI clients writing the field directly.
* fix(control-ui): rebalance quick settings into stable 3-col bento
Pair Appearance with Automations and let Channels stand alone in the
middle column so all three top-row columns reach similar heights.
Promote Personal to a full-width row with a horizontal body
(identity tiles | emoji + actions) so the avatar block stops fighting
for half-width space. Drops the unused .qs-stack--wide hook.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair
Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities.
Changes:
- Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure)
- Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group
- Remove .qs-personal-body and .qs-personal-form wrapper divs
- Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing
- Responsive collapse to 1-column at ≤760px
Tests:
- config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form)
- config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure
- All 10 quick settings view tests passing
- All 20 schema regression tests passing
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* chore: ignore .vmux worktree paths
* fix(control-ui): persist assistant avatar override locally instead of via gateway config
Mirrors the user-avatar pattern: assistant avatar uploads now go to
localStorage and overlay the gateway-resolved identity at bootstrap and on
agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap
that rejected uploaded data URLs as 'Too big: expected string to have
<=200 characters', removes one config.patch RPC from the avatar path, and
collapses the upload handler from a 44-line async/loadConfig dance into a
plain synchronous setter.
Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to
2,000,000 to match the user-avatar size budget for non-UI clients writing
the field directly, and adds a content-aware text/image normalizer in
ui/src/ui/assistant-identity.ts so short-text avatars stay short while
data URLs survive round-tripping.
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Fix duplicate section title and description rendering in single-section Control UI config pages.\n\nKeeps root multi-section card headers intact, keeps single-section hero copy as the only visible section title, and adds browser coverage for both single-section and root views.\n\nFixes #68003.\n\nThanks @d1rshan.
Vincent's commit ab1d1a5c9e (#71560) added user-facing config keys to
existing-session profiles for the Chrome DevTools MCP launch path:
- browser.profiles.<name>.mcpCommand
- browser.profiles.<name>.mcpArgs
Plus runtime behavior changes:
- cdpUrl http(s) -> --browserUrl, cdpUrl ws(s) -> --wsEndpoint
- endpoint flags and userDataDir are mutually exclusive
The CHANGELOG entry covered the change, but docs/tools/browser.md
existing-session reference did not. Add a 'Custom Chrome MCP launch'
subsection describing the new fields and the cdpUrl endpoint mapping
rules.
Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
Fix Telegram partial-stream preview finalization so ambiguous final edit failures fall back to a final send when the visible preview is a strict prefix of the answer.
Includes archived-preview regression coverage and generated config metadata refresh.
Thanks @sahilsatralkar.
Co-authored-by: Sahil Satralkar <62758655+sahilsatralkar@users.noreply.github.com>
Polishes the basic config identity layout, aligns assistant avatar rendering with chat, and adds a Control UI assistant avatar override with IDENTITY.md fallback.
* fix(memory-host-sdk): use TRUSTED_ENV_PROXY mode in withRemoteHttpResponse
When a HTTP/HTTPS proxy is configured via environment variables
(HTTPS_PROXY, HTTP_PROXY, ALL_PROXY), the withRemoteHttpResponse
function now passes mode=TRUSTED_ENV_PROXY to fetchWithSsrFGuard.
This causes DNS resolution to skip the local resolver and route
through the configured proxy, fixing 'fetch failed' errors for
remote memory embeddings (including GitHub Copilot embeddings) in
proxy environments (e.g. Clash TUN, corporate proxies).
Previously, without an explicit mode, fetchWithSsrFGuard defaulted
to STRICT mode which performs local DNS pre-resolution via
resolvePinnedHostnameWithPolicy(), failing in proxy environments
where DNS must go through the proxy.
Fixes: openclaw/openclaw#52162
* fix: harden memory env proxy guard (#71506) (thanks @DhtIsCoding)
---------
Co-authored-by: Dht <dht@openclaw.ai>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fixes#71494.
- Render Control UI links with https:// when gateway TLS is enabled.
- Render websocket links with wss:// through the shared link resolver.
- Add daemon status handoff coverage and TLS scheme docs.
Co-authored-by: deepkilord <wang_hgang@msn.com>
The empty-body guard only checked baseBodyFinal (current message body)
and softResetTail, ignoring inboundUserContext which includes
InboundHistory from group chat context. This caused the bot to reject
bare @mentions in Feishu group chats where prior messages provided the
conversation context via InboundHistory.
Now hasUserBody also checks whether inboundUserContext has content,
matching the behavior before the 2026.4.12 refactor.
Eduardo Cruz's PWA web push feat (21b7ad5805, #44590) added a substantial
user-facing surface — manifest.webmanifest, sw.js, gateway push.web.*
methods, persisted vapid-keys.json/web-push-subscriptions.json, and
OPENCLAW_VAPID_* env overrides — but did not touch any docs/.
Add a 'PWA install and web push' section to docs/web/control-ui.md
covering the new persisted state files, env vars, and the four scope-gated
gateway methods (push.web.vapidPublicKey, push.web.subscribe,
push.web.unsubscribe, push.web.test). Distinguish from the existing
APNS relay-backed iOS push path.
Adds browser PWA manifest and service worker support for the Control UI, plus gateway RPC methods and persisted Web Push subscription handling.
Maintainer verification:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/infra/push-web.test.ts src/gateway/server-methods/push.test.ts src/gateway/control-ui.test.ts src/gateway/protocol/push.test.ts
- pnpm check:changed passed before final GitHub update-branch merge commit
- pnpm build
Source head: 0720024368
Strip configured trailing /v1beta from Google music/video generation base URLs before calling the Google GenAI SDK.\n\nFixes #63240.\n\nThanks @Hybirdss.
Preserve Google Chat reply text when typing indicator cleanup or update fails.
- Extract Google Chat reply delivery into a focused module
- Retry the failed first text chunk as a new message after placeholder update failure
- Cover media caption and chunk fallback regressions
Thanks @colin-lgtm.
Summary:
- Show full date and time in Control UI chat message footers.
- Collapse assistant model/token/context metadata behind an explicit Context disclosure.
- Update changelog attribution guidance to allow multi-author credited entries.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub CI passed on f071a38177
eleven_v3 already works end-to-end (model_id passes through to the API
without validation), but was missing from ELEVENLABS_TTS_MODELS so it
never appeared in the in-product model picker or catalog metadata.
* fix(heartbeat): clamp scheduler delay to Node setTimeout cap (#71414)
When `agents.defaults.heartbeat.every` resolves to >2_147_483_647 ms
(~24.85d), the previous scheduleNext() called setTimeout with the raw
delay. Node clamps any delay > 2^31-1 to 1 ms, fires the callback, and
the heartbeat re-arms with the same oversized value - a tight loop that
floods the log with TimeoutOverflowWarning and crashes the gateway with
exit code 1.
Clamp the computed delay to HEARTBEAT_MAX_TIMEOUT_MS (2_147_483_647)
before calling setTimeout. The worst case is now one heartbeat every
~24.85d instead of crash-loop. Warn once per process when clamping
fires, so a misconfigured "365d" remains visible without flooding.
This is a defense-in-depth fix at the scheduler layer; loadConfig-level
rejection is a broader change with more blast radius and a separate
question (some users may legitimately want "every: 365d" to mean
"effectively never"). The clamped behaviour is closer to that intent
than the crash is.
Test: new scheduler test sets heartbeat.every="365d" with fake timers,
advances 60s, and asserts runSpy was never called (with the bug, it
would be called ~60_000 times).
* style: format heartbeat scheduler clamp
* fix: share safe timeout delay clamp (#71478) (thanks @hclsys)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Remove the misleading API Keys card from the quick settings page.
The card was hardcoded to a fixed env-var provider list and routed all actions to the broad Environment config section, which made the Add/Change affordances look more precise than they were. This removes the dead surface and keeps the quick settings grid focused on meaningful controls.
Verified:
- pnpm test ui/src/ui/views/config-quick.test.ts
- CI passed on PR #71496
Remove the startup persisted-offset getUpdates preflight so polling restarts do not self-conflict before the grammY runner starts.\n\nFixes #69304.\n\nThanks @chinar-amrutkar.
Add bounded outbound message delivery lifecycle diagnostics and OTEL export without message body, recipient, room, media path, or raw channel result data.
Polish the Control UI markdown preview chrome and sidebar raw-text behavior.
- Add the upgraded preview dialog/sidebar chrome and tighten related CSS coverage.
- Show workspace-relative paths in the markdown preview dialog instead of absolute filesystem paths.
- Preserve raw markdown source for idempotent raw-text toggles.
- Align browser plugin-sdk facade export parity for DEFAULT_BROWSER_ACTION_TIMEOUT_MS.
- Stabilize the gateway update channel test by waiting for the async update runner call.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/views/agents.test.ts ui/src/ui/views/chat.test.ts src/plugins/contracts/plugin-sdk-subpaths.test.ts src/gateway/server.roles-allowlist-update.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub checks green on ebbe96fc88
Three entries were missing co-credits I should have preserved:
- Diagnostics/OTEL exec-process spans (#71451): @vincentkoc implemented,
but @jlapenna's #70424 proposed the broader tracing work this entry
builds on. Now credits both.
- Diagnostics/OTEL preloaded SDK (#71450): same pattern — credits
@vincentkoc and @jlapenna.
- Agents/tool-result pruning (#51267): @cgdusek's PR explicitly built
on prior work in #39331 by @alvinttang and #34980 by @coffeexcoin.
Now credits all three.
- Two Diagnostics/OTEL Changes entries credited issue #70424 (jlapenna's
open meta-tracing proposal) as the PR ref. The actual implementing
PRs landed as #71451 (exec-process telemetry) and #71450 (preloaded
SDK mode), both authored by @vincentkoc — corrected.
- Telegram/webhook fix had no Thanks credit. Issue #71392 reporter
@joelforsberg46-source identified the delivery-retry behaviour, so
credit them on the entry.
* test(browser): cover tilde edge cases for executablePath
Adds coverage for cases the original tilde-expansion fix in 95a2c9b
intentionally supports but does not assert:
- bare "~" expands to the home directory
- Windows-style "~\AppData\..." expands to $HOME on Windows
- a stray "~" mid-path (e.g. /opt/~chromium/chrome) is preserved verbatim,
guarding the regex anchor against future regressions
No production code changes; tests only.
* test(browser): skip Windows-style ~\ tilde test on POSIX
path.resolve treats backslashes as literal characters on POSIX, so
"~\AppData\..." cannot resolve to "$HOME/AppData/..." on Linux/macOS.
Gate that case to win32 to keep the assertion meaningful.
- Remove duplicate #66884 alexlomt entry from top Unreleased > Fixes;
the canonical entry already lives under 2026.4.24 (Unreleased) per
Mason Huang's earlier 'move #66884 entry to 2026.4.24' commit.
- Reflow the wrapped 3-line Tool Access bullet (#71405) onto a single
line so it matches every other bullet in the section.
Three external-contributor commits from the last day landed without
CHANGELOG entries:
- Alex Fries (#68286, @ajfonthemove): hybrid memory search component
scores. Added under Unreleased > Changes (feat).
- Charles Dusek (#51267, @cgdusek): malformed tool-result text-block
guard. Added under Unreleased > Fixes.
- Jerome Benoit (#59935, @jerome-benoit): Nix Home Manager daemon PATH
support. Added under Unreleased > Fixes.
Also drop a duplicate raw-subject changelog line for #66884 that
restated alexlomt's already-formatted entry one line above.
Expose raw `vectorScore` and `textScore` alongside the combined hybrid memory search `score`.
- Preserve vector/text component scores from `mergeHybridResults` output.
- Add optional component-score fields to both memory host SDK type surfaces.
- Extend hybrid merge tests for vector-only, text-only, and overlapping result cases.
- Document that component scores remain raw retrieval diagnostics while temporal decay/MMR only adjust or reorder the combined ranking `score`.
Closes#68166.
Maintainer verification:
- `pnpm test extensions/memory-core/src/memory/hybrid.test.ts`
- `pnpm check:changed`
- Fresh GitHub checks passed.
Co-authored-by: Alex Fries <alex@engramlabs.io>
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by #39331 and #34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two recent code changes lacked or had only partial doc coverage:
- contextInjection 'never' (#65006, xDarkicex): the new mode is now
documented under agents.defaults.contextInjection, alongside the
existing 'continuation-skip' mode, with guidance on when to use it
(custom context engines, native runtimes that own their prompt).
- Nix Home Manager daemon PATH (#44402, jerome.benoit): document the
service PATH auto-discovery (NIX_PROFILES right-to-left precedence
and ~/.nix-profile/bin fallback) under the Nix install page.
Also sentence-case three Title-Cased headings on the Nix page ('What
You Get', 'Quick Start', 'Nix Mode Runtime Behavior') and drop a
duplicate body H1 that restated the frontmatter title.
* fix(feishu): prevent duplicate message after streaming card close (#67791)
When onIdle closed the streaming card before the final delivery arrived, the streamed text was not tracked in deliveredFinalTexts. The subsequent final payload bypassed the streaming?.isActive() guard (already closed) and fell through to the non-streaming path, sending the same content as a redundant text/card message. Track raw streamText in deliveredFinalTexts when closeStreaming finalizes the card so the duplicate-final check catches it.
* test(feishu): cover idle streaming final dedupe
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Add Nix Home Manager profile bin directories to generated gateway service PATHs on macOS and Linux.
Includes ~/.nix-profile/bin fallback when NIX_PROFILES is absent, honors NIX_PROFILES right-to-left precedence when present, and covers the service PATH resolver with focused unit tests.
Closes#44402.
Defers the Node fs.constants lookup until tmp-dir resolution actually runs, adds browser-shim import regression coverage, and records the fix in the changelog.\n\nLocal verification:\n- pnpm test src/infra/tmp-openclaw-dir.browser-import.test.ts src/infra/tmp-openclaw-dir.test.ts src/logging/logger.browser-import.test.ts\n- pnpm test src/infra/run-node.test.ts -t "serializes runtime postbuild restaging|forwards wrapper SIGTERM"\n- pnpm build\n\nCo-authored-by: Valentinws <Valentinws@users.noreply.github.com>
Adds regression coverage for provider-qualified nested model ids such as nvidia/deepseek-ai/deepseek-v3.2.
Validated:
- pnpm test ui/src/ui/chat-model-ref.test.ts ui/src/ui/chat-model-select-state.test.ts
Thanks @monsonego.
Three table headers introduced in recent agent-runtime / Codex-harness
doc commits used Title Case despite the surrounding house style:
- agent-runtimes.md L17: 'What It Means' -> 'What it means'
- agent-runtimes.md L100: 'Why It Matters' -> 'Why it matters'
- codex-harness.md L615: 'V1 Boundary' / 'Future Path' ->
'V1 boundary' / 'Future path' (V1 stays as the recognized acronym)
Four pages started with weak meta-descriptions ('This page covers...')
that restate the frontmatter summary. Replace with direct content-first
openings, and sentence-case a stray 'Slash Commands' link in
configuration-reference.
- concepts/streaming.md: remove '# Streaming + chunking'.
- reference/session-management-compaction.md: remove Title Case H1
'# Session Management & Compaction (Deep Dive)'.
- plugins/voice-call.md: remove '# Voice Call (plugin)'.
CLI pages keep their command-formatted body H1s since that is the repo
convention and the formatting is not expressible in frontmatter.
Sweep recent (last ~5h) doc edits for two readability/uniformity issues:
- Replace 42 path-as-text links of the form '[/foo/bar](/foo/bar)' with
descriptive labels derived from each target page's frontmatter title
(e.g. '[Anthropic]', '[Token use and costs]', '[OpenAI-compatible
endpoints]'). Affected files include gateway/troubleshooting,
concepts/oauth, reference/session-management-compaction, and
reference/transcript-hygiene.
- Sentence-case Title-Cased headings and link text in Related sections
across codex-harness, model-providers, tools/plugin, sdk-runtime,
sdk-setup, prompt-caching, ci, cli/config, google-meet, browser,
rich-output-protocol, subagents, web/control-ui, while preserving
brand and proper-noun capitalization (OpenAI, Codex, Chrome, Parallels,
Z.AI, etc.).
* [EV-001] memory-core: filter memory_search session hits by visibility
- Move session visibility + listSpawnedSessionKeys to plugin-sdk; sync test
hook with sessions-resolution __testing.setDepsForTest
- Extract loadCombinedSessionStoreForGateway to config/sessions; re-export
from gateway session-utils
- Add session-transcript-hit stem resolver for builtin + QMD paths
- Post-filter memory_search results before citations/recall; fail closed when
requester session key missing; optional corpus=sessions
- Tests: stem extraction, visibility filter smoke, existing suites green
* chore: sync plugin-sdk exports for session-transcript-hit and session-visibility
Run pnpm plugin-sdk:sync-exports so package.json exports match
scripts/lib/plugin-sdk-entrypoints.json. Fixes contract tests and
lint:plugins:plugin-sdk-subpaths-exported for memory-core imports.
* fix(EV-001): cross-agent session memory hits + hoist combined store load
- resolveTranscriptStemToSessionKeys: stop filtering by requester agentId so
keys from other agents reach createSessionVisibilityGuard (a2a + visibility=all).
- Re-export loadCombinedSessionStoreForGateway from session-transcript-hit;
filterMemorySearchHitsBySessionVisibility loads the combined store once per pass.
- Drop unused agentId from filter params; extend tests (Greptile/Codex review).
* fix(memory_search): honor corpus=sessions before maxResults cap
Pass sources into MemoryIndexManager.search so FTS/vector queries add
source IN (...) before ranking and top-N slice (Codex: non-session hits
could fill the window).
QMD path: oversample fetch limit for single-source recall, filter by
source, then diversify/clamp to the requested maxResults.
Wire corpus=sessions from tools; extend MemorySearchManager opts and
wrappers.
* fix(memory_search): apply corpus=memory source filter like sessions
Pass sources: ["memory"] into manager.search so maxResults applies only
within the memory index; post-filter for defense in depth. Document
corpus=memory in the tool description.
* fix: scope qmd session memory search
* fix: enforce memory search session visibility (#70761) (thanks @nefainl)
---------
Co-authored-by: NefAI <info@nefai.nl>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Preserve the requester-agent announce path for thread-bound subagent completions, while falling back to direct thread delivery only when the announce fails or produces no visible output.\n\nThanks @DolencLuka.
When navigating the /models picker via provider button click, the model
list showed raw model IDs (e.g. gemini-3.1-pro-preview) instead of
configured display names (e.g. Gemini 3.1 Pro (Bridge)).
Root cause: the button-click callback handler destructured modelData as
{ byProvider, providers } omitting modelNames, then called
buildModelsKeyboard() without it. buildModelsKeyboard falls back to the
raw model ID via modelNames?.get(...) ?? model when modelNames is absent.
The text-command path (/models <provider>) already passes modelNames
correctly through buildTelegramModelsListChannelData, confirming the fix.
Fix: destructure modelNames from modelData and forward it to
buildModelsKeyboard in the button-click callback handler.
Closes#70560
Fixes#70889 and #70890.
Retains overlapping/shared agent workspaces during `openclaw agents delete`, keeps `--json` output machine-readable, and repairs the stale hook-runner test harness mock that blocked CI.
Thanks @kaseonedge.
- Expand author->handle map with maintainers from docs/CONTRIBUTING.md
(Robin Waslander/@hydro13, Josh Lehman/@jalehman, Radek/@velvet-shark,
Muhammed/@mukhtharcm, Tengji/@odysseus0, Sliverp, Mason Huang/@hxy91819)
and PR-author lookups via gh for two dozen one-off contributors.
- Strip duplicate trailing 'Thanks @x' lines that prior backfill chunks
had introduced when an existing lowercase 'thanks @y' credit was already
present (case-sensitive skip check missed them); preserve the original
contributor credit.
- Dedupe doubled '(#NNNN)' tokens introduced by the same bug.
* fix(cron): default missing sessionTarget on load and guard assertSupportedJobSpec
* fix(cron): use Object.hasOwn for payload.kind check and log the backfill
Address review feedback on #70367:
- Switch the new payload.kind lookup from `in` to `Object.hasOwn` so
prototype pollution cannot drive the defaulter (Aisle Low finding).
- Log a warning when a job is auto-defaulted at load time, matching the
adjacent legacyJobIdIssue pattern so operators can run `openclaw
doctor --fix` to persist the canonical shape (Greptile P2).
* fix(cron): dedupe sessionTarget backfill warn per jobId and sharpen crash site reference
Address deep-review feedback on #70367:
- The code comment referenced assertSupportedJobSpec as the tick-time
crash site, but that function is only called from create/patch
(jobs.ts:607, 686) and manual-run preflight (ops.ts:516). The actual
on-tick TypeError surfaces in runIsolatedAgentJob (server-cron.ts).
Update the comment to say so.
- ensureLoaded runs with forceReload:true on every onTimer tick (~60s).
Before this change, a persistent legacy job missing sessionTarget
produced one warn line per tick, forever. Add a per-jobId dedupe set
on CronServiceState (mirroring the existing warnedDisabled flag) so
the warn fires once per job per process.
- Drop the 'run openclaw doctor --fix' remediation from the warn
message. Doctor's cron-store migration has no trackIssue entry for
missing sessionTarget (doctor-cron-store-migration.ts CronStoreIssueKey),
so doctor --fix on a store whose only defect is missing sessionTarget
silently returns without writing anything. Point operators at
jobs.json directly until that gap is closed.
* docs(changelog): note cron session target repair
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
prepareCliBundleMcpConfig was not including cfg.mcp.servers when building
the temporary mcp.json that gets passed to claude-cli via --mcp-config.
This meant user-defined MCP servers (e.g. mcp.servers.omi in openclaw.json)
were silently dropped, even though --strict-mcp-config prevents any other
path for those servers to reach the CLI session.
The Pi runtime path (loadEmbeddedPiMcpConfig) already merges cfg.mcp.servers
after the bundle layer. This commit applies the same merge to the CLI runtime
path, with identical precedence: bundle defaults < user mcp.servers <
additionalConfig (loopback). The loopback entry remains last so it cannot be
overridden by user config.
Fixes: user-configured MCP servers not appearing as mcp__<name>__* tools in
claude-cli sessions started by OpenClaw.
* fix(auth): bootstrap codex cli credential without clobbering local
readCodexCliCredentialsCached was imported but never registered in
EXTERNAL_CLI_SYNC_PROVIDERS, so overlayExternalAuthProfiles could not
seed openai-codex:default on fresh agents and runtime surfaced
"No API key found for provider openai-codex" even after a successful
codex login.
Register the provider with a new bootstrapOnly flag. Providers flagged
bootstrapOnly are adopted only to fill an empty slot: the overlay skips
them when a local OAuth credential already exists for the profile, and
readExternalCliBootstrapCredential returns null so the refresh path
never replaces the locally stored canonical refresh token with stale
CLI state. Minimax keeps its existing replace-on-expiry behavior.
* test(auth): cover codex cli bootstrap
---------
Co-authored-by: sudol <sudol@A8Max.localdomain>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Patch live session usage metadata into the Control UI session list, coalesce overlapping refreshes, and add a compact action when fresh context usage is high.
Keep session refresh loading separate from session mutation ownership so background refreshes cannot re-enable mutation UI or overwrite delete/restore state mid-flight.
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
Preserve tokenjuice runtime rule JSON under dist/rules/tests during bundled plugin runtime dependency staging while continuing to prune unrelated tests directories.
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.
Replaces #70815. Closes#71183.
Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
Browser control now authorizes only the resolved active gateway credential and fails closed when password mode lacks a resolved password.
Also removes the duplicate Slack test-helper middleware stub that kept current CI red after the base rebase.
Fixes#65626.
Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
* fix(gateway): restart channels after secret reload
* fix(gateway): serialize secrets.reload and isolate channel restart errors
Address review feedback from Greptile (P1), Codex (P2), and Aisle (Medium,
CWE-362) on #70720:
- Serialize the entire secrets.reload path through a promise tail lock so
concurrent callers cannot overlap the stop/start loop or diff against a
stale pre-activation snapshot.
- Wrap each channel's stop/start pair in a try/catch so one channel failing
to restart does not leave other changed channels unrestarted.
- Register slack/zalo/discord channel plugins with reload.configPrefixes in
the test setup so channels.<id>.* diff paths actually match a restart rule
(without this, the diff falls through to restart-gateway and the handler
never enters the per-channel restart branch).
- Add tests covering concurrent-reload serialization and per-channel
restart-failure isolation.
* fix(gateway): surface channel restart failures from secrets.reload
Address review feedback on the previous commit:
- Codex P1: `secrets.reload` swallowed per-channel restart failures and
still returned `{ ok: true }`, so a rotation that left a channel on the
old secret looked successful to the caller. The handler now collects
restart failures during the loop and throws an aggregate error after
attempting every channel, so the client-side RPC response surfaces the
partial failure while unaffected channels still restart (preserving the
original Greptile P1 non-cascading semantic).
- Greptile P2: test mock-call assertions sorted the captured channel
arguments so they no longer depend on `Set`/object-key iteration order,
which is not a stable contract of the handler.
* fix(gateway): harden secrets reload followups
* docs(changelog): note secret-backed channel restart on secrets.reload
* test(gateway): align secrets reload snapshot activation
* test(gateway): reset plugin runtime state in aux handlers
* fix(gateway): refresh reload rules and roll back channels
* fix(gateway): harden secrets.reload rollback tests
* test(gateway): inject aux handler reload plan
* test(gateway): avoid resettable reload-plan mocks
* test(gateway): isolate aux handler tests from skip env-var leakage
test-helpers.mocks.ts and test-helpers.server.ts set
OPENCLAW_SKIP_CHANNELS=1 / OPENCLAW_SKIP_PROVIDERS=1 at module load. When
a shared vitest worker imports those helpers before this file's tests
run, the leaked env vars route the secrets.reload skip-mode branch and
the channel restart loop never fires. Add a beforeEach that clears both
env vars so the suite is independent of worker import order.
* fix(gateway): restore required generation on secrets.reload rollback
setCurrentSharedGatewaySessionGeneration can clear `required` as a side
effect of activating a new generation. The previous rollback path
restored only `current`, leaving `required` cleared and weakening
shared-gateway auth-generation enforcement after a failed reload (Aisle
CWE-287). Capture both fields before activation and restore both in the
catch block. Add a focused regression test that locks in the contract.
* fix(gateway): track restart channels for rollback before stopChannel awaits
Pushing to stoppedChannels only after `await stopChannel` succeeded meant
that if stopChannel rejected mid-call (for example, a plugin stopAccount
hook throws after the runtime already closed the socket), the rollback
loop skipped that channel entirely. A failed secrets.reload could then
leave the channel down. Track the channel before awaiting so rollback
always attempts to bring it back, and add a regression test.
Export diagnostics OTEL logs through bounded diagnostic log events while keeping core log records off the public plugin diagnostic stream.\n\nIncludes security hardening for log payload redaction, bounded attributes, prototype-pollution keys, OTEL export failure reporting, and extension SDK seam usage.
* fix(slack): route stream-fallback delivery through chunked sender
deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.
Two changes:
1. deliverPendingStreamFallback now routes through deliverReplies so
long pendingText is chunked by the normal sender and the fallback
honors the configured replyToMode / identity.
2. The non-benign streaming-error branch in deliverWithStreaming now
clears the session via markSlackStreamFallbackDelivered before
falling back to deliverNormally. Without this, pendingText stays
populated and the post-loop finalize (stopSlackStream →
SlackStreamNotDeliveredError → fallback) re-posts the same chunk
that deliverNormally already sent.
Addresses the three Codex P1 findings on #70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.
Follow-up to #70370.
* fix(slack): preserve pending buffered text on non-benign stream errors
Address Codex P1 on #71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.
Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.
Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.
* fix(slack): harden stream fallback docs and chunking test (#71124)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Verified against Carbon 0.16.0 source:
- Client constructor calls plugin.registerClient(this) without awaiting it.
- GatewayPlugin.registerClient publishes client before its awaited metadata fetch.
- identify() silently returns when client is missing.
This patch matches Carbon's ordering in OpenClaw's subclass, avoids a second super.registerClient call if lifecycle connect already opened the socket during metadata loading, and keeps regression coverage for both ws and isConnecting cases.
Local proof:
- pnpm test extensions/discord/src/monitor/provider.proxy.test.ts extensions/discord/src/monitor/gateway-plugin.test.ts
- pnpm lint:tmp:no-raw-channel-fetch
- pnpm check:changed
- pnpm check
- pnpm test
GitHub checks green for 72547825e1.
Fixes outbound Twilio realtime conversations so the TwiML fetch returns the realtime <Connect><Stream> path for outbound directions and the answered-call path does not overwrite it with legacy <Say> TwiML.
Local proof:
- pnpm test extensions/voice-call/src/manager.notify.test.ts extensions/voice-call/src/webhook.test.ts
- pnpm check:changed
- pnpm check
- pnpm build
- local VoiceCallWebhookServer + CallManager smoke for Direction=outbound-api
Closes#68713.
Keep WebChat runtime context available to the model while persisting only the transcript-facing user prompt across gateway, CLI, queued follow-up, and embedded Pi paths.
Adds regression coverage for history sanitization, CLI transcript persistence, media-only auto-reply prompts, and embedded Pi prompt rewrite against a real SessionManager file.
Co-authored-by: 91wan <91wan@users.noreply.github.com>
Render assistant text avatars from IDENTITY.md consistently in the Control UI chat welcome state and transcript groups.
Also supports authenticated blob avatar URLs in grouped messages and rejects bidi/invisible controls in assistant text avatars.
Verification:
- pnpm test ui/src/ui/chat/grouped-render.test.ts ui/src/ui/views/chat.test.ts ui/src/styles/chat/layout.test.ts
- pnpm check:changed
- GitHub CI green
- Review threads resolved
Keep WhatsApp QR login state synced across gateway, macOS, and UI wait flows.
- Preserve the latest QR data URL/version while login polling rotates codes.
- Keep the wait-result protocol bounded to current QR metadata.
- Stabilize QR rendering and media fixture coverage after rebasing on main.
Validation:
- pnpm test extensions/whatsapp/src/login-qr.test.ts extensions/whatsapp/src/media.test.ts extensions/whatsapp/src/agent-tools-login.test.ts src/gateway/protocol/channels.schema.test.ts src/gateway/server-methods/web.start.test.ts ui/src/ui/controllers/channels.test.ts
- pnpm test:extension whatsapp
- cd apps/macos && swift test --filter ChannelsSettingsSmokeTests
- GitHub PR checks: 62 success, 5 skipped
Adds the VoiceClaw-compatible realtime brain WebSocket endpoint backed by Gemini Live, with owner-auth gating, async OpenClaw tool handoff, docs, and lifecycle tests.
Maintainer fixup: terminal upstream errors now send the error, emit session.ended while the client socket is still open, then close the client-facing socket.
Co-authored-by: Michael Yagudaev <1386966+yagudaev@users.noreply.github.com>
Register agent RPC runs in the shared abort controller map so chat.abort and sessions.abort can interrupt them like chat.send runs.
Also centralize abort-controller registration/owned cleanup, preserve agent timeout semantics for maintenance expiry, and cover pre-dispatch failure cleanup with regression tests.
Fixes#71128.
* fix(googlechat): log webhook auth reject reasons and warn on appPrincipal misconfig
Closes#71078
Webhook auth failures previously returned 401 with no log line, leaving
operators no signal to diagnose. Additionally, app-url audience requires
a numeric OAuth 2.0 client ID as appPrincipal, but a misconfigured email
or empty value silently caused all requests to be rejected.
Changes:
- Log a WARN with accountId and reject reason when verifyGoogleChatRequest fails.
- Add warnAppPrincipalMisconfiguration() called at provider init: warns when
audienceType=app-url and appPrincipal is missing or contains '@'.
Tests: +9 cases in monitor-webhook.test.ts (3 reject-reason scenarios + 4 warner cases).
* fix(googlechat): defer auth rejection logs
* docs: note googlechat webhook auth fix
---------
Co-authored-by: luyao618 <luyao618@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
two bugs. both squash user model choice silently.
bug 1: applyDefaultModel() unconditional primary: model overwrite.
wizard calls with setDefaultModel=true, provider returns its default
(e.g. openrouter/auto), bam user primary gone. fix: existingPrimary ?? model.
bug 2: applyModelFallbacksFromSelection() phantom primary injection.
when no primary configured, resolvedKey (hardcoded default) written as
primary via nullish coalescing fallback. fix: conditional spread — only
include primary key when one actually existed.
tests for both. closes#70696
Expose first-class hook correlation fields for plugin message and run lifecycle hooks, including frozen diagnostic trace copies for plugin-facing events.
Adds the Gradium bundled plugin with TTS and speech-provider registration, docs, label routing, and focused/live coverage.
Also carries the current main lint cleanup needed for the rebased CI lane.
Co-authored-by: laurent <laurent.mazare@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): setting systemPrompt to "" suppresses the wildcard instead of falling through to it
* test(whatsapp): reset mocks instead of only clearing call history
* docs(changelog): note WhatsApp empty systemPrompt suppresses wildcard
* test(whatsapp): preserve real module exports in process-message mocks
* test(whatsapp): whitespace-only systemPrompt also suppresses wildcard
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Export diagnostics OTEL lifecycle spans for runs, model calls, and tool executions while avoiding retained live span state and high-cardinality/sensitive exported attributes.
* fix(qqbot): enable qqbot plugin by default so runtime deps install before QR-code setup
The qqbot plugin manifest was missing the enabledByDefault: true flag.
Without it, ensureBundledPluginRuntimeDeps treats qqbot as bundled-but-
disabled-by-default (isBundledPluginConfiguredForRuntimeDeps returns
false when no qqbot channel/account is configured yet), so
@tencent-connect/qqbot-connector is never installed into
dist/extensions/qqbot/node_modules on first launch.
This creates a chicken-and-egg failure for the QR-code binding flow:
finalize.ts dynamically imports @tencent-connect/qqbot-connector to run
qrConnect(), but the package isn't present yet because no account is
configured — binding is exactly the step that configures the first
account. Users hit:
QQ Bot 绑定失败: Error [ERR_MODULE_NOT_FOUND]: Cannot find package
'@tencent-connect/qqbot-connector' imported from
.../dist/extensions/qqbot/channel-*.js
Adding enabledByDefault: true makes the host install qqbot's runtime
deps eagerly on first launch, mirroring the pattern already used by
mistral / groq / deepgram / amazon-bedrock-mantle and other bundled
plugins whose providers must be available before any channel config
exists. No code changes required; the existing runtime-deps install
pipeline handles everything once the gate is opened.
* fix(qqbot): changelog for enable-by-default fix (#71051) (thanks @cxyhhhhh)
---------
Co-authored-by: sliverp <870080352@qq.com>
Make diagnostics-otel startup restart-safe by tearing down stale SDK, log transport, and diagnostic-event listener handles before reinitializing or disabling the service. Adds regression coverage for repeated start and disabled restart paths.\n\nThanks @vincentkoc.
Record onboarding plugin install source metadata for npm and local paths, while keeping local path install records portable and preserving uninstall cleanup for relative source paths.
Pass immutable diagnostic trace contexts through agent and tool hook surfaces, emit model usage with the run trace, and parent OTEL spans/logs from validated trace context without retained global state.\n\nThanks @vincentkoc.
* test(cli-runner): RED — assert before_agent_reply fires on cron triggers
Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts
for the CLI runner. Asserts:
1. When trigger=cron and a before_agent_reply hook claims the turn
(handled: true), runCliAgent must NOT invoke the codex subprocess and
must return the hook's reply text in payloads[0].
2. When the hook claims without a reply body, the synthesized payload
uses SILENT_REPLY_TOKEN.
3. Non-cron triggers do not invoke the hook (no behavior change for
normal user/heartbeat traffic).
4. Without a registered hook, falls through to the CLI subprocess.
Currently fails (RED): tests 1 and 2 fail because runCliAgent never
fires before_agent_reply — the hook gate exists only in the embedded PI
runner (src/agents/pi-embedded-runner/run.ts:326). This is the
CLI-backed-agent dreaming gap reported in #70940 and identified in
PR #70737 review.
Next commit: implement the hook gate in runPreparedCliAgent (GREEN).
* fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns
Mirrors the embedded PI runner gate from
src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs
(notably memory-core dreaming) can short-circuit a CLI-backed agent
turn before the codex/claude/gemini subprocess is spawned.
Without this, configuring a default agent's model to a CLI backend
(codex-cli, claude-cli, gemini-cli, or any third-party
`registerCliBackend` provider) silently broke dreaming: the cron
sentinel was sent to the underlying LLM as a literal user prompt and
the dreaming hook never executed. See openclaw/openclaw#70940 for the
empirical repro (codex-cli observed sending the dream-token to GPT-5.5
with no `memory-core: dreaming promotion complete` line).
Also extracts `buildHandledReplyPayloads` locally; eventually that
should be unified with the embedded PI runner's helper, but that's a
mechanical refactor for a follow-up.
Closes#70940 once both this PR and #70737 land — this fix is only
useful if cron-driven dreaming exists, which is what #70737 introduces.
TDD trail:
- prior commit: RED test asserting the hook gate (4 cases)
- this commit: implementation that turns those tests green (4/4 pass).
Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts
4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean
on touched files; pre-existing tsgo failure in
src/plugin-sdk/provider-tools.ts is unrelated to these changes.
* Revert "fix(memory/dreaming): surface blocked status when heartbeat is disabled for main (#69875)"
This reverts commit 529577e045.
Making way for the dreaming-vs-heartbeat decoupling from Josh's
josh/dreaming-isolated-cron-fix branch, which moves the managed dreaming
cron to isolated agent turns (sessionTarget: "isolated") so dreaming no
longer requires heartbeat to fire. Once the cron no longer rides the
heartbeat path, the blocked-reason observability has nothing left to
report — removing it cleanly here before the cherry-picks land.
* openclaw-3ba.1: move managed dreaming cron to isolated agent turns
* openclaw-46d: claim cron runs before embedded attempts
* openclaw-575: disable managed dreaming cron delivery
* openclaw-575: accept wrapped dreaming cron tokens
* openclaw-ccd: filter cron and wrapper transcript noise from dreaming corpus
* openclaw-cd9: filter archived, cron, and heartbeat transcript noise from dreaming corpus
* openclaw-cd9: suppress role-label reflection tags in rem dreaming
* openclaw-b49: stop narrative timeouts from blocking dreaming cron
* openclaw-b49: keep managed dreaming cron out of diary subagents
* openclaw-ff9: restore cron dream diary generation without serial waits
* openclaw-ff9: run dreaming narratives with lightweight isolated subagent lanes
* openclaw-ff9: detach cron dream diary generation from run completion
* openclaw-ff9: defer cron diary task startup until after cron completion
* doctor/cron: migrate stale managed dreaming jobs to isolated agent turns
After the dreaming cron moved off the heartbeat path to sessionTarget:
"isolated" + payload.kind: "agentTurn" (see the preceding memory-core
changes), users with existing ~/.openclaw/cron/jobs.json entries in the
old sessionTarget: "main" + payload.kind: "systemEvent" shape still
carry stale jobs until the gateway restart reconcile rewrites them.
Add a dreaming-specific cron migration to the existing
maybeRepairLegacyCronStore doctor path so "openclaw doctor" (and
"openclaw doctor --fix") rewrites those jobs without needing a gateway
restart. Match lives in a new doctor-cron-dreaming-payload-migration
helper alongside the existing legacy-delivery and store-migration files.
The matching uses the memory-core managed-job name and description tag
plus the short-term-promotion payload token. Constants are mirrored
from extensions/memory-core/src/dreaming.ts and commented so a future
rename in memory-core is a visible drift point here too.
* memory/dreaming: tighten cron-token match to known wrapper, not substring
The previous match relaxed the line check from 'trimmed line equals token'
to 'line contains token anywhere as a substring' to accept the
`[cron:<id>] <token>` wrapper that isolated-cron turns add. Substring
matching also let any user message embedding the token mid-sentence
trigger the dream-promotion hook, and was flagged by both Greptile and
Aisle on PR #70737.
Replace it with strip-the-known-prefix-then-exact-match: keep the
`[cron:<id>]` wrapper case working, reject every other variant. Add
focused unit coverage that the bare token, the wrapped token, and bare
multiline cases match while embedded / code-fenced / arbitrarily-wrapped
variants do not.
* memory/dreaming: drop assistant followup only on assistant-side signals
Per PR #70737 review (aisle-research-bot, Medium): the previous logic
suppressed the next assistant message whenever the prior user message
matched a 'generated prompt' pattern (`[cron:...]`,
`System (untrusted): ...`, heartbeat prompts, exec-completion events).
Real users can type those same patterns, which let a user exfiltrate
real assistant replies from the dreaming corpus by prefixing their own
prompt — the assistant's reply would be silently dropped.
Remove the cross-message coupling. Assistant-side machinery (silent
replies, system wrappers) is already dropped by sanitizeSessionText,
which is the right layer for that filter. Add an explicit assistant-side
HEARTBEAT_TOKEN check to keep the legitimate `HEARTBEAT_OK` ack drop
working without depending on the prior user message. Add a regression
test exercising the spoofing scenario.
* doctor/cron: assert mirrored dreaming constants stay in sync
Per PR #70737 review (greptile-apps): the doctor migration mirrors three
constants (MANAGED_DREAMING_CRON_NAME, MANAGED_DREAMING_CRON_TAG,
DREAMING_SYSTEM_EVENT_TEXT) from extensions/memory-core/src/dreaming.ts.
A future rename in either file would silently break the migration.
Add a vitest unit that reads both files and asserts the literals match.
Manually verified the assertion fires with a clear error when one side
diverges. Adds no runtime cost; sits in the regular test pipeline.
* fix(memory): stabilize dreaming CI checks
* memory/dreaming: skip eager narrative session cleanup when detached
Per PR #70737 review (chatgpt-codex-connector, P2): runDreamingSweepPhases
called deleteNarrativeSessionBestEffort synchronously right after each
phase. Once narrative generation moved to detached mode (queued via
queueMicrotask), the eager cleanup races the writer: the session is
deleted before the queued subagent run reads it, silently dropping cron
diary entries.
Skip the eager cleanup branch when params.detachNarratives is true.
generateAndAppendDreamNarrative still runs its own deleteSession in the
finally{} block, so the cleanup intent is preserved without the race.
Heartbeat-driven (non-detached) runs keep the original eager-cleanup
behavior.
* fix(plugin-sdk): restore heartbeat-summary re-export
Per PR #70737 review (chatgpt-codex-connector, P1): the revert of
PR #69875 dropped the `heartbeat-summary` re-export from
`openclaw/plugin-sdk/infra-runtime`. That subpath shipped publicly two
days earlier, so removing it is technically a breaking change to a
public SDK surface — third-party plugins importing
`isHeartbeatEnabledForAgent` / `resolveHeartbeatIntervalMs` from this
path would fail with no replacement contract introduced.
Restore the re-export. Costs nothing to keep; the helpers are already
public via `../infra/heartbeat-summary.ts`. SDK additions are by
default backwards-compatible (CLAUDE.md), so removing within days of
introduction violates that intent.
* changelog: note dreaming decoupling from heartbeat
Refs PR #70737.
---------
Co-authored-by: Josh Lehman <josh@martian.engineering>
Require the Codex app-server bridge to wait for the final two-phase approval decision, while preserving the explicit no-route sentinel behavior.
Local gate on rebased branch: pnpm check:changed (20 files, 157 tests).
Thanks @Lucenx9.
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
Harden Codex app-server approval preview text sanitization and truncation handling.
Thanks @Lucenx9.
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
Port the Codex app-server harness onto the context-engine lifecycle, add Codex context projection and compaction integration, and cover bootstrap/history/compaction fallback behavior.
Thanks @jalehman.
* fix(whatsapp): normalize outbound media payloads
* fix(embedded-runner): preserve final media directives
* fix(auto-reply): keep non-streaming media on final path
* fix(auto-reply): warn when reply media is dropped
* fix(whatsapp): align auto-reply media delivery
* docs(changelog): note whatsapp media normalization
* fix: propagate timeoutMs to guarded dispatchers
Thread timeoutMs through the dispatcher creation chain so that
per-request (guarded) dispatchers honor the configured LLM timeout
instead of falling back to undici's hardcoded 60s bodyTimeout/headersTimeout.
Changes:
- undici-runtime.ts: createHttp1Agent/ProxyAgent/EnvHttpProxyAgent now accept
timeoutMs and apply bodyTimeout/headersTimeout to dispatcher options
- ssrf.ts: createPinnedDispatcher accepts timeoutMs and passes it through
- fetch-guard.ts: fetchWithSsrFGuard reads timeout from params or falls back
to global dispatcher bodyTimeout via getGlobalDispatcher()
- provider-transport-fetch.ts: buildGuardedModelFetch accepts optional
timeoutMs and passes it to fetchWithSsrFGuard
The global dispatcher timeout (set by ensureGlobalUndiciStreamTimeouts)
is still applied to non-guarded requests. Guarded requests (used by LLM
transports) now also receive the timeout via a fallback to the global
dispatcher when not explicitly provided.
Fixes#70829
* fix: resolve fallback timeout via module-level bridge variable
Replace dead-code .options.bodyTimeout read in resolveDispatcherTimeoutMs
with a module-level bridge (_globalUndiciStreamTimeoutMs) set by
ensureGlobalUndiciStreamTimeouts. This avoids reliance on Undici's
non-public .options field and ensures guarded dispatchers inherit the
configured stream timeout instead of falling back to undici's 60s default.
Fixes Greptile P1 and Codex comments on PR #70831
* chore: re-run CI smoke tests
* test: cover guarded dispatcher timeout propagation
* test: align timeout bridge expectation
* docs: note guarded dispatcher timeout fix
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
- Keep commit messages concise and action-oriented.
- Group related changes; avoid bundling unrelated refactors.
- Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues.
- Do not commit PR-only artifacts such as screenshots under `.github/pr-assets`; attach them to the PR/comment or use an external artifact store instead.
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## QA credentials and 1Password
- Use `op` only inside `tmux` for QA secret lookup in this repo.
- Quick auth check inside tmux:
```bash
op account list
```
- Direct Telegram npm live test secrets currently live in 1Password item:
- vault: `OpenClaw`
- item: `Telegram E2E`
- That item is the first place to look for:
-`OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN`
-`OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN`
-`OPENCLAW_QA_PROVIDER_MODE`
-`OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC`
- Convex QA secrets currently live in 1Password items:
- vault: `OpenClaw`
- item: `OPENCLAW_QA_CONVEX_SITE_URL`
- item: `OPENCLAW_QA_CONVEX_SECRET_MAINTAINER`
- item: `OPENCLAW_QA_CONVEX_SECRET_CI`
- Additional related notes/login items seen during QA credential work:
- vault: `Private`
- items: `OPENCLAW QA`, `Convex`, `Telegram`
- If a required value is missing from those notes:
- do not guess
- ask the maintainer/operator for the current value or the current 1Password item name
- for Telegram direct runs, `OPENCLAW_QA_TELEGRAM_GROUP_ID` may be stored separately from `Telegram E2E`
- for Convex runs, the leased Telegram credential should provide the Telegram group id and bot tokens together; do not require a separate `OPENCLAW_QA_TELEGRAM_GROUP_ID`
- for Convex runs, prefer `OpenClaw/OPENCLAW_QA_CONVEX_SITE_URL`; if that is stale or unclear, ask for the active pool URL before running
- Prefer direct Telegram envs for the npm Telegram Docker lane when available:
- do not assume `OPENCLAW_QA_PROVIDER_MODE` is consumed by that wrapper
- if a 1Password note only gives `OPENCLAW_QA_PROVIDER_MODE`, map it explicitly to `OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE` before running the Docker lane
- Verified live shape:
- Convex mode can pass the real Docker lane without direct Telegram env vars
- leased Telegram payload includes the group id coupled to the driver/SUT tokens
- a real run of `pnpm test:docker:npm-telegram-live` passed with:
- Replies: repo-root file refs only, e.g.`extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
-CODEOWNERS: maintenance/refactors/tests are ok. For larger behavior, product, security, or ownership-sensitive changes, get a listed owner request/review first.
-First pass: run docs list (`pnpm docs:list`; ignore if unavailable), then read only relevant docs/guides.
-Missing deps: run `pnpm install`, rerun once, then report first actionable error.
-Use "plugin/plugins" in docs/UI/changelog. `extensions/` remains internal workspace layout.
-Add channel/plugin/app/doc surface: update `.github/labeler.yml` and matching GitHub labels.
-New `AGENTS.md`: add sibling `CLAUDE.md` symlink to it.
- Replies: repo-root refs only:`extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
-Run docs list first: `pnpm docs:list` if available; read relevant docs only.
-High-confidence answers only when fixing/triaging: verify source, tests, shipped/current behavior, and dependency contracts before deciding.
-Dependency-backed behavior: read upstream dependency docs/source/types first. Do not assume APIs, defaults, errors, timing, or runtime behavior.
-Live-verify when feasible. Check env/`~/.profile` for keys before assuming live tests are blocked; keep secret output redacted.
-Missing deps: `pnpm install`, retry once, then report first actionable error.
-CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
- Wording: product/docs/UI/changelog say "plugin/plugins"; `extensions/` is internal.
- New channel/plugin/app/doc surface: update `.github/labeler.yml` + GH labels.
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink.
- Core must stay extension-agnostic. No core special cases for bundled plugin/provider/channel ids when manifest/registry/capability contracts can express it.
- Extensions cross into core only via `openclaw/plugin-sdk/*`, manifest metadata, injected runtime helpers, and documented local barrels (`api.ts`, `runtime-api.ts`).
- Extension production code must not import core `src/**`, `src/plugin-sdk-internal/**`, another extension's`src/**`, or relative paths outside its package.
- Core code/tests must not deep-import plugin internals (`extensions/*/src/**`, `onboard.js`). Use plugin `api.ts` / public SDK facade / generic contract.
- Extension-owned behavior stays in the extension: legacy repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Config contract: keep exported types, schema/help, generated metadata, baselines, docs aligned. Retired public keys stay retired; compatibility belongs in raw migration/doctor paths.
-Plugin architecture direction: manifest-first control plane; targeted runtime loaders; no hidden paths around declared contracts; broad mutable registries are transitional.
- Prompt-cache rule: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
- Channels: `src/channels/**` is implementation; plugin authors get SDK seams.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor.
-Direction: manifest-first control plane; targeted runtime loaders; no hidden contract bypasses; broad mutable registries transitional.
- Promptcache: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
## Commands
- Runtime: Node 22+. Keep Node and Bun paths working.
-Normal full prod sweep: `pnpm check` (prod typecheck/lint/guards, no tests)
-Full tests: `pnpm test`
-Changed tests only: `pnpm test:changed`
-Local serial loop: `pnpm test:serial`
-Extension tests: `pnpm test:extensions` or `pnpm test extensions` = all extension shards; `pnpm test extensions/<id>` = one extension lane. Heavy channels/OpenAI have dedicated shards.
- Shard timing artifact: `.artifacts/vitest-shard-timings.json`; auto-used for balanced shard ordering. Disable with `OPENCLAW_TEST_PROJECTS_TIMINGS=0`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; do not call raw `vitest`.
- Coverage: `pnpm test:coverage`
- Format check/fix: `pnpm format:check` / `pnpm format`
- Typecheck:
-`pnpm tsgo`: fastest core prod graph
-`pnpm tsgo:prod`: core + extensions prod graphs; used by `pnpm check`
-`pnpm check:test-types` / `pnpm tsgo:test`: all test graphs
-`pnpm tsgo:all`: all prod + test project refs
- Debug slices exist; do not present as normal user flow.
-`pnpm lint:apps`: Swift/app surface, separate from TS lint
-`pnpm lint:all`: legacy comparison lane
- Local heavy-check behavior: `OPENCLAW_LOCAL_CHECK=1` default; `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; `OPENCLAW_LOCAL_CHECK=0` for CI/shared runs.
- Local validation is local-first. Do not default to Blacksmith/Testbox for routine OpenClaw iteration; it burns warm caches and startup time. Use repo `pnpm` lanes first, then reach for remote CI/Testbox only for parity-only failures, secrets/services, or when explicitly requested.
- Runtime: Node 22+. Keep Node + Bun paths working.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- Sparse worktrees: `pnpm check:changed`is sparse-safe and may skip sparse-missing typecheck projects; do not expand sparse checkout just to satisfy changed-gate tsgo. Direct `pnpm tsgo*` remains strict; use a fuller worktree when you need direct typecheck proof.
-Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
-Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
## GitHub API
## GitHub / CI
-Issue/PR triage: list first, hydrate few. Use bounded fields + `--jq`, e.g. `gh issue list --state open --limit 80 --json number,title,labels,updatedAt,comments --jq '.[]|[.number,.updatedAt,.comments,.title]|@tsv'`; then `gh issue view <n> --json title,body,comments,labels,createdAt,updatedAt,url --jq '{title,labels,createdAt,updatedAt,url,body,comments:[.comments[]|{author:.author.login,createdAt,body}]}'` only for shortlisted items.
-After landing a PR: search for duplicate open issues/PRs that can be closed.
-Before closing an issue/PR: add a comment explaining why, usually duplicate/invalid, with the canonical issue/PR when relevant.
-PR links: `gh pr list --state open --search '<issue-or-terms>' --json number,title,updatedAt,headRefName --limit 20`; use `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision` only after shortlist.
-CI polling: keep full `gh` capability, but request only needed fields. Known run status: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
-Non-blocking background workflows: `Auto response`, `Docs Sync Publish Repo`, `Docs Agent`, and `Test Performance Agent` are service/agent work. Do not wait on, rerun, or fix them during normal push/PR verification unless the user explicitly asks or the task is about those workflows. Report them as background if mentioned.
-`/landpr` CI wait scope: do not idle on pending `auto-response`/`Auto response` or `check-docs`. Treat docs as local proof unless `check-docs` already failed with a relevant, actionable error. If required product/code gates and touched-surface local gates are green, proceed without waiting for docs-only or auto-response automation.
-Waiting: poll lightly, usually 30-60s backoff. Fetch jobs/logs/artifacts only after completion/failure or when job detail is needed; avoid repeated workflow + run + jobs loops.
-Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
-PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
-After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
-GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
-PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
-PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
-Post-land wait: minimal. Exact landed SHA only. If superseded on `main`, same-branch `cancel-in-progress` cancellations are expected; stop once local touched-surface proof exists. Never wait for newer unrelated `main` unless asked.
- explicit/surface only: `QA-Lab - All Lanes`, `Scheduled Live And E2E`, `Install Smoke`, `CodeQL`, `Sandbox Common Smoke`, `Parity gate`, `Blacksmith Testbox`, `Control UI Locale Refresh`.
-`/landpr`: do not idle on `auto-response` or `check-docs`. Treat docs as local proof unless `check-docs` already failed with actionable relevant error.
- Poll 30-60s. Fetch jobs/logs/artifacts only after failure/completion or concrete need.
## Gates
- Pre-commit hook: staged formatting only. It does not run lint, typecheck, or tests.
- extension tests => extension test typecheck/tests only
- public SDK/plugin contract => extension prod/test validation too
- unknown root/config => all lanes
-Local loop: run `pnpm check:changed` explicitly before handoff/push; use`pnpm test:changed` for testsonly; use`pnpm check` for full prod TS/lint sweep without tests.
- Landing on `main`: verify touched surface near landing; default bar is`pnpm check` + `pnpm test` when feasible.
- Hard build gate: run/pass`pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
- Do not land related failing format/lint/type/build/tests. If failures are unrelated on latest `origin/main`, say so and give scoped proof.
-Commit helper is formatting-only; validation gates are explicit commands, not commit side effects.
- CI architecture gate: `check-additional`; local equivalent `pnpm check:architecture`.
- Config docs drift: `pnpm config:docs:gen/check`
- Plugin SDK API drift: `pnpm plugin-sdk:api:gen/check`
- Generated docs baselines: tracked `docs/.generated/*.sha256`; full JSON ignored.
- Dynamic import: do not mix static and dynamic import for same module in prod path. Use dedicated`*.runtime.ts` lazy boundary. After lazy-boundary edits, run`pnpm build` and check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Runtime branching: discriminated unions/closed codes over freeform strings.
- Avoid semantic sentinels:`?? 0`, empty object/string, etc.
- Dynamic import: no static+dynamic import for same prod module. Use`*.runtime.ts` lazy boundary. After edits:`pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
-Update docs when behavior/API changes. Use docs list/read_when hints.
-Docs links: see `docs/AGENTS.md`.
- Changelog: user-facing only. Pure test/internal changes usually no entry.
- Changelog placement: append to active version `### Changes`/`### Fixes`; at most one contributor mention, prefer `Thanks @user`.
-Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
-Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s).
## Git
-Use`scripts/committer "<msg>" <file...>`; stage only intended files. It formats staged files only; run validation separately.
- Commits: conventional-ish, concise/action-oriented. Group related changes.
- No manual stash/autostash unless explicitly requested. No branch/worktree changes unless requested.
-No merge commits on `main`; rebase on latest `origin/main` before push.
- User says "commit": commit your changes only. "commit all": commit everything in grouped chunks. "push": may `git pull --rebase` first.
- Do not delete/rename unexpected files; ask if it blocks. Otherwise ignore unrelated WIP.
- Mobile LAN pairing: plaintext `ws://` is loopback-only by default. Trusted private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or a tunnel.
- SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
-Mac gateway: use app or `openclaw gateway restart/status --deep`; no ad-hoc tmux gateway. Logs: `./scripts/clawlog.sh`.
-Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
-Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
- Provider-facing tool schemas: prefer flat string enum helpers over `Type.Union([Type.Literal(...)])`; some providers reject generated `anyOf`. Do not treat this as a repo-wide protocol/schema ban.
- External messaging surfaces: no token-delta channel messages. Follow `docs/concepts/streaming.md`; preview/block streaming uses message edits/chunks and must preserve final/fallback delivery.
- Provider tool schemas: prefer flat string enum helpers over `Type.Union([Type.Literal(...)])`; some providers reject `anyOf`. Not a repo-wide protocol/schema ban.
- External messaging: no token-delta channel messages. Follow `docs/concepts/streaming.md`; preview/block streaming uses edits/chunks and preserves final/fallback delivery.
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
openclaw message send --target +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/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/QQ/WebChat)
openclaw agent --message "Ship checklist" --thinking high
@@ -53,12 +53,24 @@ We prioritize secure defaults, but also expose clear knobs for trusted high-powe
OpenClaw has an extensive plugin API.
Core stays lean; optional capability should usually ship as plugins.
We are generally slimming down core while expanding what plugins can do.
If a useful feature cannot be built as a plugin yet, we welcome PRs and design discussions that extend the plugin API instead of adding one-off core behavior.
There are two broad plugin styles:
- Code plugins run OpenClaw plugin code and are appropriate for deeper runtime extension.
- Bundle-style plugins package stable external surfaces such as skills, MCP servers, and related configuration.
Prefer bundle-style plugins when they can express the capability.
They have a smaller, more stable interface and better security boundaries.
Use code plugins when the capability needs runtime hooks, providers, channels, tools, or other in-process extension points.
Preferred plugin path is npm package distribution plus local extension loading for development.
If you build a plugin, host and maintain it in your own repository.
The bar for adding optional plugins to core is intentionally high.
Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.
## Quick start
@@ -88,6 +86,8 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
@@ -96,6 +96,11 @@ When isolated cron runs orchestrate subagents, delivery also prefers the final
descendant output over stale parent interim text. If descendants are still
running, OpenClaw suppresses that partial parent update instead of announcing it.
For text-only Discord announce targets, OpenClaw sends the canonical final
assistant text once instead of replaying both streamed/intermediate text payloads
and the final answer. Media and structured Discord payloads are still delivered
as separate payloads so attachments and components are not dropped.
### Payload options for isolated jobs
-`--message`: prompt text (required for isolated)
@@ -113,7 +118,7 @@ Model-selection precedence for isolated jobs is:
1. Gmail hook model override (when the run came from Gmail and that override is allowed)
2. Per-job payload `model`
3.Stored cron session model override
3.User-selected stored cron session model override
4. Agent/default model selection
Fast mode follows the resolved live selection too. If the selected model config
@@ -121,10 +126,11 @@ has `params.fastMode`, isolated cron uses that by default. A stored session
`fastMode` override still wins over config in either direction.
If an isolated run hits a live model-switch handoff, cron retries with the
switched provider/model and persists that live selection before retrying. When
the switch also carries a new auth profile, cron persists that auth profile
override too. Retries are bounded: after the initial attempt plus 2 switch
retries, cron aborts instead of looping forever.
switched provider/model and persists that live selection for the active run
before retrying. When the switch also carries a new auth profile, cron persists
that auth profile override for the active run too. Retries are bounded: after
the initial attempt plus 2 switch retries, cron aborts instead of looping
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data).
Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (push to send to user), and `context` (event-specific data). Agent and tool plugin hook contexts can also include `trace`, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation.
### Event context highlights
@@ -205,9 +205,12 @@ Runs `BOOT.md` from the active workspace when the gateway starts.
## Plugin hooks
Plugins can register hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. The Plugin SDK exposes 28 hooks covering model resolution, agent lifecycle, message flow, tool execution, subagent coordination, and gateway lifecycle.
Plugins can register typed hooks through the Plugin SDK for deeper integration:
intercepting tool calls, modifying prompts, controlling message flow, and more.
Use plugin hooks when you need `before_tool_call`, `before_agent_reply`,
`before_install`, or other in-process lifecycle hooks.
For the complete plugin hook reference including `before_tool_call`, `before_agent_reply`, `before_install`, and all other plugin hooks, see [Plugin Architecture](/plugins/architecture-internals#provider-runtime-hooks).
For the complete plugin hook reference, see [Plugin hooks](/plugins/hooks).
## Configuration
@@ -315,5 +318,5 @@ Check for missing binaries (PATH), environment variables, config values, or OS c
- [CLI Reference: hooks](/cli/hooks)
- [Webhooks](/automation/cron-jobs#webhooks)
- [Plugin Architecture](/plugins/architecture-internals#provider-runtime-hooks) — full plugin hook reference
| Audit what ran and when | Background Tasks | `openclaw tasks list` and `openclaw tasks audit` |
| Multi-step research then summarize | Task Flow | Durable orchestration with revision tracking |
| Run a script on session reset | Hooks | Event-driven, fires on lifecycle events |
| Execute code on every tool call | Hooks | Hooks can filter by event type |
| Execute code on every tool call | Plugin hooks | In-process hooks can intercept tool calls |
| Always check compliance before replying | Standing Orders | Injected into every session automatically |
### Scheduled Tasks (Cron) vs Heartbeat
@@ -83,7 +83,11 @@ See [Standing Orders](/automation/standing-orders).
### Hooks
Hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, message flow, and tool calls. Hooks are automatically discovered from directories and can be managed with `openclaw hooks`.
Internal hooks are event-driven scripts triggered by agent lifecycle events
(`/new`, `/reset`, `/stop`), session compaction, gateway startup, and message
flow. They are automatically discovered from directories and can be managed
with `openclaw hooks`. For in-process tool-call interception, use
[Plugin hooks](/plugins/hooks).
See [Hooks](/automation/hooks).
@@ -97,7 +101,7 @@ See [Heartbeat](/gateway/heartbeat).
- **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. All cron executions create task records.
- **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
- **Hooks** react to specific events (tool calls, session resets, compaction) with custom scripts.
- **Hooks** react to specific events (session resets, compaction, message flow) with custom scripts. Plugin hooks cover tool calls.
- **Standing orders** give the agent persistent context and authority boundaries.
@@ -27,7 +27,7 @@ This is the difference between telling your assistant "send the weekly report" e
- You only get involved for exceptions and approvals
- The agent fills idle time productively
## How They Work
## How they work
Standing orders are defined in your [agent workspace](/concepts/agent-workspace) files. The recommended approach is to include them directly in `AGENTS.md` (which is auto-injected every session) so the agent always has them in context. For larger configurations, you can also place them in a dedicated file like `standing-orders.md` and reference it from `AGENTS.md`.
@@ -198,8 +198,6 @@ This pattern prevents the most common agent failure mode: acknowledging a task w
For agents managing multiple concerns, organize standing orders as separate programs with clear boundaries:
@@ -20,6 +20,78 @@ Use Task Flow when work spans multiple sequential or branching steps and you nee
| Observe externally created tasks | Task Flow (mirrored) |
| One-shot reminder | Cron job |
## Reliable scheduled workflow pattern
For recurring workflows such as market intelligence briefings, treat the schedule, orchestration, and reliability checks as separate layers:
1. Use [Scheduled Tasks](/automation/cron-jobs) for timing.
2. Use a persistent cron session when the workflow should build on prior context.
3. Use [Lobster](/tools/lobster) for deterministic steps, approval gates, and resume tokens.
4. Use Task Flow to track the multi-step run across child tasks, waits, retries, and gateway restarts.
Example cron shape:
```bash
openclaw cron add \
--name "Market intelligence brief"\
--cron "0 7 * * 1-5"\
--tz "America/New_York"\
--session session:market-intel \
--message "Run the market-intel Lobster workflow. Verify source freshness before summarizing."\
--announce \
--channel slack \
--to "channel:C1234567890"
```
Use `session:<id>` instead of `isolated` when the recurring workflow needs deliberate history, previous run summaries, or standing context. Use `isolated` when each run should start fresh and all required state is explicit in the workflow.
Inside the workflow, put reliability checks before the LLM summary step:
```yaml
name:market-intel-brief
steps:
- id:preflight
command:market-intel check --json
- id:collect
command:market-intel collect --json
stdin:$preflight.json
- id:summarize
command:market-intel summarize --json
stdin:$collect.json
- id:approve
command:market-intel deliver --preview
stdin:$summarize.json
approval:required
- id:deliver
command:market-intel deliver --execute
stdin:$summarize.json
condition:$approve.approved
```
Recommended preflight checks:
- Browser availability and profile choice, for example `openclaw` for managed state or `user` when a signed-in Chrome session is required. See [Browser](/tools/browser).
- API credentials and quota for each source.
- Network reachability for required endpoints.
- Required tools enabled for the agent, such as `lobster`, `browser`, and `llm-task`.
- Failure destination configured for cron so preflight failures are visible. See [Scheduled Tasks](/automation/cron-jobs#delivery-and-output).
Recommended data provenance fields for every collected item:
```json
{
"sourceUrl":"https://example.com/report",
"retrievedAt":"2026-04-24T12:00:00Z",
"asOf":"2026-04-24",
"title":"Example report",
"content":"..."
}
```
Have the workflow reject or mark stale items before summarization. The LLM step should receive only structured JSON and should be asked to preserve `sourceUrl`, `retrievedAt`, and `asOf` in its output. Use [LLM Task](/tools/llm-task) when you need a schema-validated model step inside the workflow.
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/tools/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
@@ -267,6 +267,9 @@ Now create some channels on your Discord server and start chatting. Your agent c
- Guild channels are isolated session keys (`agent:<agentId>:discord:channel:<channelId>`).
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
- Native slash commands run in isolated command sessions (`agent:<agentId>:discord:slash:<userId>`), while still carrying `CommandTargetSessionKey` to the routed conversation session.
- Text-only cron/heartbeat announce delivery to Discord uses the final
assistant-visible answer once. Media and structured component payloads remain
multi-message when the agent emits multiple deliverable payloads.
## Forum channels
@@ -305,7 +308,7 @@ By default, components are single use. Set `components.reusable=true` to allow b
To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial.
The `/model` and `/models` slash commands open an interactive model picker with provider and model dropdowns plus a Submit step. Unless `commands.modelsWrite=false`, `/models add` also supports adding a new provider/model entry from chat, and newly added models show up without restarting the gateway. The picker reply is ephemeral and only the invoking user can use it.
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it.
File attachments:
@@ -958,14 +961,23 @@ Discord has two distinct voice surfaces: realtime **voice channels** (continuous
### Voice channels
Requirements:
Setup checklist:
- Enable native commands (`commands.native` or `channels.discord.commands.native`).
- Configure `channels.discord.voice`.
- The bot needs Connect + Speak permissions in the target voice channel.
1. Enable Message Content Intent in the Discord Developer Portal.
2. Enable Server Members Intent when role/user allowlists are used.
3. Invite the bot with `bot` and `applications.commands` scopes.
4. Grant Connect, Speak, Send Messages, and Read Message History in the target voice channel.
5. Enable native commands (`commands.native` or `channels.discord.commands.native`).
6. Configure `channels.discord.voice`.
Use `/vc join|leave|status` to control sessions. The command uses the account default agent and follows the same allowlist and group policy rules as other Discord commands.
```bash
/vc join channel:<voice-channel-id>
/vc status
/vc leave
```
Auto-join example:
```json5
@@ -974,6 +986,7 @@ Auto-join example:
discord: {
voice: {
enabled: true,
model: "openai/gpt-5.4-mini",
autoJoin: [
{
guildId: "123456789012345678",
@@ -984,7 +997,7 @@ Auto-join example:
decryptionFailureTolerance: 24,
tts: {
provider: "openai",
openai: { voice: "alloy" },
openai: { voice: "onyx" },
},
},
},
@@ -995,12 +1008,24 @@ Auto-join example:
Notes:
-`voice.tts` overrides `messages.tts` for voice playback only.
-`voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model.
- STT uses `tools.media.audio`; `voice.model` does not affect transcription.
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
- Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable it.
-`voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options.
-`@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)`, this may be the upstream `@discordjs/voice` receive bug tracked in [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419).
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.jsissue#11419.
Voice channel pipeline:
- Discord PCM capture is converted to a WAV temp file.
-`tools.media.audio` handles STT, for example `openai/gpt-4o-mini-transcribe`.
- The transcript is sent through normal Discord ingress and routing.
-`voice.model`, when set, overrides only the response LLM for this voice-channel turn.
-`voice.tts` is merged over `messages.tts`; the resulting audio is played in the joined channel.
Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, and TTS auth for `messages.tts`/`voice.tts`.
- if failures continue after automatic rejoin, collect logs and compare against [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419)
- if failures continue after automatic rejoin, collect logs and compare against the upstream DAVE receive history in [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419) and [discord.js #11449](https://github.com/discordjs/discord.js/pull/11449)
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway.
Text is supported everywhere; media and reactions vary by channel.
## Delivery notes
- Telegram replies that contain markdown image syntax, such as ``,
are converted into media replies on the final outbound path when possible.
- Slack multi-person DMs route as group chats, so group policy, mention
behavior, and group-session rules apply to MPIM conversations.
- WhatsApp setup is install-on-demand: onboarding can show the setup flow before
Baileys runtime dependencies are staged, and the Gateway loads the WhatsApp
runtime only when the channel is actually active.
## Supported channels
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (bundled plugin; edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
For lower-level or inbound verification requests, use:
```bash
openclaw matrix verify accept <id>
openclaw matrix verify start <id>
openclaw matrix verify sas <id>
openclaw matrix verify confirm-sas <id>
```
Use `openclaw matrix verify cancel <id>` to cancel a request.
Verbose restore diagnostics:
```bash
openclaw matrix verify backup restore --verbose
```
Delete the current server backup and create a fresh backup baseline. If the stored
backup key cannot be loaded cleanly, this reset can also recreate secret storage so
future cold starts can load the new backup key:
```bash
openclaw matrix verify backup reset --yes
```
All `verify` commands are concise by default (including quiet internal SDK logging) and show detailed diagnostics only with `--verbose`.
Use `--json` for full machine-readable output when scripting.
In multi-account setups, Matrix CLI commands use the implicit Matrix default account unless you pass `--account <id>`.
If you configure multiple named accounts, set `channels.matrix.defaultAccount` first or those implicit CLI operations will stop and ask you to choose an account explicitly.
@@ -341,7 +452,9 @@ When encryption is disabled or unavailable for a named account, Matrix warnings
-`Cross-signing verified`: the SDK reports verification via cross-signing
-`Signed by owner`: signed by your own self-signing key
`Verified by owner` becomes `yes` only when cross-signing or owner-signing is present. Local trust alone is not enough.
`Verified by owner` becomes `yes` only when cross-signing verification is present.
Local trust or an owner signature by itself is not enough for OpenClaw to treat
- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
-`channels.msteams.allowFrom` should use stable AAD object IDs.
- UPNs/displaynames are mutable; direct matching is disabled by default and only enabled with `channels.msteams.dangerouslyAllowNameMatching: true`.
- Do not rely on UPN/display-name matching for allowlists — they can change. OpenClaw disables direct name matching by default; opt in explicitly with `channels.msteams.dangerouslyAllowNameMatching: true`.
- The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
## Prerequisites
@@ -210,6 +208,7 @@ Groups:
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Attachments supported (base64 fetched from `signal-cli`).
- Voice-note attachments use the `signal-cli` filename as a MIME fallback when `contentType` is missing, so audio transcription can still classify AAC voice memos.
- Default media cap: `channels.signal.mediaMaxMb` (default 8).
- Use `channels.signal.ignoreAttachments` to skip downloading media.
- Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
@@ -436,7 +436,7 @@ Available action groups in current Slack tooling:
| memberInfo | enabled |
| emojiList | enabled |
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`.
Current Slack message actions include `send`, `upload-file`, `download-file`, `read`, `edit`, `delete`, `pin`, `unpin`, `list-pins`, `member-info`, and `emoji-list`.`download-file` accepts Slack file IDs shown in inbound file placeholders and returns image previews for images or local file metadata for other file types.
## Access control and routing
@@ -606,7 +606,7 @@ Notes:
<AccordionGroup>
<Accordiontitle="Inbound attachments">
Slack file attachments are downloaded from Slack-hosted private URLs (token-authenticated request flow) and written to the media store when fetch succeeds and size limits permit.
Slack file attachments are downloaded from Slack-hosted private URLs (token-authenticated request flow) and written to the media store when fetch succeeds and size limits permit. File placeholders include the Slack `fileId` so agents can fetch the original file with `download-file`.
Runtime inbound size cap defaults to `20MB` unless overridden by `channels.slack.mediaMaxMb`.
@@ -773,7 +773,8 @@ Same-chat `/approve` also works in Slack channels and DMs that already support c
## Events and operational behavior
- Message edits/deletes/thread broadcasts are mapped into system events.
- Message edits/deletes are mapped into system events.
- Thread broadcasts ("Also send to channel" thread replies) are processed as normal user messages.
- Reaction add/remove events are mapped into system events.
- Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events.
-`channel_id_changed` can migrate channel config keys when `configWrites` is enabled.
- Group sessions are isolated by group ID. Forum topics append `:topic:<threadId>` to keep topics isolated.
- DM messages can carry `message_thread_id`; OpenClaw routes them with thread-aware session keys and preserves thread ID for replies.
- Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses `agents.defaults.maxConcurrent`.
- Long polling is guarded inside each gateway process so only one active poller can use a bot token at a time. If you still see `getUpdates` 409 conflicts, another OpenClaw gateway, script, or external poller is likely using the same token.
- Long-polling watchdog restarts trigger after 120 seconds without completed `getUpdates` liveness by default. Increase `channels.telegram.pollingStallThresholdMs` only if your deployment still sees false polling-stall restarts during long-running work. The value is in milliseconds and is allowed from `30000` to `600000`; per-account overrides are supported.
- Telegram Bot API has no read-receipt support (`sendReadReceipts` does not apply).
-`progress` maps to `partial` on Telegram (compat with cross-channel naming)
-`streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true`). Set `false` to keep separate tool/progress messages.
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
-`streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
Tool-progress preview updates are the short "Working..." lines shown while tools run, for example command execution, file reads, planning updates, or patch summaries. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later. To keep the edited preview for answer text but hide tool-progress lines, set:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": {
"toolProgress": false
}
}
}
}
}
```
Use `streaming.mode: "off"` only when you want to disable Telegram preview edits entirely. Use `streaming.preview.toolProgress: false` when you only want to disable the tool-progress status lines.
The local listener binds to `127.0.0.1:8787`. For public ingress, either put a reverse proxy in front of the local port or set `webhookHost: "0.0.0.0"` intentionally.
Webhook mode validates request guards, the Telegram secret token, and the JSON body before returning `200` to Telegram.
OpenClaw then processes the update asynchronously through the same per-chat/per-topic bot lanes used by long polling, so slow agent turns do not hold Telegram's delivery ACK.
</Accordion>
<Accordion title="Limits, retry, and CLI targets">
openclaw channels login --channel whatsapp --account work
```
To attach an existing/custom WhatsApp Web auth directory before login:
```bash
openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth
openclaw channels login --channel whatsapp --account work
```
</Step>
@@ -145,6 +152,46 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
## Plugin hooks and privacy
WhatsApp inbound messages can contain personal message content, phone numbers,
group identifiers, sender names, and session correlation fields. For that reason,
WhatsApp does not broadcast inbound `message_received` hook payloads to plugins
unless you explicitly opt in:
```json5
{
channels: {
whatsapp: {
pluginHooks: {
messageReceived: true,
},
},
},
}
```
You can scope the opt-in to one account:
```json5
{
channels: {
whatsapp: {
accounts: {
work: {
pluginHooks: {
messageReceived: true,
},
},
},
},
},
}
```
Only enable this for plugins you trust to receive inbound WhatsApp message
content and identifiers.
## Access control and activation
<Tabs>
@@ -164,7 +211,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- pairings are persisted in channel allow-store and merged with configured `allowFrom`
- if no allowlist is configured, the linked self number is allowed by default
- outbound `fromMe` DMs are never auto-paired
- OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send to yourself from the linked device)
</Tab>
@@ -314,9 +361,11 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
<Accordion title="Outbound media behavior">
- supports image, video, audio (PTT voice-note), and document payloads
- `audio/ogg` is rewritten to `audio/ogg; codecs=opus` for voice-note compatibility
- reply payloads preserve `audioAsVoice`; WhatsApp sends audio media as Baileys PTT voicenotes
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded to Ogg/Opus before PTT delivery
- native Ogg/Opus audio is sent with `audio/ogg; codecs=opus` for voice-note compatibility
- animated GIF playback is supported via `gifPlayback: true` on video sends
- captions are applied to the first media item when sending multi-media reply payloads
- captions are applied to the first media item when sending multi-media reply payloads, except PTT voice notes send the audio first and visible text separately because WhatsApp clients do not render voice-note captions consistently
- media source can be HTTP(S), `file://`, or local paths
</Accordion>
@@ -333,19 +382,20 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`.
Default is `"auto"`. Per-account overrides use `channels.whatsapp.accounts.<id>.replyToMode`.
Default is `"off"`. Per-account overrides use `channels.whatsapp.accounts.<id>.replyToMode`.
```json5
{
channels: {
whatsapp: {
replyToMode: "on",
replyToMode: "first",
},
},
}
@@ -493,15 +543,15 @@ Resolution hierarchy for group messages:
The effective `groups` map is determined first: if the account defines its own `groups`, it fully replaces the root `groups` map (no deep merge). Prompt lookup then runs on the resulting single map:
1. **Group-specific system prompt** (`groups["<groupId>"].systemPrompt`): used if the specific group entry defines a `systemPrompt`.
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent or defines no `systemPrompt`.
1. **Group-specific system prompt** (`groups["<groupId>"].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied.
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
Resolution hierarchy for direct messages:
The effective `direct` map is determined first: if the account defines its own `direct`, it fully replaces the root `direct` map (no deep merge). Prompt lookup then runs on the resulting single map:
1. **Direct-specific system prompt** (`direct["<peerId>"].systemPrompt`): used if the specific peer entry defines a `systemPrompt`.
2. **Direct wildcard system prompt** (`direct["*"].systemPrompt`): used when the specific peer entry is absent or defines no `systemPrompt`.
1. **Direct-specific system prompt** (`direct["<peerId>"].systemPrompt`): used when the specific peer entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`), the wildcard is suppressed and no system prompt is applied.
2. **Direct wildcard system prompt** (`direct["*"].systemPrompt`): used when the specific peer entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
Note: `dms` remains the lightweight per-DM history override bucket (`dms.<id>.historyLimit`); prompt overrides live under `direct`.
@@ -10,7 +10,7 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
QA Lab has dedicated CI lanes outside the main smart-scoped workflow. The
`Parity gate` workflow runs on matching PR changes and manual dispatch; it
builds the private QA runtime and compares the mock GPT-5.4 and Opus 4.6
builds the private QA runtime and compares the mock GPT-5.5 and Opus 4.6
agentic packs. The `QA-Lab - All Lanes` workflow runs nightly on `main` and on
manual dispatch; it fans out the mock parity gate, live Matrix lane, and live
Telegram lane as parallel jobs. The live jobs use the `qa-live-shared`
@@ -79,7 +79,7 @@ gh workflow run duplicate-after-merge.yml \
| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes |
| `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
## Fail-Fast Order
## Fail-fast order
Jobs are ordered so cheap checks fail before expensive ones run:
@@ -90,14 +90,15 @@ Jobs are ordered so cheap checks fail before expensive ones run:
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits use a fast Node-only manifest path: preflight, security, and a single `checks-fast-core` task. That path avoids build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the changed files are limited to the routing or helper surfaces that the fast task exercises directly.
Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 120-second command timeout. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with`OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes with a weighted scheduler and`OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks.
Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes.
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run plugin config groups serially with one Vitest worker and a larger Node heap so import-heavy plugin batches do not overcommit small CI runners. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest`, then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles that flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
`extension-fast` is PR-only because push runs already execute the full bundled plugin shards. That keeps changed-plugin feedback for reviews without reserving an extra Blacksmith worker on `main` for coverage already present in `checks-node-extensions`.
@@ -136,3 +137,8 @@ node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main C
- Gateway mode falls back to the embedded agent when the Gateway request fails. Use `--local` to force embedded execution up front.
-`--local` still preloads the plugin registry first, so plugin-provided providers, tools, and channels stay available during embedded runs.
- Each `openclaw agent` invocation is treated as a one-shot run. Bundled or user-configured MCP servers opened for that run are retired after the reply, even when the command uses the Gateway path, so stdio MCP child processes do not stay alive between scripted invocations.
-`--channel`, `--reply-channel`, and `--reply-account` affect reply delivery, not session routing.
-`--json` keeps stdout reserved for the JSON response. Gateway, plugin, and embedded-fallback diagnostics are routed to stderr so scripts can parse stdout directly.
- When this command triggers `models.json` regeneration, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names, `secretref-env:ENV_VAR_NAME`, or `secretref-managed`), not resolved secret plaintext.
- Marker writes are source-authoritative: OpenClaw persists markers from the active source config snapshot, not from resolved runtime secret values.
Agents can run the same readiness check with `browser({ action: "doctor" })`.
## Quick troubleshooting
If `start` fails with `not reachable after start`, troubleshoot CDP readiness first. If `start` and `tabs` succeed but `open` or `navigate` fails, the browser control plane is healthy and the failure is usually navigation SSRF policy.
@@ -40,6 +42,7 @@ If `start` fails with `not reachable after start`, troubleshoot CDP readiness fi
Minimal sequence:
```bash
openclaw browser --browser-profile openclaw doctor
openclaw browser --browser-profile openclaw start
openclaw browser --browser-profile openclaw tabs
openclaw browser --browser-profile openclaw open https://example.com
Whole-file recovery is reserved for globally broken config, such as parse
errors, root-level schema failures, legacy migration failures, or mixed plugin
and root failures. If validation fails only under `plugins.entries.<id>...`,
OpenClaw keeps the active `openclaw.json` in place and reports the plugin-local
issue instead of restoring `.last-good`. This prevents plugin schema changes or
`minHostVersion` skew from rolling back unrelated user settings such as models,
providers, auth profiles, channels, gateway exposure, tools, memory, browser, or
cron config.
## Subcommands
-`config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location). The path should name a regular file, not a symlink.
`https://` Control UI URLs and connect over `wss://`.
- For SecretRef-managed tokens (resolved or unresolved), `dashboard` prints/copies/opens a non-tokenized URL to avoid exposing external secrets in terminal output, clipboard history, or browser-launch arguments.
- If `gateway.auth.token` is SecretRef-managed but unresolved in this command path, the command prints a non-tokenized URL and explicit remediation guidance instead of embedding an invalid token placeholder.
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.