- 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>
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.
-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).
-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 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]`.
- 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.
@@ -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.
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.
-`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.
-`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 auto-mapped
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.
For text-only replies:
- DM: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
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,6 +361,7 @@ 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
- reply payloads preserve `audioAsVoice`; WhatsApp sends audio media as Baileys PTT voice notes
- `audio/ogg` is rewritten to `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
@@ -333,19 +381,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 +542,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`.
@@ -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.
-`--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.
-`dashboard` resolves configured `gateway.auth.token` SecretRefs when possible.
- 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.
-`--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
- State integrity checks now detect orphan transcript files in the sessions directory and can archive them as `.deleted.<timestamp>` to reclaim space safely.
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
- Doctor repairs missing bundled plugin runtime dependencies without requiring write access to the installed OpenClaw package. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`.
- Doctor repairs missing bundled plugin runtime dependencies without writing into packaged global installs. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`.
- Doctor auto-migrates legacy flat Talk config (`talk.voiceId`, `talk.modelId`, and friends) into `talk.provider` + `talk.providers.<provider>`.
- Repeat `doctor --fix` runs no longer report/apply Talk normalization when the only difference is object key order.
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
- Use `image edit` when starting from existing input files.
- Use `image providers --json` to verify which bundled image providers are
discoverable, configured, selected, and which generation/edit capabilities
each provider exposes.
- Use `image generate --model <provider/model> --json` as the narrowest live
CLI smoke for image generation changes. Example:
```bash
openclaw infer image providers --json
openclaw infer image generate \
--model google/gemini-3.1-flash-image-preview \
--prompt "Minimal flat test image: one blue square on a white background, no text." \
--output ./openclaw-infer-image-smoke.png \
--json
```
The JSON response reports `ok`, `provider`, `model`, `attempts`, and written
output paths. When `--output` is set, the final extension may follow the
provider's returned MIME type.
- For `image describe`, `--model` must be an image-capable `<provider/model>`.
- For local Ollama vision models, pull the model first and set `OLLAMA_API_KEY` to any placeholder value, for example `ollama-local`. See [Ollama](/providers/ollama#vision-and-image-description).
@@ -258,6 +282,10 @@ Top-level fields are stable:
- `outputs`
- `error`
For generated media commands, `outputs` contains files written by OpenClaw. Use
the `path`, `mimeType`, `size`, and any media-specific dimensions in that array
for automation instead of parsing human-readable stdout.
- Use `--local-time` to render timestamps in your local timezone.
- If the local loopback Gateway asks for pairing, `openclaw logs` falls back to the configured local log file automatically. Explicit `--url` targets do not use this fallback.
-`memory rem-backfill --path <file-or-dir> --stage-short-term` also seeds grounded durable candidates into the live short-term promotion store so the normal deep phase can rank them.
-`memory rem-backfill --rollback` removes previously written grounded diary entries, and `memory rem-backfill --rollback-short-term` removes previously staged grounded short-term candidates.
- See [Dreaming](/concepts/dreaming) for full phase descriptions and configuration reference.
For plaintext private-network `ws://` targets (trusted networks only), set
`OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` in the onboarding process environment.
There is no `openclaw.json` equivalent for this client-side transport
break-glass.
Non-interactive custom provider:
@@ -113,6 +116,7 @@ Non-interactive local gateway health:
- Unless you pass `--skip-health`, onboarding waits for a reachable local gateway before it exits successfully.
-`--install-daemon` starts the managed gateway install path first. Without it, you must already have a local gateway running, for example `openclaw gateway run`.
- If you only want config/workspace/bootstrap writes in automation, use `--skip-health`.
- If you manage workspace files yourself, pass `--skip-bootstrap` to set `agents.defaults.skipBootstrap: true` and skip creating `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`.
- On native Windows, `--install-daemon` tries Scheduled Tasks first and falls back to a per-user Startup-folder login item if task creation is denied.
Interactive onboarding behavior with reference mode:
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.