Compare commits

..

1 Commits

Author SHA1 Message Date
Tak Hoffman
91625aa9f3 feat capability CLI on latest main 2026-04-06 17:30:59 -05:00
3398 changed files with 42908 additions and 93112 deletions

View File

@@ -16,7 +16,6 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Pass `--json` for machine-readable summaries.
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
- Do not run multiple smoke lanes against the same guest family at once. Tahoe lanes share the host HTTP port, and Windows/Linux lanes can collide on snapshot restore/start state if two jobs touch the same VM concurrently.
- If `main` is moving under active multi-agent work, prefer a detached worktree pinned to one commit for long Parallels suites. The smoke scripts now verify the packed tgz commit instead of live `git rev-parse HEAD`, but a pinned worktree still avoids noisy rebuild/version drift during reruns.
- For `openclaw update --channel dev` lanes, remember the guest clones GitHub `main`, not your local worktree. If a local fix exists but the rerun still fails inside the cloned dev checkout, do not treat that as disproof of the fix until the branch has been pushed.
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
@@ -34,8 +33,6 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
- In those Windows same-guest update checks, do not treat one nonzero `openclaw gateway restart` as definitive failure. Current login-item restarts can report failure before the background service becomes observable again; follow with a longer RPC-ready wait and use `gateway start` only as a recovery step if readiness still never returns.
- After that Windows restart, do not trust one `gateway status --deep --require-rpc` call after a fixed sleep. Retry the RPC-ready probe for roughly 30 seconds and log each attempt; current guests can keep port `18789` bound while the fresh RPC endpoint is still coming up.
- For Windows same-guest update checks, prefer the done-file/log-drain PowerShell runner pattern over one long-lived `prlctl exec ... powershell -EncodedCommand ...` transport. The guest can finish successfully while the outer `prlctl exec` still hangs.
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials.
@@ -59,8 +56,6 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
- For Tahoe `fresh.gateway-status`, prefer non-TTY `prlctl exec --current-user ... openclaw gateway status ...` plus a few short retries. `prlctl enter` can spam TTY control bytes and hang the phase log even when the CLI itself is healthy.
- If a Tahoe lane times out in `fresh.first-agent-turn` and the phase log stops right after `__OPENCLAW_RC__:0` from `models set`, suspect the `prlctl enter` / `expect` wrapper before blaming auth or the model lane. That pattern means the first guest command finished but the transport never released for the next `guest_current_user_cli` call.
- If a packaged install regresses with `500` on `/`, `/healthz`, or `__openclaw/control-ui-config.json` after `fresh.install-main` or `upgrade.install-main`, suspect bundled plugin runtime deps resolving from the package root `node_modules` rather than `dist/extensions/*/node_modules`. Repro quickly with a real `npm pack`/global install lane before blaming dashboard auth or Safari.
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
- Multi-word `openclaw agent --message ...` checks should go through a guest shell wrapper (`guest_current_user_sh` / `guest_current_user_cli` or `/bin/sh -lc ...`), not raw `prlctl exec ... node openclaw.mjs ...`, or the message can be split into extra argv tokens and Commander reports `too many arguments for 'agent'`.
@@ -91,7 +86,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Fresh Windows tgz install phases should also use the background PowerShell runner plus done-file/log-drain pattern; do not rely on one long-lived `prlctl exec ... powershell ... npm install -g` transport for package installs.
- Windows release-to-dev helpers should log `where pnpm` before and after the update and require `where pnpm` to succeed post-update. That proves the updater installed or enabled `pnpm` itself instead of depending on a smoke-only bootstrap.
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
- Fresh Windows daemon-health reachability should use `openclaw gateway probe --json` with a longer timeout and treat `ok: true` as success; full `gateway status --require-rpc` checks are too eager during initial startup on current main.
- Fresh Windows daemon-health reachability should use a hello-only gateway probe and a longer per-probe timeout than the default local attach path; full health RPCs are too eager during initial startup on current main.
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.
- The standalone Windows upgrade smoke lane should stop the managed gateway after `upgrade.install-main` and before `upgrade.onboard-ref`. Restarting before onboard can leave the old process alive on the pre-onboard token while onboard rewrites `~/.openclaw/openclaw.json`, which then fails `gateway-health` with `unauthorized: gateway token mismatch`.
- If standalone Windows upgrade fails with a gateway token mismatch but `pnpm test:parallels:npm-update` passes, trust the mismatch as a standalone ref-onboard ordering bug first; the npm-update helper does not re-run ref-mode onboard on the same guest.

View File

@@ -39,6 +39,7 @@ pnpm openclaw qa suite \
--provider-mode live-openai \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast \
--output-dir .artifacts/qa-e2e/run-all-live-openai-<tag>
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -753,11 +753,6 @@ jobs:
continue-on-error: true
run: pnpm run lint:extensions:bundled
- name: Run extension package boundary TypeScript check
id: extension_package_boundary_tsc
continue-on-error: true
run: pnpm run test:extensions:package-boundary
- name: Enforce safe external URL opening policy
id: no_raw_window_open
continue-on-error: true
@@ -802,7 +797,6 @@ jobs:
EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME: ${{ steps.extension_relative_outside_package_boundary.outcome }}
EXTENSION_CHANNEL_LINT_OUTCOME: ${{ steps.extension_channel_lint.outcome }}
EXTENSION_BUNDLED_LINT_OUTCOME: ${{ steps.extension_bundled_lint.outcome }}
EXTENSION_PACKAGE_BOUNDARY_TSC_OUTCOME: ${{ steps.extension_package_boundary_tsc.outcome }}
NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }}
CONTROL_UI_I18N_OUTCOME: ${{ steps.control_ui_i18n.outcome == 'skipped' && 'success' || steps.control_ui_i18n.outcome }}
GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }}
@@ -826,7 +820,6 @@ jobs:
"extension-relative-outside-package-boundary|$EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME" \
"lint:extensions:channels|$EXTENSION_CHANNEL_LINT_OUTCOME" \
"lint:extensions:bundled|$EXTENSION_BUNDLED_LINT_OUTCOME" \
"test:extensions:package-boundary|$EXTENSION_PACKAGE_BOUNDARY_TSC_OUTCOME" \
"lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \
"ui:i18n:check|$CONTROL_UI_I18N_OUTCOME" \
"gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME"; do

View File

@@ -4,119 +4,70 @@ Docs: https://docs.openclaw.ai
## Unreleased
## 2026.4.8
### Fixes
- Telegram/setup: load setup and secret contracts through packaged top-level sidecars so installed npm builds no longer try to import missing `dist/extensions/telegram/src/*` files during gateway startup.
- Bundled channels/setup: load shared secret contracts through packaged top-level sidecars across BlueBubbles, Feishu, Google Chat, IRC, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Slack, and Zalo so installed npm builds no longer rely on missing `dist/extensions/*/src/*` files during gateway startup.
- Bundled plugins: align packaged plugin compatibility metadata with the release version so bundled channels and providers load on OpenClaw 2026.4.8.
- Agents/progress: keep `update_plan` available for OpenAI-family runs while returning compact success payloads and allowing `tools.experimental.planTool=false` to opt out.
- Agents/exec: keep `/exec` current-default reporting aligned with real runtime behavior so `host=auto` sessions surface the correct host-aware fallback policy (`full/off` on gateway or node, `deny/off` on sandbox) instead of stale stricter defaults.
- Slack: honor ambient HTTP(S) proxy settings for Socket Mode WebSocket connections, including NO_PROXY exclusions, so proxy-only deployments can connect without a monkey patch. (#62878) Thanks @mjamiv.
- Slack/actions: pass the already resolved read token into `downloadFile` so SecretRef-backed bot tokens no longer fail after a raw config re-read. (#62097) Thanks @martingarramon.
- Network/fetch guard: skip target DNS pinning when trusted env-proxy mode is active so proxy-only sandboxes can let the trusted proxy resolve outbound hosts. (#59007) Thanks @cluster2600.
## 2026.4.7
### Changes
- CLI/infer: add a first-class `openclaw infer ...` hub for provider-backed inference workflows across model, media, web, and embedding tasks. Thanks @Takhoffman.
- Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, preserve intent during provider switches, remap size/aspect/resolution/duration hints to the closest supported option, and surface provider capabilities plus mode-aware video-to-video support.
- Memory/wiki: restore the bundled `memory-wiki` stack with plugin, CLI, sync/query/apply tooling, memory-host integration, structured claim/evidence fields, compiled digest retrieval, claim-health linting, contradiction clustering, staleness dashboards, and freshness-weighted search. Thanks @vincentkoc.
- CLI/capabilities: add a first-class `openclaw capability ...` hub for provider-backed inference workflows across model, media, web, and embedding tasks, with capability inspection, provider discovery, and consistent JSON output. Thanks @Takhoffman.
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, and doctor flows again, and keep the Docker Claude CLI live lane aligned with the restored guidance.
- Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.
- Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.
- Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via `agents.defaults.compaction.provider`; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.
- Agents/system prompt: add `agents.defaults.systemPromptOverride` for controlled prompt experiments plus heartbeat prompt-section controls so heartbeat runtime behavior can stay enabled without injecting heartbeat instructions every turn.
- Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.
- Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in compatibility wrappers. (#62127) Thanks @romgenie.
- Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.
- Providers/Ollama: detect vision capability from the `/api/show` response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.
- Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.
- Providers/inferrs: add string-content compatibility for stricter OpenAI-compatible chat backends, document `inferrs` setup with a full config example, and add troubleshooting guidance for local backends that pass direct probes but fail on full agent-runtime prompts.
- Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.
- Plugin SDK/context engines: pass `availableTools` and `citationsMode` into `assemble()`, and expose memory-artifact and memory-prompt seams so companion plugins and non-legacy context engines can consume active memory state without reaching into internals. Thanks @vincentkoc.
- ACP/ACPX plugin: bump the bundled `acpx` pin to `0.5.1` so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.
- Discord/events: allow `event-create` to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.
- Plugins/provider-auth: expose runtime-ready provider auth through `openclaw/plugin-sdk/provider-auth-runtime` so native plugins and context engines can resolve request-ready credentials after provider-owned runtime exchanges like GitHub Copilot device-token-to-bearer flows. (#62753) Thanks @jalehman.
- Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips.
### Fixes
- CLI/infer: keep provider-backed infer behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription `prompt`/`language` overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.
- Plugins/media: when `plugins.allow` is set, capability fallback now merges bundled capability plugin ids into the allowlist (not only `plugins.entries`), so media understanding providers such as OpenAI-compatible STT load for voice transcription without requiring `openai` in `plugins.allow`. (#62205) Thanks @neeravmakwana.
- Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on `final_answer` text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin and contributors.
- Control UI: show `/tts` audio replies in webchat, detect mistaken `?token=` auth links with the correct `#token=` hint, and keep Copy, Canvas, and mobile exec-approval UI from covering chat content on narrow screens. (#54842, #61514, #61598) Thanks @neeravmakwana.
- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.
- TUI: route `/status` through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan and contributors.
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including reconnect recovery, pending approval persistence, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
- CLI/capabilities: keep provider-backed capability behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription `prompt`/`language` overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.
- Channels/secrets: keep bundled channel artifact and secret-contract loading stable under lazy loading so bundled channel secrets continue to appear in `openclaw secret`, status, and security-audit surfaces.
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again so native xAI web-search attribution keeps working on Grok-hosted base URLs. (#61377) Thanks @jjjojoj.
- Providers/Anthropic/cache: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so Anthropic prompt-cache prefixes keep matching after thinking turns. (#61793)
- Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after `refresh_token_reused` rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.
- Auth/OpenAI Codex OAuth: keep native `/model ...@profile` selections on the target session and honor explicit user-locked auth profiles even when per-agent auth order excludes them. (#62744) Thanks @jalehman.
- Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip `service_tier` injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)
- Agents/Claude CLI: surface nested API error messages from structured CLI output so billing/auth/provider failures show the real provider error instead of an opaque CLI failure.
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, fail loud on invalid elevated cross-host overrides, and keep `strictInlineEval` commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.
- Nodes/exec approvals: keep `host=node` POSIX transport shell wrappers (`/bin/sh -lc ...`) aligned with inner-command allowlist analysis so allowlisted scripts stop prompting unnecessarily, while Windows `cmd.exe` wrapper runs stay approval-gated. (#62401) Thanks @ngutman.
- Nodes/exec approvals: keep Windows `cmd.exe /c` wrapper runs approval-gated even when `env` carriers, including env-assignment carriers, wrap the shell invocation. (#62439) Thanks @ngutman.
- Gateway tool/exec config: block model-facing `gateway config.apply` and `config.patch` writes from changing exec approval paths such as `safeBins`, `safeBinProfiles`, `safeBinTrustedDirs`, and `strictInlineEval`, while still allowing unchanged structured values through. (#62001) Thanks @eleqtrizit.
- Host exec/env sanitization: block dangerous Java, Rust, Cargo, Git, Kubernetes, cloud credential, config-path, and Helm env overrides so host-run tools cannot be redirected to attacker-chosen code, config, credentials, or repository state. (#59119, #62002, #62291) Thanks @eleqtrizit and contributors.
- Commands/allowlist: require owner authorization for `/allowlist add` and `/allowlist remove` before channel resolution, so non-owner but command-authorized senders can no longer persistently rewrite allowlist policy state. (#62383) Thanks @pgondhi987.
- Feishu/docx uploads: honor `tools.fs.workspaceOnly` for local `upload_file` and `upload_image` paths by forwarding workspace-constrained `localRoots` into the media loader, so docx uploads can no longer read host-local files outside the workspace when workspace-only mode is active. (#62369) Thanks @pgondhi987.
- Network/fetch guard: drop request bodies and body-describing headers on cross-origin `307` and `308` redirects by default, so attacker-controlled redirect hops cannot receive secret-bearing POST payloads from SSRF-guarded fetch flows unless a caller explicitly opts in. (#62357) Thanks @pgondhi987.
- Browser/SSRF: treat main-frame `document` redirect hops as navigations even when Playwright does not flag them as `isNavigationRequest()`, so strict private-network blocking still stops forbidden redirect pivots before the browser reaches the internal target. (#62355) Thanks @pgondhi987.
- Browser/node invoke: block persistent browser profile create, reset, and delete mutations through `browser.proxy` on both gateway-forwarded `node.invoke` and the node-host proxy path, even when no profile allowlist is configured. (#60489)
- Gateway/node pairing: require a fresh pairing request when a previously paired node reconnects with additional declared commands, and keep the live session pinned to the earlier approved command set until the upgrade is approved. (#62658) Thanks @eleqtrizit.
- Gateway/auth: invalidate existing shared-token and password WebSocket sessions when the configured secret rotates, so stale authenticated sockets cannot stay attached after token or password changes. (#62350) Thanks @pgondhi987.
- MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)
- Media/base64 decode guards: enforce byte limits before decoding missed base64-backed Teams, Signal, QQ Bot, and image-tool payloads so oversized inbound media and data URLs no longer bypass pre-decode size checks. (#62007) Thanks @eleqtrizit.
- Runtime event trust: mark background `notifyOnExit` summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text. (#62003)
- Auto-reply/media: allow managed generated-media `MEDIA:` paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.
- Gateway/status and containers: auto-bind to `0.0.0.0` inside Docker and Podman environments, and probe local TLS gateways over `wss://` with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and contributors.
- Gateway/OpenAI-compatible HTTP: abort in-flight `/v1/chat/completions` and `/v1/responses` turns when clients disconnect so abandoned HTTP requests stop wasting agent runtime. (#54388) Thanks @Lellansin.
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
- Discord/ACP bindings: canonicalize DM conversation identity across inbound messages, component interactions, native commands, and current-conversation binding resolution so `--bind here` in Discord DMs keeps routing follow-up replies to the bound agent instead of falling back to the default agent.
- Discord: recover forwarded referenced message text and attachments when snapshots are missing, use `ws://` again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and contributors.
- Slack/thread mentions: add `channels.slack.thread.requireExplicitMention` so Slack channels that already require mentions can also require explicit `@bot` mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
- Slack/media: keep attachment downloads on the SSRF-guarded dispatcher path so Slack media fetching works on Node 22 without dropping pinned transport enforcement. (#62239) Thanks @openperf.
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
- Plugins/loaders: centralize bundled `dist/**` Jiti native-load policy and keep channel, public-surface, facade, and config-metadata loader seams off native Jiti on Windows so onboarding and configure flows stop tripping `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#62286) Thanks @chen-zhang-cs-code.
- Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows `file://` plus native-Jiti plugin loader paths so onboarding, doctor, `openclaw secret`, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and contributors.
- Plugins/ClawHub: verify downloaded plugin archives against version metadata SHA-256, fail closed when archive integrity metadata is missing or malformed, and tighten fallback ZIP verification so plugin installs cannot proceed on mismatched or incomplete ClawHub package metadata. (#60517) Thanks @mappel-nv.
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
- Docker/plugins: stop forcing bundled plugin discovery to `/app/extensions` in runtime images so packaged installs use compiled `dist/extensions` artifacts again and Node 24 containers do not boot through source-only plugin entry paths. Fixes #62044. (#62316) Thanks @gumadeiras.
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
- Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again and keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.
- Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistrals Chat Completions API. (#62162) Thanks @neeravmakwana.
- OpenAI TTS/Groq: send `wav` to Groq-compatible speech endpoints, honor explicit `responseFormat` overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is `opus`. (#62233) Thanks @neeravmakwana.
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.
- Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin instead of always targeting `memory-core`. (#62275) Thanks @SnowSky1.
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
- Cron: load `jobId` into `id` when the on-disk store omits `id`, matching doctor migration and fixing `unknown cron job id` for hand-edited `jobs.json`. (#62246) Thanks @neeravmakwana.
- Agents/model fallback: classify minimal HTTP 404 API errors (for example `404 status code (no body)`) as `model_not_found` so assistant failures throw into the fallback chain instead of stopping at the first fallback candidate. (#62119) Thanks @neeravmakwana.
- BlueBubbles/network: respect explicit private-network opt-out for loopback and private `serverUrl` values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan.
- Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) Thanks @100yenadmin.
- Agents/heartbeat: respect disabled heartbeat prompt guidance so operators can suppress heartbeat prompt instructions without disabling heartbeat runtime behavior.
- Agents/compaction: stop compaction-wait aborts from re-entering prompt failover and replaying completed tool turns. (#62600) Thanks @i-dentifier.
- Approvals/runtime: move native approval lifecycle assembly into shared core bootstrap/runtime seams driven by channel capabilities and runtime contexts, and remove the legacy bundled approval fallback wiring. (#62135) Thanks @gumadeiras.
- Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk.
- Logging: make `logging.level` and `logging.consoleLevel` honor the documented severity threshold ordering again, and keep child loggers inheriting the parent `minLevel`. (#44646) Thanks @zhumengzhu.
- Agents/sessions_send: pass `threadId` through announce delivery so cross-session notifications land in the correct Telegram forum topic instead of the group's general thread. (#62758) Thanks @jalehman.
- Daemon/systemd: keep sudo systemctl calls scoped to the invoking user when machine-scoped systemctl fails, while still avoiding machine fallback for permission-denied user bus errors. (#62337) Thanks @Aftabbs.
- Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.
- Agents/exec: keep timed-out shell-backgrounded commands on the failed path and point long-running jobs to exec background/yield sessions so process polling is only suggested for registered sessions.
- Agents/model resolution: let explicit `openai-codex/gpt-5.4` selection prefer provider runtime metadata when it reports a larger context window, keeping configured Codex runs aligned with the live provider limits. (#62694) Thanks @ruclaw7.
- Agents/model resolution: keep explicit-model runtime comparisons on the configured workspace plugin registry, so workspace-installed providers do not silently fall back to stale explicit metadata during runtime model lookup.
- Providers/Z.AI: default onboarding and endpoint detection to GLM-5.1 instead of GLM-5. (#61998) Thanks @serg0x.
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded so memory indexing no longer reports false-success while semantic recall is impaired.
- MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)
- Discord/gateway monitor: use `ws://` again for gateway monitor sockets so Discord monitor connections recover reliably after recent gateway socket changes.
- Control UI/auth URLs: detect mistaken `?token=` links, show the correct `#token=` fragment hint only on real auth failures, and stop masking the real problem behind a generic device-identity error. (#54842)
- Control UI/chat layout: keep Copy and Canvas actions plus mobile exec-approval overlays from covering chat text or command previews on narrow screens. (#61514)
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps its content attached to the correct list item. (#60997) Thanks @gucasbrg.
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
- Secrets/x_search: keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load after the plugin-owned auth move.
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including background-safe reconnects, persisted pending approvals, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
- Discord/forwarding: recover forwarded referenced message text and attachments when Discord omits snapshot payloads, so forwarded-message relays keep the original content. (#61670) Thanks @artwalker.
- TUI/status: route `/status` through the shared session-status command and move the old gateway-wide diagnostic summary to `/gateway-status` (`/gwstatus`). Thanks @vincentkoc.
- TUI/history and heartbeat: keep assistant commentary hidden on both streamed and reloaded TUI history views, preserve the phase-sanitized REST history contract, and stop forced heartbeat runs from targeting subagent sessions. (#61463) Thanks @100yenadmin.
- TUI/command messages: strip inbound envelope metadata before rendering command/system messages so async completion notices stop leaking raw wrappers into the operator terminal. (#59985) Thanks @MoerAI.
- TUI/terminal: restore Kitty keyboard protocol and `modifyOtherKeys` state on TUI exit and fatal CLI crashes so parent shells stop inheriting broken keyboard input after `openclaw tui` exits. (#49130) Thanks @biefan.
- Plugins/Windows: load plugin entrypoints through `file://` import specifiers on Windows without breaking plugin SDK alias resolution, fixing `ERR_UNSUPPORTED_ESM_URL_SCHEME` for absolute plugin paths. (#61832) Thanks @Zeesejo.
- Plugins/Windows: disable native Jiti loading for setup and doctor contract registries on Windows so onboarding and config-doctor plugin probes stop crashing with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#61836, #61853)
- Plugins/install: preserve plugin-schema defaults during fresh-install raw config validation so bundled plugin installs stop failing when required fields rely on schema defaults. (#61856) Thanks @SuperMarioYL.
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
- Gateway/containers: auto-bind to `0.0.0.0` during container startup for Docker and Podman compatibility, while keeping host-side status and doctor checks on the hardened loopback default when `gateway.bind` is unset. (#61818) Thanks @openperf.
- Gateway/status: probe local TLS gateways over `wss://`, forward the local cert fingerprint for self-signed loopback probes, and warn when the local TLS runtime cannot load the configured cert. (#61935) Thanks @ThanhNguyxn07.
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
- Discord/voice: re-arm DAVE receive passthrough without suppressing decrypt-failure rejoin recovery, and clear capture state before finalize teardown so rapid speaker restarts keep their next utterance. (#41536) Thanks @wit-oc.
- Providers/Google: recognize Gemma model ids in native Google forward-compat resolution, keep the requested provider when cloning fallback templates, and force Gemma reasoning off so Gemma 4 routes stop failing through the Google catalog fallback. (#61507) Thanks @eyjohn.
- Providers/Anthropic: skip `service_tier` injection for OAuth-authenticated stream wrapper requests so Claude OAuth requests stop failing with HTTP 401. (#60356) thanks @openperf.
- Providers/OpenAI: keep WebSocket text buffered until a real assistant phase arrives, even when text deltas land before a phaseless `output_item.added` announcement. (#61954) Thanks @100yenadmin.
- Providers/OpenAI: accept case-insensitive `plugins.entries.openai.config.personality` values, keep unknown overrides on the friendly overlay path, and add `on` as an alias for `friendly`. Thanks @vincentkoc.
- Discord/thread titles: stop forcing a hardcoded temperature for generated auto-thread names so Codex-backed thread title generation works on `openai-codex/*` models again. (#59525)
- Agents/message tool: add a `read` plus `threadId` discoverability hint when the configured channel actions support threaded message reads.
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one repair pass, and restore a total-context overflow backstop during tool loops so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, and fail loud on invalid elevated cross-host overrides. (#61739) Thanks @obviyus.
- Agents/heartbeat: stop truncating live session transcripts after no-op heartbeat acks, move heartbeat cleanup to prompt assembly and compaction, and keep post-filter context-engine ingestion aligned with the real session baseline. (#60998) Thanks @nxmxbbd.
- Gateway/TUI: defer terminal chat finalization for per-attempt lifecycle errors so fallback retries keep streaming before the run is marked failed. (#60043) Thanks @jwchmodx.
- Gateway/history: seed SSE startup history and raw transcript sequence tracking from one initial transcript snapshot so first history events cannot diverge from subsequent message sequence numbering. (#61855) Thanks @100yenadmin.
- Agents/history: keep history-based reply reads and subagent completion summaries on `final_answer` text only so internal commentary stops leaking into user-visible follow-up replies. (#61747) Thanks @afurm.
- Agents/history: suppress commentary-only visible-text leaks in streaming and chat history views, and keep sanitized SSE history sequence numbers monotonic after transcript-only refreshes. (#61829) Thanks @100yenadmin.
- Agents/history: use one shared assistant-visible sanitizer across embedded delivery and chat-history extraction so leaked `<tool_call>` and `<tool_result>` XML blocks stay hidden from user-facing replies. (#61729) Thanks @openperf.
- Agents/history: keep truly legacy unsigned replay text unphased when mixed with phased OpenAI WS assistant blocks, while still inheriting message phase for id-only replay signatures. (#61529) Thanks @100yenadmin.
- Memory/dreaming: strip managed Light Sleep and REM blocks before daily-note ingestion so dreaming summaries stop re-ingesting their own staged output into new candidates. (#61720) Thanks @MonkeyLeeT.
- Docs/i18n: relocalize final localized-page links after translation so generated locale pages stop keeping stale English-root links when targets appear later in the same run. (#61796) thanks @hxy91819.
- Docs/i18n: remove the zh-CN homepage redirect override so Mintlify can resolve the localized Chinese homepage without self-redirecting `/zh-CN/index`.
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
- Agents/session keys: backfill `sessionKey` from `sessionId` in the embedded PI runner when callers omit it, so hooks, LCM, and compaction receive a valid key; also normalize whitespace-only session keys to `undefined` before downstream consumers see them. (#60555) Thanks @100yenadmin.
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
- Discord/voice: re-arm DAVE receive passthrough without suppressing decrypt-failure rejoin recovery, and clear capture state before finalize teardown so rapid speaker restarts keep their next utterance. (#41536) Thanks @wit-oc.
- Agents/exec: keep `strictInlineEval` commands blocked after approval timeouts on both gateway and node exec hosts, so timeout fallback no longer turns timed-out inline interpreter prompts into automatic execution.
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
- Exec/runtime events: mark background `notifyOnExit` summaries and ACP parent-stream relays as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text.
- Hooks/wake: queue direct and mapped wake-hook payloads as untrusted system events so external wake content no longer enters the main session as trusted input. (#62003)
## 2026.4.5
@@ -255,7 +206,6 @@ Docs: https://docs.openclaw.ai
- Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly `reasoning:stream`, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc.
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.
- Browser/security: re-run SSRF safety checks after interaction-driven navigations and before snapshot reads so click, submit, keyboard, and current-page snapshot flows fail closed on disallowed destinations. (#62023) Thanks @eleqtrizit.
- Memory: keep `memory-core` builtin embedding registration on the already-registered path so selecting `memory-core` no longer recurses through plugin discovery and crashes during startup. (#61402) Thanks @ngutman.
- Agents/tool results: keep large `read` outputs visible longer, preserve the latest `read` output when older tool output can absorb the overflow budget, and fall back to Pi's normal overflow compaction/retry path before replacing a fresh `read` with a compacted stub. Thanks @vincentkoc.
- Memory/QMD: prefer modern `qmd collection add --glob`, accept newer single-line JSON hit metadata while keeping legacy line fields, refresh QMD docs/doctor install guidance and model-override guidance, and keep older QMD releases working. Thanks @vincentkoc.
@@ -298,7 +248,6 @@ Docs: https://docs.openclaw.ai
- Providers/OpenRouter failover: classify `403 “Key limit exceeded”` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
- Providers/Anthropic: keep `claude-cli/*` auth on live Claude CLI credentials at runtime, avoid persisting stale bearer-token profiles, and suppress macOS Keychain prompts during non-interactive Claude CLI setup. (#61234) Thanks @darkamenosa.
- Providers/Anthropic: when Claude CLI auth becomes the default, write a real `claude-cli` auth profile so local and gateway agent runs can use Claude CLI immediately without missing-API-key failures. Thanks @vincentkoc.
- Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin (including `doctor.memory.status` and Control UI fallback state) instead of always targeting `memory-core`. (#62275) Thanks @SnowSky1.
- Providers/Anthropic Vertex: honor `cacheRetention: “long”` with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default `anthropic-vertex` cache retention like direct Anthropic. (#60888) Thanks @affsantos.
- Agents/Anthropic: preserve native `toolu_*` replay ids on direct Anthropic and Anthropic Vertex paths so cache-sensitive history stops rewriting known-valid Anthropic tool-use ids. (#52612)
- Providers/Google: add model-level `cacheRetention` support for direct Gemini system prompts by creating, reusing, and refreshing `cachedContents` automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch.
@@ -853,7 +802,6 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
- Docs: add `pnpm docs:check-links:anchors` for Mintlify anchor validation while keeping `scripts/docs-link-audit.mjs` as the stable link-audit entrypoint. (#55912) Thanks @velvet-shark.
- Tavily: mark outbound API requests with `X-Client-Source: openclaw` so Tavily can attribute OpenClaw-originated traffic. (#55335) Thanks @lakshyaag-tavily.
- Plugins/hooks: add async `requireApproval` to `before_tool_call` hooks, letting plugins pause tool execution and prompt the user for approval via the exec approval overlay, Telegram buttons, Discord interactions, or the `/approve` command on any channel. The `/approve` command now handles both exec and plugin approvals with automatic fallback. (#55339) Thanks @vaclavbelak and @joshavant.
### Fixes
@@ -1791,9 +1739,6 @@ Docs: https://docs.openclaw.ai
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
- Doctor/Codex OAuth: warn only for legacy `models.providers.openai-codex` transport overrides that can shadow the built-in Codex OAuth path, while leaving supported custom proxies and header-only overrides alone. (#40143) Thanks @bde1.
- Hooks/session-memory: keep `/new` and `/reset` memory artifacts in the bound agent workspace and align saved reset session keys with that workspace when stale main-agent keys leak into the hook path. (#39875) thanks @rbutera.
- Sessions/model switch: clear stale cached `contextTokens` when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.
- ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky.

View File

@@ -62,7 +62,6 @@ RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
@@ -103,19 +102,7 @@ RUN pnpm qa:lab:build
# Prune dev dependencies and strip build-only metadata before copying
# runtime assets into the final image.
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# Keep the install layer frozen, but allow prune to run against the full copied
# workspace tree subset used during `pnpm install`. The build stage only copied
# the root, `ui`, and opted-in plugin manifests into the install layer, so
# prune must not rediscover unrelated workspaces from the later full source
# copy.
RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
for ext in $OPENCLAW_EXTENSIONS; do \
printf ' - %s/%s\n' "$OPENCLAW_BUNDLED_PLUGIN_DIR" "$ext" >> /tmp/pnpm-workspace.runtime.yaml; \
done && \
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
RUN CI=true pnpm prune --prod && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
@@ -172,6 +159,10 @@ COPY --from=runtime-assets --chown=node:node /app/skills ./skills
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
COPY --from=runtime-assets --chown=node:node /app/qa ./qa
# In npm-installed Docker images, prefer the copied source extension tree for
# bundled discovery so package metadata that points at source entries stays valid.
ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/${OPENCLAW_BUNDLED_PLUGIN_DIR}
# Keep pnpm available in the runtime image for container-local workflows.
# Use a shared Corepack home so the non-root `node` user does not need a
# first-run network fetch when invoking pnpm.

View File

@@ -89,7 +89,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
Model note: while many providers/models are supported, for the best experience and lower prompt-injection risk use the strongest latest-generation model available to you. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
## Models (selection + auth)
@@ -371,7 +371,7 @@ Minimal `~/.openclaw/openclaw.json` (model + defaults):
```json5
{
agent: {
model: "<provider>/<model-id>",
model: "anthropic/claude-opus-4-6",
},
}
```

View File

@@ -2,113 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.7</title>
<pubDate>Wed, 08 Apr 2026 02:54:26 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026040790</sparkle:version>
<sparkle:shortVersionString>2026.4.7</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.7</h2>
<h3>Changes</h3>
<ul>
<li>CLI/infer: add a first-class <code>openclaw infer ...</code> hub for provider-backed inference workflows across model, media, web, and embedding tasks. Thanks @Takhoffman.</li>
<li>Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, preserve intent during provider switches, remap size/aspect/resolution/duration hints to the closest supported option, and surface provider capabilities plus mode-aware video-to-video support.</li>
<li>Memory/wiki: restore the bundled <code>memory-wiki</code> stack with plugin, CLI, sync/query/apply tooling, memory-host integration, structured claim/evidence fields, compiled digest retrieval, claim-health linting, contradiction clustering, staleness dashboards, and freshness-weighted search. Thanks @vincentkoc.</li>
<li>Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.</li>
<li>Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.</li>
<li>Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via <code>agents.defaults.compaction.provider</code>; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.</li>
<li>Agents/system prompt: add <code>agents.defaults.systemPromptOverride</code> for controlled prompt experiments plus heartbeat prompt-section controls so heartbeat runtime behavior can stay enabled without injecting heartbeat instructions every turn.</li>
<li>Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.</li>
<li>Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in compatibility wrappers. (#62127) Thanks @romgenie.</li>
<li>Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.</li>
<li>Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.</li>
<li>Providers/Ollama: detect vision capability from the <code>/api/show</code> response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.</li>
<li>Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.</li>
<li>Providers/inferrs: add string-content compatibility for stricter OpenAI-compatible chat backends, document <code>inferrs</code> setup with a full config example, and add troubleshooting guidance for local backends that pass direct probes but fail on full agent-runtime prompts.</li>
<li>Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.</li>
<li>Plugin SDK/context engines: pass <code>availableTools</code> and <code>citationsMode</code> into <code>assemble()</code>, and expose memory-artifact and memory-prompt seams so companion plugins and non-legacy context engines can consume active memory state without reaching into internals. Thanks @vincentkoc.</li>
<li>ACP/ACPX plugin: bump the bundled <code>acpx</code> pin to <code>0.5.1</code> so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.</li>
<li>Discord/events: allow <code>event-create</code> to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>CLI/infer: keep provider-backed infer behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription <code>prompt</code>/<code>language</code> overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.</li>
<li>Plugins/media: when <code>plugins.allow</code> is set, capability fallback now merges bundled capability plugin ids into the allowlist (not only <code>plugins.entries</code>), so media understanding providers such as OpenAI-compatible STT load for voice transcription without requiring <code>openai</code> in <code>plugins.allow</code>. (#62205) Thanks @neeravmakwana.</li>
<li>Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on <code>final_answer</code> text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin and contributors.</li>
<li>Control UI: show <code>/tts</code> audio replies in webchat, detect mistaken <code>?token=</code> auth links with the correct <code>#token=</code> hint, and keep Copy, Canvas, and mobile exec-approval UI from covering chat content on narrow screens. (#54842, #61514, #61598) Thanks @neeravmakwana.</li>
<li>iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.</li>
<li>TUI: route <code>/status</code> through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan and contributors.</li>
<li>iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including reconnect recovery, pending approval persistence, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.</li>
<li>Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.</li>
<li>Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after <code>refresh_token_reused</code> rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.</li>
<li>Auth/OpenAI Codex OAuth: keep native <code>/model ...@profile</code> selections on the target session and honor explicit user-locked auth profiles even when per-agent auth order excludes them. (#62744) Thanks @jalehman.</li>
<li>Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip <code>service_tier</code> injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)</li>
<li>Agents/Claude CLI: surface nested API error messages from structured CLI output so billing/auth/provider failures show the real provider error instead of an opaque CLI failure.</li>
<li>Agents/exec: preserve explicit <code>host=node</code> routing under elevated defaults when <code>tools.exec.host=auto</code>, fail loud on invalid elevated cross-host overrides, and keep <code>strictInlineEval</code> commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.</li>
<li>Nodes/exec approvals: keep <code>host=node</code> POSIX transport shell wrappers (<code>/bin/sh -lc ...</code>) aligned with inner-command allowlist analysis so allowlisted scripts stop prompting unnecessarily, while Windows <code>cmd.exe</code> wrapper runs stay approval-gated. (#62401) Thanks @ngutman.</li>
<li>Nodes/exec approvals: keep Windows <code>cmd.exe /c</code> wrapper runs approval-gated even when <code>env</code> carriers, including env-assignment carriers, wrap the shell invocation. (#62439) Thanks @ngutman.</li>
<li>Gateway tool/exec config: block model-facing <code>gateway config.apply</code> and <code>config.patch</code> writes from changing exec approval paths such as <code>safeBins</code>, <code>safeBinProfiles</code>, <code>safeBinTrustedDirs</code>, and <code>strictInlineEval</code>, while still allowing unchanged structured values through. (#62001) Thanks @eleqtrizit.</li>
<li>Host exec/env sanitization: block dangerous Java, Rust, Cargo, Git, Kubernetes, cloud credential, config-path, and Helm env overrides so host-run tools cannot be redirected to attacker-chosen code, config, credentials, or repository state. (#59119, #62002, #62291) Thanks @eleqtrizit and contributors.</li>
<li>Commands/allowlist: require owner authorization for <code>/allowlist add</code> and <code>/allowlist remove</code> before channel resolution, so non-owner but command-authorized senders can no longer persistently rewrite allowlist policy state. (#62383) Thanks @pgondhi987.</li>
<li>Feishu/docx uploads: honor <code>tools.fs.workspaceOnly</code> for local <code>upload_file</code> and <code>upload_image</code> paths by forwarding workspace-constrained <code>localRoots</code> into the media loader, so docx uploads can no longer read host-local files outside the workspace when workspace-only mode is active. (#62369) Thanks @pgondhi987.</li>
<li>Network/fetch guard: drop request bodies and body-describing headers on cross-origin <code>307</code> and <code>308</code> redirects by default, so attacker-controlled redirect hops cannot receive secret-bearing POST payloads from SSRF-guarded fetch flows unless a caller explicitly opts in. (#62357) Thanks @pgondhi987.</li>
<li>Browser/SSRF: treat main-frame <code>document</code> redirect hops as navigations even when Playwright does not flag them as <code>isNavigationRequest()</code>, so strict private-network blocking still stops forbidden redirect pivots before the browser reaches the internal target. (#62355) Thanks @pgondhi987.</li>
<li>Browser/node invoke: block persistent browser profile create, reset, and delete mutations through <code>browser.proxy</code> on both gateway-forwarded <code>node.invoke</code> and the node-host proxy path, even when no profile allowlist is configured. (#60489)</li>
<li>Gateway/node pairing: require a fresh pairing request when a previously paired node reconnects with additional declared commands, and keep the live session pinned to the earlier approved command set until the upgrade is approved. (#62658) Thanks @eleqtrizit.</li>
<li>Gateway/auth: invalidate existing shared-token and password WebSocket sessions when the configured secret rotates, so stale authenticated sockets cannot stay attached after token or password changes. (#62350) Thanks @pgondhi987.</li>
<li>MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)</li>
<li>Media/base64 decode guards: enforce byte limits before decoding missed base64-backed Teams, Signal, QQ Bot, and image-tool payloads so oversized inbound media and data URLs no longer bypass pre-decode size checks. (#62007) Thanks @eleqtrizit.</li>
<li>Runtime event trust: mark background <code>notifyOnExit</code> summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted <code>System:</code> text. (#62003)</li>
<li>Auto-reply/media: allow managed generated-media <code>MEDIA:</code> paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.</li>
<li>Gateway/status and containers: auto-bind to <code>0.0.0.0</code> inside Docker and Podman environments, and probe local TLS gateways over <code>wss://</code> with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and contributors.</li>
<li>Gateway/OpenAI-compatible HTTP: abort in-flight <code>/v1/chat/completions</code> and <code>/v1/responses</code> turns when clients disconnect so abandoned HTTP requests stop wasting agent runtime. (#54388) Thanks @Lellansin.</li>
<li>macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like <code>OpenClaw 2026.4.2 (d74a122)</code> again. (#61111) Thanks @oliviareid-svg.</li>
<li>Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.</li>
<li>Discord/ACP bindings: canonicalize DM conversation identity across inbound messages, component interactions, native commands, and current-conversation binding resolution so <code>--bind here</code> in Discord DMs keeps routing follow-up replies to the bound agent instead of falling back to the default agent.</li>
<li>Discord: recover forwarded referenced message text and attachments when snapshots are missing, use <code>ws://</code> again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and contributors.</li>
<li>Slack/thread mentions: add <code>channels.slack.thread.requireExplicitMention</code> so Slack channels that already require mentions can also require explicit <code>@bot</code> mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.</li>
<li>Slack/threading: keep legacy thread stickiness for real replies when older callers omit <code>isThreadReply</code>, while still honoring <code>replyToMode</code> for Slack's auto-created top-level <code>thread_ts</code>. (#61835) Thanks @kaonash.</li>
<li>Slack/media: keep attachment downloads on the SSRF-guarded dispatcher path so Slack media fetching works on Node 22 without dropping pinned transport enforcement. (#62239) Thanks @openperf.</li>
<li>Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.</li>
<li>Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.</li>
<li>Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into <code>accounts.default</code>, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.</li>
<li>Plugins/loaders: centralize bundled <code>dist/**</code> Jiti native-load policy and keep channel, public-surface, facade, and config-metadata loader seams off native Jiti on Windows so onboarding and configure flows stop tripping <code>ERR_UNSUPPORTED_ESM_URL_SCHEME</code>. (#62286) Thanks @chen-zhang-cs-code.</li>
<li>Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows <code>file://</code> plus native-Jiti plugin loader paths so onboarding, doctor, <code>openclaw secret</code>, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and contributors.</li>
<li>Plugins/ClawHub: verify downloaded plugin archives against version metadata SHA-256, fail closed when archive integrity metadata is missing or malformed, and tighten fallback ZIP verification so plugin installs cannot proceed on mismatched or incomplete ClawHub package metadata. (#60517) Thanks @mappel-nv.</li>
<li>Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)</li>
<li>Docker/plugins: stop forcing bundled plugin discovery to <code>/app/extensions</code> in runtime images so packaged installs use compiled <code>dist/extensions</code> artifacts again and Node 24 containers do not boot through source-only plugin entry paths. Fixes #62044. (#62316) Thanks @gumadeiras.</li>
<li>Providers/Ollama: honor the selected provider's <code>baseUrl</code> during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)</li>
<li>Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.</li>
<li>Providers/xAI: recognize <code>api.grok.x.ai</code> as an xAI-native endpoint again and keep legacy <code>x_search</code> auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.</li>
<li>Providers/Mistral: send <code>reasoning_effort</code> for <code>mistral/mistral-small-latest</code> (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistrals Chat Completions API. (#62162) Thanks @neeravmakwana.</li>
<li>OpenAI TTS/Groq: send <code>wav</code> to Groq-compatible speech endpoints, honor explicit <code>responseFormat</code> overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is <code>opus</code>. (#62233) Thanks @neeravmakwana.</li>
<li>Tools/web_fetch and web_search: fix <code>TypeError: fetch failed</code> caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set <code>allowH2: false</code> to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.</li>
<li>Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.</li>
<li>Memory/vector recall: surface explicit warnings when <code>sqlite-vec</code> is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.</li>
<li>Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin instead of always targeting <code>memory-core</code>. (#62275) Thanks @SnowSky1.</li>
<li>QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.</li>
<li>Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.</li>
<li>UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.</li>
<li>Agents/subagents: honor <code>sessions_spawn(lightContext: true)</code> for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.</li>
<li>Cron: load <code>jobId</code> into <code>id</code> when the on-disk store omits <code>id</code>, matching doctor migration and fixing <code>unknown cron job id</code> for hand-edited <code>jobs.json</code>. (#62246) Thanks @neeravmakwana.</li>
<li>Agents/model fallback: classify minimal HTTP 404 API errors (for example <code>404 status code (no body)</code>) as <code>model_not_found</code> so assistant failures throw into the fallback chain instead of stopping at the first fallback candidate. (#62119) Thanks @neeravmakwana.</li>
<li>BlueBubbles/network: respect explicit private-network opt-out for loopback and private <code>serverUrl</code> values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan.</li>
<li>Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) Thanks @100yenadmin.</li>
<li>Agents/heartbeat: respect disabled heartbeat prompt guidance so operators can suppress heartbeat prompt instructions without disabling heartbeat runtime behavior.</li>
<li>Agents/compaction: stop compaction-wait aborts from re-entering prompt failover and replaying completed tool turns. (#62600) Thanks @i-dentifier.</li>
<li>Approvals/runtime: move native approval lifecycle assembly into shared core bootstrap/runtime seams driven by channel capabilities and runtime contexts, and remove the legacy bundled approval fallback wiring. (#62135) Thanks @gumadeiras.</li>
<li>Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk.</li>
<li>Logging: make <code>logging.level</code> and <code>logging.consoleLevel</code> honor the documented severity threshold ordering again, and keep child loggers inheriting the parent <code>minLevel</code>. (#44646) Thanks @zhumengzhu.</li>
<li>Agents/sessions_send: pass <code>threadId</code> through announce delivery so cross-session notifications land in the correct Telegram forum topic instead of the group's general thread. (#62758) Thanks @jalehman.</li>
<li>Daemon/systemd: keep sudo systemctl calls scoped to the invoking user when machine-scoped systemctl fails, while still avoiding machine fallback for permission-denied user bus errors. (#62337) Thanks @Aftabbs.</li>
<li>Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.</li>
<li>Agents/exec: keep timed-out shell-backgrounded commands on the failed path and point long-running jobs to exec background/yield sessions so process polling is only suggested for registered sessions.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.7/OpenClaw-2026.4.7.zip" length="25324827" type="application/octet-stream" sparkle:edSignature="RyFWRz1trE/qvOiInD4vR6je9wx7fUTtHpZ94W8rMlZDByux9CyXOm/Anai96b9KyjTeQyC7YnJp5SRnYY3iCg=="/>
</item>
<item>
<title>2026.4.5</title>
<pubDate>Mon, 06 Apr 2026 04:55:17 +0100</pubDate>
@@ -471,5 +364,76 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.2/OpenClaw-2026.4.2.zip" length="25843797" type="application/octet-stream" sparkle:edSignature="bNNXr4BJEU8W7ghXOujLJTYHZL2PL/r/p4llGBw0BFL+46mJ2Bir+IK8XQaCj5zp+O5JSuh5mY+Y/Nrq6TR7Cg=="/>
</item>
<item>
<title>2026.4.1</title>
<pubDate>Wed, 01 Apr 2026 17:14:12 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026040190</sparkle:version>
<sparkle:shortVersionString>2026.4.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.1</h2>
<h3>Changes</h3>
<ul>
<li>Tasks/chat: add <code>/tasks</code> as a chat-native background task board for the current session, with recent task details and agent-local fallback counts when no linked tasks are visible. Related #54226. Thanks @vincentkoc.</li>
<li>Web search/SearXNG: add the bundled SearXNG provider plugin for <code>web_search</code> with configurable host support. (#57317) Thanks @cgdusek.</li>
<li>Amazon Bedrock/Guardrails: add Bedrock Guardrails support to the bundled provider. (#58588) Thanks @MikeORed.</li>
<li>macOS/Voice Wake: add the Voice Wake option to trigger Talk Mode. (#58490) Thanks @SmoothExec.</li>
<li>Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and <code>feishu_drive</code> comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.</li>
<li>Gateway/webchat: make <code>chat.history</code> text truncation configurable with <code>gateway.webchat.chatHistoryMaxChars</code> and per-request <code>maxChars</code>, while preserving silent-reply filtering and existing default payload limits. (#58900)</li>
<li>Agents/default params: add <code>agents.defaults.params</code> for global default provider parameters. (#58548) Thanks @lpender.</li>
<li>Agents/failover: cap prompt-side and assistant-side same-provider auth-profile retries for rate-limit failures before cross-provider model fallback, add the <code>auth.cooldowns.rateLimitedProfileRotations</code> knob, and document the new fallback behavior. (#58707) Thanks @Forgely3D</li>
<li>Cron/tools allowlist: add <code>openclaw cron --tools</code> for per-job tool allowlists. (#58504) Thanks @andyk-ms.</li>
<li>Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.</li>
<li>WhatsApp/reactions: add <code>reactionLevel</code> guidance for agent reactions. Thanks @mcaxtr.</li>
<li>Telegram/errors: add configurable <code>errorPolicy</code> and <code>errorCooldownMs</code> controls so Telegram can suppress repeated delivery errors per account, chat, and topic without muting distinct failures. (#51914) Thanks @chinar-amrutkar</li>
<li>ZAI/models: add <code>glm-5.1</code> and <code>glm-5v-turbo</code> to the bundled Z.AI provider catalog. (#58793) Thanks @tomsun28</li>
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Chat/error replies: stop leaking raw provider/runtime failures into external chat channels, return a friendly retry message instead, and add a specific <code>/new</code> hint for Bedrock toolResult/toolUse session mismatches. (#58831) Thanks @ImLukeF.</li>
<li>Gateway/reload: ignore startup config writes by persisted hash in the config reloader so generated auth tokens and seeded Control UI origins do not trigger a restart loop, while real <code>gateway.auth.*</code> edits still require restart. (#58678) Thanks @yelog</li>
<li>Tasks/gateway: keep the task registry maintenance sweep from stalling the gateway event loop under synchronous SQLite pressure, so upgraded gateways stop hanging about a minute after startup. (#58670) Thanks @openperf</li>
<li>Tasks/status: hide stale completed background tasks from <code>/status</code> and <code>session_status</code>, prefer live task context, and show recent failures only when no active work remains. (#58661) Thanks @vincentkoc</li>
<li>Tasks/gateway: re-check the current task record before maintenance marks runs lost or prunes them, so a task heartbeat or cleanup update that lands during a sweep no longer gets overwritten by stale snapshot state.</li>
<li>Exec/approvals: honor <code>exec-approvals.json</code> security defaults when inline or configured tool policy is unset, and keep Slack and Discord native approval handling aligned with inferred approvers and real channel enablement so remote exec stops falling into false approval timeouts and disabled states. Thanks @scoootscooob and @vincentkoc.</li>
<li>Exec/approvals: make <code>allow-always</code> persist as durable user-approved trust instead of behaving like <code>allow-once</code>, reuse exact-command trust on shell-wrapper paths that cannot safely persist an executable allowlist entry, keep static allowlist entries from silently bypassing <code>ask:"always"</code>, and require explicit approval when Windows cannot build an allowlist execution plan instead of hard-dead-ending remote exec. Thanks @scoootscooob and @vincentkoc.</li>
<li>Exec/cron: resolve isolated cron no-route approval dead-ends from the effective host fallback policy when trusted automation is allowed, and make <code>openclaw doctor</code> warn when <code>tools.exec</code> is broader than <code>~/.openclaw/exec-approvals.json</code> so stricter host-policy conflicts are explicit. Thanks @scoootscooob and @vincentkoc.</li>
<li>Sessions/model switching: keep <code>/model</code> changes queued behind busy runs instead of interrupting the active turn, and retarget queued followups so later work picks up the new model as soon as the current turn finishes.</li>
<li>Gateway/HTTP: skip failing HTTP request stages so one broken facade no longer forces every HTTP endpoint to return 500. (#58746) Thanks @yelog</li>
<li>Gateway/nodes: stop pinning live node commands to the approved node-pair record. Node pairing remains a trust/token flow, while per-node <code>system.run</code> policy stays in that node's exec approvals config. Fixes #58824.</li>
<li>WebChat/exec approvals: use native approval UI guidance in agent system prompts instead of telling agents to paste manual <code>/approve</code> commands in webchat sessions. Thanks @vincentkoc.</li>
<li>Web UI/OpenResponses: preserve rewritten stream snapshots in webchat and keep OpenResponses final streamed text aligned when models rewind earlier output. (#58641) Thanks @neeravmakwana</li>
<li>Discord/inbound media: pass Discord attachment and sticker downloads through the shared idle-timeout and worker-abort path so slow or stuck inbound media fetches stop hanging message processing. (#58593) Thanks @aquaright1</li>
<li>Telegram/retries: keep non-idempotent sends on the strict safe-send path, retry wrapped pre-connect failures, and preserve <code>429</code> / <code>retry_after</code> backoff for safe delivery retries. (#51895) Thanks @chinar-amrutkar</li>
<li>Telegram/exec approvals: route topic-aware exec approval followups through Telegram-owned threading and approval-target parsing, so forum-topic approvals stay in the originating topic instead of falling back to the root chat. (#58783)</li>
<li>Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov</li>
<li>Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae</li>
<li>Channels/QQ Bot: keep <code>/bot-logs</code> export gated behind a truly explicit QQBot allowlist, rejecting wildcard and mixed wildcard entries while preserving the real framework command path. Thanks @vincentkoc.</li>
<li>Channels/plugins: keep bundled channel plugins loadable from legacy <code>channels.<id></code> config even under restrictive plugin allowlists, and make <code>openclaw doctor</code> warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus</li>
<li>Plugins/bundled runtimes: restore externalized bundled plugin runtime dependency staging across packed installs, Docker builds, and local runtime staging so bundled plugins keep their declared runtime deps after the 2026.3.31 externalization change. (#58782)</li>
<li>LINE/runtime: resolve the packaged runtime contract from the built <code>dist/plugins/runtime</code> layout so LINE channels start correctly again after global npm installs on <code>2026.3.31</code>. (#58799) Thanks @vincentkoc.</li>
<li>MiniMax/plugins: auto-enable the bundled MiniMax plugin for API-key auth/config so MiniMax image generation and other plugin-owned capabilities load without manual plugin allowlisting. (#57127) Thanks @tars90percent.</li>
<li>Ollama/model picker: show only Ollama models after provider selection in the CLI picker. (#55290) Thanks @Luckymingxuan.</li>
<li>CDP/profiles: prefer <code>cdpPort</code> over stale WebSocket URLs so browser automation reconnects cleanly. (#58499) Thanks @Mlightsnow.</li>
<li>Media/paths: resolve relative <code>MEDIA</code> paths against the agent workspace so local attachment references keep working. (#58624) Thanks @aquaright1.</li>
<li>Memory/session indexing: keep full reindexes from skipping session transcripts when sync is triggered by <code>session-start</code> or <code>watch</code>, so restart-driven reindexes preserve session memory. (#39732) Thanks @upupc</li>
<li>Memory/QMD: prefer <code>--mask</code> over <code>--glob</code> when creating QMD collections so default memory collections keep their intended patterns and stop colliding on restart. (#58643) Thanks @GitZhangChi.</li>
<li>Subagents/tasks: keep subagent completion and cleanup from crashing when task-registry writes fail, so a corrupt or missing task row no longer takes down the gateway during lifecycle finalization. Thanks @vincentkoc.</li>
<li>Sandbox/browser: compare browser runtime inspection against <code>agents.defaults.sandbox.browser.image</code> so <code>openclaw sandbox list --browser</code> stops reporting healthy browser containers as image mismatches. (#58759) Thanks @sandpile.</li>
<li>Plugins/install: forward <code>--dangerously-force-unsafe-install</code> through archive and npm-spec plugin installs so the documented override reaches the security scanner on those install paths. (#58879) Thanks @ryanlee-gemini.</li>
<li>Auto-reply/commands: strip inbound metadata before slash command detection so wrapped <code>/model</code>, <code>/new</code>, and <code>/status</code> commands are recognized. (#58725) Thanks @Mlightsnow.</li>
<li>Agents/Anthropic: preserve thinking blocks and signatures across replay, cache-control patching, and context pruning so compacted Anthropic sessions continue working instead of failing on later turns. (#58916) Thanks @obviyus</li>
<li>Agents/failover: unify structured and raw provider error classification so provider-specific <code>400</code>/<code>422</code> payloads no longer get forced into generic format failures before retry, billing, or compaction logic can inspect them. (#58856) Thanks @aaron-he-zhu.</li>
<li>Auth profiles/store: coerce misplaced SecretRef objects out of plaintext <code>key</code> and <code>token</code> fields during store load so agents without ACP runtime stop crashing on <code>.trim()</code> after upgrade. (#58923) Thanks @openperf.</li>
<li>ACPX/runtime: repair <code>queue owner unavailable</code> session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana</li>
<li>ACPX/runtime: retry dead-session queue-owner repair without <code>--resume-session</code> when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.</li>
<li>Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to <code>auth-profiles.json</code> before returning them, so rotated Codex refresh tokens survive restart and stop falling into <code>refresh_token_reused</code> loops. (#53082)</li>
<li>Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.1/OpenClaw-2026.4.1.zip" length="25841903" type="application/octet-stream" sparkle:edSignature="0TPiyshScmwDbgs626JU08NOUUFJmIsVFa5g0xmizfl64Fr+IoT4l/dkXarFqbZAJidtj5WN7Bff7fG8ye/7AA=="/>
</item>
</channel>
</rss>
</rss>

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026040801
versionName = "2026.4.8"
versionCode = 2026040601
versionName = "2026.4.6"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.4.8
OPENCLAW_MARKETING_VERSION = 2026.4.8
OPENCLAW_BUILD_VERSION = 2026040801
OPENCLAW_GATEWAY_VERSION = 2026.4.6
OPENCLAW_MARKETING_VERSION = 2026.4.6
OPENCLAW_BUILD_VERSION = 2026040601
#include? "../build/Version.xcconfig"

View File

@@ -1,5 +1,4 @@
import Foundation
import OpenClawKit
enum GatewayConnectionIssue: Equatable {
case none
@@ -30,37 +29,6 @@ enum GatewayConnectionIssue: Equatable {
return false
}
static func detect(problem: GatewayConnectionProblem?) -> Self {
guard let problem else { return .none }
if problem.needsPairingApproval {
return .pairingRequired(requestId: problem.requestId)
}
if problem.needsCredentialUpdate {
return problem.kind == .gatewayAuthTokenMissing ? .tokenMissing : .unauthorized
}
switch problem.kind {
case .deviceIdentityRequired,
.deviceSignatureExpired,
.deviceNonceRequired,
.deviceNonceMismatch,
.deviceSignatureInvalid,
.devicePublicKeyInvalid,
.deviceIdMismatch,
.tailscaleIdentityMissing,
.tailscaleProxyMissing,
.tailscaleWhoisFailed,
.tailscaleIdentityMismatch,
.authRateLimited:
return .unauthorized
case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled:
return .network
case .unknown:
return .unknown(problem.message)
default:
return .none
}
}
static func detect(from statusText: String) -> Self {
let trimmed = statusText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return .none }

View File

@@ -1,232 +0,0 @@
import OpenClawKit
import SwiftUI
import UIKit
struct GatewayProblemBanner: View {
let problem: GatewayConnectionProblem
var primaryActionTitle: String?
var onPrimaryAction: (() -> Void)?
var onShowDetails: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: self.iconName)
.font(.headline.weight(.semibold))
.foregroundStyle(self.tint)
.frame(width: 20)
.padding(.top, 2)
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.problem.title)
.font(.subheadline.weight(.semibold))
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
Text(self.ownerLabel)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
Text(self.problem.message)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
if let requestId = self.problem.requestId {
Text("Request ID: \(requestId)")
.font(.system(.caption, design: .monospaced).weight(.medium))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
}
HStack(spacing: 10) {
if let primaryActionTitle, let onPrimaryAction {
Button(primaryActionTitle, action: onPrimaryAction)
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
if let onShowDetails {
Button("Details", action: onShowDetails)
.buttonStyle(.bordered)
.controlSize(.small)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.background(
.thinMaterial,
in: RoundedRectangle(cornerRadius: 16, style: .continuous)
)
}
private var iconName: String {
switch self.problem.kind {
case .pairingRequired,
.pairingRoleUpgradeRequired,
.pairingScopeUpgradeRequired,
.pairingMetadataUpgradeRequired:
return "person.crop.circle.badge.clock"
case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled:
return "wifi.exclamationmark"
case .deviceIdentityRequired,
.deviceSignatureExpired,
.deviceNonceRequired,
.deviceNonceMismatch,
.deviceSignatureInvalid,
.devicePublicKeyInvalid,
.deviceIdMismatch:
return "lock.shield"
default:
return "exclamationmark.triangle.fill"
}
}
private var tint: Color {
switch self.problem.kind {
case .pairingRequired,
.pairingRoleUpgradeRequired,
.pairingScopeUpgradeRequired,
.pairingMetadataUpgradeRequired:
return .orange
case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled:
return .yellow
default:
return .red
}
}
private var ownerLabel: String {
switch self.problem.owner {
case .gateway:
return "Fix on gateway"
case .iphone:
return "Fix on iPhone"
case .both:
return "Check both"
case .network:
return "Check network"
case .unknown:
return "Needs attention"
}
}
}
struct GatewayProblemDetailsSheet: View {
@Environment(\.dismiss) private var dismiss
let problem: GatewayConnectionProblem
var primaryActionTitle: String?
var onPrimaryAction: (() -> Void)?
@State private var copyFeedback: String?
var body: some View {
NavigationStack {
List {
Section {
VStack(alignment: .leading, spacing: 10) {
Text(self.problem.title)
.font(.title3.weight(.semibold))
Text(self.problem.message)
.font(.body)
.foregroundStyle(.secondary)
Text(self.ownerSummary)
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
if let requestId = self.problem.requestId {
Section("Request") {
Text(verbatim: requestId)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
Button("Copy request ID") {
UIPasteboard.general.string = requestId
self.copyFeedback = "Copied request ID"
}
}
}
if let actionCommand = self.problem.actionCommand {
Section("Gateway command") {
Text(verbatim: actionCommand)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
Button("Copy command") {
UIPasteboard.general.string = actionCommand
self.copyFeedback = "Copied command"
}
}
}
if let docsURL = self.problem.docsURL {
Section("Help") {
Link(destination: docsURL) {
Label("Open docs", systemImage: "book")
}
Text(verbatim: docsURL.absoluteString)
.font(.footnote)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
if let technicalDetails = self.problem.technicalDetails {
Section("Technical details") {
Text(verbatim: technicalDetails)
.font(.system(.footnote, design: .monospaced))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
if let copyFeedback {
Section {
Text(copyFeedback)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Connection problem")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
if let primaryActionTitle, let onPrimaryAction {
Button(primaryActionTitle) {
self.dismiss()
onPrimaryAction()
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
self.dismiss()
}
}
}
}
}
private var ownerSummary: String {
switch self.problem.owner {
case .gateway:
return "Primary fix: gateway"
case .iphone:
return "Primary fix: this iPhone"
case .both:
return "Primary fix: check both this iPhone and the gateway"
case .network:
return "Primary fix: network or remote access"
case .unknown:
return "Primary fix: review details and retry"
}
}
}

View File

@@ -8,7 +8,6 @@ struct GatewayQuickSetupSheet: View {
@AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false
@State private var connecting: Bool = false
@State private var connectError: String?
@State private var showGatewayProblemDetails: Bool = false
var body: some View {
NavigationStack {
@@ -16,14 +15,6 @@ struct GatewayQuickSetupSheet: View {
Text("Connect to a Gateway?")
.font(.title2.bold())
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemBanner(
problem: gatewayProblem,
onShowDetails: {
self.showGatewayProblemDetails = true
})
}
if let candidate = self.bestCandidate {
VStack(alignment: .leading, spacing: 6) {
Text(verbatim: candidate.name)
@@ -36,7 +27,7 @@ struct GatewayQuickSetupSheet: View {
// Use verbatim strings so Bonjour-provided values can't be interpreted as
// localized format strings (which can crash with Objective-C exceptions).
Text(verbatim: "Discovery: \(self.gatewayController.discoveryStatusText)")
Text(verbatim: "Status: \(self.appModel.gatewayDisplayStatusText)")
Text(verbatim: "Status: \(self.appModel.gatewayStatusText)")
Text(verbatim: "Node: \(self.appModel.nodeStatusText)")
Text(verbatim: "Operator: \(self.appModel.operatorStatusText)")
}
@@ -113,11 +104,6 @@ struct GatewayQuickSetupSheet: View {
}
}
}
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemDetailsSheet(problem: gatewayProblem)
}
}
}
private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? {

View File

@@ -120,10 +120,6 @@ final class NodeAppModel {
// multiple pending requests and cause the onboarding UI to "flip-flop".
var gatewayPairingPaused: Bool = false
var gatewayPairingRequestId: String?
private(set) var lastGatewayProblem: GatewayConnectionProblem?
var gatewayDisplayStatusText: String {
self.lastGatewayProblem?.statusText ?? self.gatewayStatusText
}
var seamColorHex: String?
private var mainSessionBaseKey: String = "main"
var selectedAgentId: String?
@@ -1819,7 +1815,6 @@ extension NodeAppModel {
self.gatewayAutoReconnectEnabled = false
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.lastGatewayProblem = nil
self.nodeGatewayTask?.cancel()
self.nodeGatewayTask = nil
self.operatorGatewayTask?.cancel()
@@ -1853,7 +1848,6 @@ private extension NodeAppModel {
self.gatewayAutoReconnectEnabled = true
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.lastGatewayProblem = nil
self.nodeGatewayTask?.cancel()
self.operatorGatewayTask?.cancel()
self.gatewayHealthMonitor.stop()
@@ -1872,38 +1866,6 @@ private extension NodeAppModel {
self.apnsLastRegisteredTokenHex = nil
}
func clearGatewayConnectionProblem() {
self.lastGatewayProblem = nil
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
}
func applyGatewayConnectionProblem(_ problem: GatewayConnectionProblem) {
self.lastGatewayProblem = problem
self.gatewayStatusText = problem.statusText
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil
self.gatewayConnected = false
self.showLocalCanvasOnDisconnect()
if problem.pauseReconnect {
self.gatewayAutoReconnectEnabled = false
}
if problem.needsPairingApproval {
self.gatewayPairingPaused = true
self.gatewayPairingRequestId = problem.requestId
} else {
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
}
}
func shouldKeepGatewayProblemStatus(forDisconnectReason reason: String) -> Bool {
guard let lastGatewayProblem else { return false }
return GatewayConnectionProblemMapper.shouldPreserve(
previousProblem: lastGatewayProblem,
overDisconnectReason: reason)
}
func shouldStartOperatorGatewayLoop(
token: String?,
bootstrapToken: String?,
@@ -2200,7 +2162,6 @@ private extension NodeAppModel {
onConnected: { [weak self] in
guard let self else { return }
await MainActor.run {
self.clearGatewayConnectionProblem()
self.gatewayStatusText = "Connected"
self.gatewayServerName = url.host ?? "gateway"
self.gatewayConnected = true
@@ -2257,13 +2218,7 @@ private extension NodeAppModel {
onDisconnected: { [weak self] reason in
guard let self else { return }
await MainActor.run {
if self.shouldKeepGatewayProblemStatus(forDisconnectReason: reason),
let lastGatewayProblem = self.lastGatewayProblem
{
self.gatewayStatusText = lastGatewayProblem.statusText
} else {
self.gatewayStatusText = "Disconnected: \(reason)"
}
self.gatewayStatusText = "Disconnected: \(reason)"
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil
self.gatewayConnected = false
@@ -2302,25 +2257,50 @@ private extension NodeAppModel {
}
attempt += 1
let problem = await MainActor.run {
let nextProblem = GatewayConnectionProblemMapper.map(
error: error,
preserving: self.lastGatewayProblem)
if let nextProblem {
self.applyGatewayConnectionProblem(nextProblem)
} else {
self.lastGatewayProblem = nil
self.gatewayStatusText = "Gateway error: \(error.localizedDescription)"
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil
self.gatewayConnected = false
self.showLocalCanvasOnDisconnect()
}
return nextProblem
await MainActor.run {
self.gatewayStatusText = "Gateway error: \(error.localizedDescription)"
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil
self.gatewayConnected = false
self.showLocalCanvasOnDisconnect()
}
GatewayDiagnostics.log("gateway connect error: \(error.localizedDescription)")
if problem?.needsPairingApproval == true {
// If auth is missing/rejected, pause reconnect churn until the user intervenes.
// Reconnect loops only spam the same failing handshake and make onboarding noisy.
let lower = error.localizedDescription.lowercased()
if lower.contains("unauthorized") || lower.contains("gateway token missing") {
await MainActor.run {
self.gatewayAutoReconnectEnabled = false
}
}
// If pairing is required, stop reconnect churn. The user must approve the request
// on the gateway before another connect attempt will succeed, and retry loops can
// generate multiple pending requests.
if lower.contains("not_paired") || lower.contains("pairing required") {
let requestId: String? = {
// GatewayResponseError for connect decorates the message with `(requestId: ...)`.
// Keep this resilient since other layers may wrap the text.
let text = error.localizedDescription
guard let start = text.range(of: "(requestId: ")?.upperBound else { return nil }
guard let end = text[start...].firstIndex(of: ")") else { return nil }
let raw = String(text[start..<end]).trimmingCharacters(in: .whitespacesAndNewlines)
return raw.isEmpty ? nil : raw
}()
await MainActor.run {
self.gatewayAutoReconnectEnabled = false
self.gatewayPairingPaused = true
self.gatewayPairingRequestId = requestId
if let requestId, !requestId.isEmpty {
self.gatewayStatusText =
"Pairing required (requestId: \(requestId)). "
+ "Approve on gateway and return to OpenClaw."
} else {
self.gatewayStatusText =
"Pairing required. Approve on gateway and return to OpenClaw."
}
}
// Hard stop the underlying WebSocket watchdog reconnects so the UI stays stable and
// we don't generate multiple pending requests while waiting for approval.
pausedForPairingApproval = true
@@ -2331,10 +2311,6 @@ private extension NodeAppModel {
break
}
if problem?.pauseReconnect == true {
continue
}
let sleepSeconds = min(8.0, 0.5 * pow(1.7, Double(attempt)))
try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000))
}
@@ -2346,7 +2322,6 @@ private extension NodeAppModel {
}
await MainActor.run {
self.lastGatewayProblem = nil
self.gatewayStatusText = "Offline"
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil

View File

@@ -376,7 +376,7 @@ private struct ConnectionStatusBox: View {
gatewayController: GatewayConnectionController
) -> [String] {
var lines: [String] = [
"gateway: \(appModel.gatewayDisplayStatusText)",
"gateway: \(appModel.gatewayStatusText)",
"discovery: \(gatewayController.discoveryStatusText)",
]
lines.append("server: \(appModel.gatewayServerName ?? "")")

View File

@@ -69,7 +69,6 @@ struct OnboardingWizardView: View {
@State private var showQRScanner: Bool = false
@State private var scannerError: String?
@State private var selectedPhoto: PhotosPickerItem?
@State private var showGatewayProblemDetails: Bool = false
@State private var lastPairingAutoResumeAttemptAt: Date?
private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
@@ -87,10 +86,6 @@ struct OnboardingWizardView: View {
self.step == .intro || self.step == .welcome || self.step == .success
}
private var currentProblem: GatewayConnectionProblem? {
self.appModel.lastGatewayProblem
}
var body: some View {
NavigationStack {
Group {
@@ -221,16 +216,6 @@ struct OnboardingWizardView: View {
}
}
}
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let currentProblem = self.currentProblem {
GatewayProblemDetailsSheet(
problem: currentProblem,
primaryActionTitle: "Retry",
onPrimaryAction: {
Task { await self.retryLastAttempt() }
})
}
}
.onAppear {
self.initializeState()
}
@@ -265,11 +250,39 @@ struct OnboardingWizardView: View {
.onChange(of: self.gatewayPassword) { _, newValue in
self.saveGatewayCredentials(token: self.gatewayToken, password: newValue)
}
.onChange(of: self.appModel.lastGatewayProblem) { _, newValue in
self.updateConnectionIssue(problem: newValue, statusText: self.appModel.gatewayStatusText)
}
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
self.updateConnectionIssue(problem: self.appModel.lastGatewayProblem, statusText: newValue)
let next = GatewayConnectionIssue.detect(from: newValue)
// Avoid "flip-flopping" the UI by clearing actionable issues when the underlying connection
// transitions through intermediate statuses (e.g. Offline/Connecting while reconnect churns).
if self.issue.needsPairing, next.needsPairing {
// Keep the requestId sticky even if the status line omits it after we pause.
let mergedRequestId = next.requestId ?? self.issue.requestId ?? self.pairingRequestId
self.issue = .pairingRequired(requestId: mergedRequestId)
} else if self.issue.needsPairing, !next.needsPairing {
// Ignore non-pairing statuses until the user explicitly retries/scans again, or we connect.
} else if self.issue.needsAuthToken, !next.needsAuthToken, !next.needsPairing {
// Same idea for auth: once we learn credentials are missing/rejected, keep that sticky until
// the user retries/scans again or we successfully connect.
} else {
self.issue = next
}
if let requestId = next.requestId, !requestId.isEmpty {
self.pairingRequestId = requestId
}
// If the gateway tells us auth is missing/rejected, stop reconnect churn until the user intervenes.
if next.needsAuthToken {
self.appModel.gatewayAutoReconnectEnabled = false
}
if self.issue.needsAuthToken || self.issue.needsPairing {
self.step = .auth
}
if !newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
self.connectMessage = newValue
self.statusLine = newValue
}
}
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
guard newValue != nil else { return }
@@ -496,7 +509,7 @@ struct OnboardingWizardView: View {
Section {
LabeledContent("Mode", value: selectedMode.title)
LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
LabeledContent("Status", value: self.appModel.gatewayDisplayStatusText)
LabeledContent("Status", value: self.appModel.gatewayStatusText)
LabeledContent("Progress", value: self.statusLine)
} header: {
Text("Status")
@@ -599,17 +612,7 @@ struct OnboardingWizardView: View {
.autocorrectionDisabled()
SecureField("Gateway Password", text: self.$gatewayPassword)
if let problem = self.currentProblem {
GatewayProblemBanner(
problem: problem,
primaryActionTitle: "Retry connection",
onPrimaryAction: {
Task { await self.retryLastAttempt() }
},
onShowDetails: {
self.showGatewayProblemDetails = true
})
} else if self.issue.needsAuthToken {
if self.issue.needsAuthToken {
Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.")
.font(.footnote)
.foregroundStyle(.secondary)
@@ -632,15 +635,14 @@ struct OnboardingWizardView: View {
Text("Pairing Approval")
} footer: {
let requestLine: String = {
if let id = self.currentProblem?.requestId ?? self.issue.requestId, !id.isEmpty {
if let id = self.issue.requestId, !id.isEmpty {
return "Request ID: \(id)"
}
return "Request ID: check `openclaw devices list`."
}()
let commandLine = self.currentProblem?.actionCommand ?? "openclaw devices approve <requestId>"
Text(
"Approve this device on the gateway.\n"
+ "1) `\(commandLine)`\n"
+ "1) `openclaw devices approve` (or `openclaw devices approve <requestId>`)\n"
+ "2) `/pair approve` in your OpenClaw chat\n"
+ "\(requestLine)\n"
+ "OpenClaw will also retry automatically when you return to this app.")
@@ -822,45 +824,6 @@ struct OnboardingWizardView: View {
self.resumeAfterPairingApprovalInBackground()
}
private func updateConnectionIssue(problem: GatewayConnectionProblem?, statusText: String) {
let next = GatewayConnectionIssue.detect(problem: problem)
let fallback = next == .none ? GatewayConnectionIssue.detect(from: statusText) : next
// Avoid "flip-flopping" the UI by clearing actionable issues when the underlying connection
// transitions through intermediate statuses (e.g. Offline/Connecting while reconnect churns).
if self.issue.needsPairing, fallback.needsPairing {
let mergedRequestId = fallback.requestId ?? self.issue.requestId ?? self.pairingRequestId
self.issue = .pairingRequired(requestId: mergedRequestId)
} else if self.issue.needsPairing, !fallback.needsPairing {
// Ignore non-pairing statuses until the user explicitly retries/scans again, or we connect.
} else if self.issue.needsAuthToken, !fallback.needsAuthToken, !fallback.needsPairing {
// Same idea for auth: once we learn credentials are missing/rejected, keep that sticky until
// the user retries/scans again or we successfully connect.
} else {
self.issue = fallback
}
if let requestId = problem?.requestId ?? fallback.requestId, !requestId.isEmpty {
self.pairingRequestId = requestId
}
if self.issue.needsAuthToken || self.issue.needsPairing || problem?.pauseReconnect == true {
self.step = .auth
}
if let problem {
self.connectMessage = problem.message
self.statusLine = problem.message
return
}
let trimmedStatus = statusText.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedStatus.isEmpty {
self.connectMessage = trimmedStatus
self.statusLine = trimmedStatus
}
}
private func detectQRCode(from data: Data) -> String? {
guard let ciImage = CIImage(data: data) else { return nil }
let detector = CIDetector(

View File

@@ -98,9 +98,6 @@ struct RootCanvas: View {
},
openSettings: {
self.presentedSheet = .settings
},
retryGatewayConnection: {
Task { await self.gatewayController.connectLastKnown() }
})
.preferredColorScheme(.dark)
@@ -232,7 +229,7 @@ struct RootCanvas: View {
private func updateCanvasDebugStatus() {
self.appModel.screen.setDebugStatusEnabled(self.canvasDebugStatusEnabled)
guard self.canvasDebugStatusEnabled else { return }
let title = self.appModel.gatewayDisplayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let title = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let subtitle = self.appModel.gatewayServerName ?? self.appModel.gatewayRemoteAddress
self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle)
}
@@ -457,7 +454,6 @@ private struct CanvasContent: View {
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@State private var showGatewayActions: Bool = false
@State private var showGatewayProblemDetails: Bool = false
var systemColorScheme: ColorScheme
var gatewayStatus: StatusPill.GatewayState
var voiceWakeEnabled: Bool
@@ -466,7 +462,6 @@ private struct CanvasContent: View {
var cameraHUDKind: NodeAppModel.CameraHUDKind?
var openChat: () -> Void
var openSettings: () -> Void
var retryGatewayConnection: () -> Void
private var brightenButtons: Bool { self.systemColorScheme == .light }
private var talkActive: Bool { self.appModel.talkMode.isEnabled || self.talkEnabled }
@@ -493,8 +488,6 @@ private struct CanvasContent: View {
onStatusTap: {
if self.gatewayStatus == .connected {
self.showGatewayActions = true
} else if self.appModel.lastGatewayProblem != nil {
self.showGatewayProblemDetails = true
} else {
self.openSettings()
}
@@ -511,35 +504,13 @@ private struct CanvasContent: View {
self.openSettings()
})
}
.overlay(alignment: .top) {
if let gatewayProblem = self.appModel.lastGatewayProblem,
self.gatewayStatus != .connected
{
GatewayProblemBanner(
problem: gatewayProblem,
primaryActionTitle: gatewayProblem.retryable ? "Retry" : "Open Settings",
onPrimaryAction: {
if gatewayProblem.retryable {
self.retryGatewayConnection()
} else {
self.openSettings()
}
},
onShowDetails: {
self.showGatewayProblemDetails = true
})
.padding(.horizontal, 12)
.safeAreaPadding(.top, 10)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.overlay(alignment: .topLeading) {
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
VoiceWakeToast(
command: voiceWakeToastText,
brighten: self.brightenButtons)
.padding(.leading, 10)
.safeAreaPadding(.top, self.appModel.lastGatewayProblem == nil ? 58 : 132)
.safeAreaPadding(.top, 58)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
@@ -547,16 +518,6 @@ private struct CanvasContent: View {
isPresented: self.$showGatewayActions,
onDisconnect: { self.appModel.disconnectGateway() },
onOpenSettings: { self.openSettings() })
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemDetailsSheet(
problem: gatewayProblem,
primaryActionTitle: "Open Settings",
onPrimaryAction: {
self.openSettings()
})
}
}
.onAppear {
// Keep the runtime talk state aligned with persisted toggle state on cold launch.
if self.talkEnabled != self.appModel.talkMode.isEnabled {

View File

@@ -9,7 +9,6 @@ struct RootTabs: View {
@State private var voiceWakeToastText: String?
@State private var toastDismissTask: Task<Void, Never>?
@State private var showGatewayActions: Bool = false
@State private var showGatewayProblemDetails: Bool = false
var body: some View {
TabView(selection: self.$selectedTab) {
@@ -33,8 +32,6 @@ struct RootTabs: View {
onTap: {
if self.gatewayStatus == .connected {
self.showGatewayActions = true
} else if self.appModel.lastGatewayProblem != nil {
self.showGatewayProblemDetails = true
} else {
self.selectedTab = 2
}
@@ -42,29 +39,11 @@ struct RootTabs: View {
.padding(.leading, 10)
.safeAreaPadding(.top, 10)
}
.overlay(alignment: .top) {
if let gatewayProblem = self.appModel.lastGatewayProblem,
self.gatewayStatus != .connected
{
GatewayProblemBanner(
problem: gatewayProblem,
primaryActionTitle: "Open Settings",
onPrimaryAction: {
self.selectedTab = 2
},
onShowDetails: {
self.showGatewayProblemDetails = true
})
.padding(.horizontal, 12)
.safeAreaPadding(.top, 10)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.overlay(alignment: .topLeading) {
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
VoiceWakeToast(command: voiceWakeToastText)
.padding(.leading, 10)
.safeAreaPadding(.top, self.appModel.lastGatewayProblem == nil ? 58 : 132)
.safeAreaPadding(.top, 58)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
@@ -95,16 +74,6 @@ struct RootTabs: View {
isPresented: self.$showGatewayActions,
onDisconnect: { self.appModel.disconnectGateway() },
onOpenSettings: { self.selectedTab = 2 })
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemDetailsSheet(
problem: gatewayProblem,
primaryActionTitle: "Open Settings",
onPrimaryAction: {
self.selectedTab = 2
})
}
}
}
private var gatewayStatus: StatusPill.GatewayState {

View File

@@ -53,7 +53,6 @@ struct SettingsTab: View {
@State private var selectedAgentPickerId: String = ""
@State private var showResetOnboardingAlert: Bool = false
@State private var showGatewayProblemDetails: Bool = false
@State private var activeFeatureHelp: FeatureHelp?
@State private var suppressCredentialPersist: Bool = false
@@ -64,20 +63,6 @@ struct SettingsTab: View {
Form {
Section {
DisclosureGroup(isExpanded: self.$gatewayExpanded) {
if let gatewayProblem = self.appModel.lastGatewayProblem,
!self.isGatewayConnected
{
GatewayProblemBanner(
problem: gatewayProblem,
primaryActionTitle: "Retry connection",
onPrimaryAction: {
Task { await self.retryGatewayConnectionFromProblem() }
},
onShowDetails: {
self.showGatewayProblemDetails = true
})
}
if !self.isGatewayConnected {
Text(
"1. Open a chat with your OpenClaw agent and send /pair\n"
@@ -138,7 +123,7 @@ struct SettingsTab: View {
if self.appModel.gatewayServerName == nil {
LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
}
LabeledContent("Status", value: self.appModel.gatewayDisplayStatusText)
LabeledContent("Status", value: self.appModel.gatewayStatusText)
Toggle("Auto-connect on launch", isOn: self.$gatewayAutoConnect)
if let serverName = self.appModel.gatewayServerName {
@@ -417,16 +402,6 @@ struct SettingsTab: View {
.accessibilityLabel("Close")
}
}
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemDetailsSheet(
problem: gatewayProblem,
primaryActionTitle: "Retry",
onPrimaryAction: {
Task { await self.retryGatewayConnectionFromProblem() }
})
}
}
.alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) {
Button("Reset", role: .destructive) {
self.resetOnboarding()
@@ -618,9 +593,6 @@ struct SettingsTab: View {
if let server = self.appModel.gatewayServerName, self.isGatewayConnected {
return server
}
if let problem = self.appModel.lastGatewayProblem {
return problem.statusText
}
let trimmed = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "Not connected" : trimmed
}
@@ -670,7 +642,7 @@ struct SettingsTab: View {
private func gatewayDebugText() -> String {
var lines: [String] = [
"gateway: \(self.appModel.gatewayDisplayStatusText)",
"gateway: \(self.appModel.gatewayStatusText)",
"discovery: \(self.gatewayController.discoveryStatusText)",
]
lines.append("server: \(self.appModel.gatewayServerName ?? "")")
@@ -917,9 +889,6 @@ struct SettingsTab: View {
}
private var setupStatusLine: String? {
if let problem = self.appModel.lastGatewayProblem {
return problem.message
}
let trimmedSetup = self.setupStatusText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
if let friendly = self.friendlyGatewayMessage(from: gatewayStatus) { return friendly }
@@ -1018,14 +987,6 @@ struct SettingsTab: View {
SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback)
}
private func retryGatewayConnectionFromProblem() async {
if self.manualGatewayEnabled || self.connectingGatewayID == "manual" {
await self.connectManual()
return
}
await self.connectLastKnown()
}
private func resetOnboarding() {
// Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
self.appModel.disconnectGateway()

View File

@@ -1,24 +1,11 @@
import Foundation
import OpenClawKit
enum GatewayStatusBuilder {
@MainActor
static func build(appModel: NodeAppModel) -> StatusPill.GatewayState {
self.build(
gatewayServerName: appModel.gatewayServerName,
lastGatewayProblem: appModel.lastGatewayProblem,
gatewayStatusText: appModel.gatewayStatusText)
}
if appModel.gatewayServerName != nil { return .connected }
static func build(
gatewayServerName: String?,
lastGatewayProblem: GatewayConnectionProblem?,
gatewayStatusText: String) -> StatusPill.GatewayState
{
if gatewayServerName != nil { return .connected }
if let lastGatewayProblem, lastGatewayProblem.pauseReconnect { return .error }
let text = gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let text = appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
if text.localizedCaseInsensitiveContains("connecting") ||
text.localizedCaseInsensitiveContains("reconnecting")
{

View File

@@ -16,31 +16,6 @@ enum StatusActivityBuilder {
tint: .orange)
}
if let gatewayProblem = appModel.lastGatewayProblem {
switch gatewayProblem.kind {
case .pairingRequired,
.pairingRoleUpgradeRequired,
.pairingScopeUpgradeRequired,
.pairingMetadataUpgradeRequired:
return StatusPill.Activity(
title: "Approval pending",
systemImage: "person.crop.circle.badge.clock",
tint: .orange)
case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled:
return StatusPill.Activity(
title: "Check network",
systemImage: "wifi.exclamationmark",
tint: .orange)
default:
if gatewayProblem.pauseReconnect {
return StatusPill.Activity(
title: "Action required",
systemImage: "exclamationmark.triangle.fill",
tint: .orange)
}
}
}
let gatewayStatus = appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let gatewayLower = gatewayStatus.lowercased()
if gatewayLower.contains("repair") {

View File

@@ -1,36 +0,0 @@
import OpenClawKit
import Testing
@testable import OpenClaw
@Suite struct GatewayStatusBuilderTests {
@Test func pausedProblemKeepsErrorStatus() {
let state = GatewayStatusBuilder.build(
gatewayServerName: nil,
lastGatewayProblem: GatewayConnectionProblem(
kind: .pairingRequired,
owner: .gateway,
title: "Pairing required",
message: "Approve this device before reconnecting.",
requestId: "req-123",
retryable: false,
pauseReconnect: true),
gatewayStatusText: "Reconnecting…")
#expect(state == .error)
}
@Test func transientProblemAllowsConnectingStatus() {
let state = GatewayStatusBuilder.build(
gatewayServerName: nil,
lastGatewayProblem: GatewayConnectionProblem(
kind: .timeout,
owner: .network,
title: "Connection timed out",
message: "The gateway did not respond before the connection timed out.",
retryable: true,
pauseReconnect: false),
gatewayStatusText: "Reconnecting…")
#expect(state == .connecting)
}
}

View File

@@ -6,180 +6,165 @@ import Foundation
enum HostEnvSecurityPolicy {
static let blockedKeys: Set<String> = [
"_JAVA_OPTIONS",
"ANT_OPTS",
"BASH_ENV",
"BROWSER",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CC",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CXX",
"DOTNET_ADDITIONAL_DEPS",
"DOTNET_STARTUP_HOOKS",
"ENV",
"GCONV_PATH",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_COMMON_DIR",
"GIT_DIR",
"GIT_EDITOR",
"GIT_EXEC_PATH",
"GIT_EXTERNAL_DIFF",
"GIT_INDEX_FILE",
"GIT_NAMESPACE",
"GIT_OBJECT_DIRECTORY",
"GIT_SEQUENCE_EDITOR",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_TEMPLATE_DIR",
"GIT_WORK_TREE",
"GLIBC_TUNABLES",
"GRADLE_OPTS",
"HGRCPATH",
"IFS",
"JAVA_OPTS",
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
"MAKEFLAGS",
"MAVEN_OPTS",
"MFLAGS",
"NODE_OPTIONS",
"NODE_PATH",
"PERL5LIB",
"PERL5OPT",
"PS4",
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"PERL5LIB",
"PERL5OPT",
"RUBYLIB",
"RUBYOPT",
"BASH_ENV",
"ENV",
"BROWSER",
"GIT_EDITOR",
"GIT_EXTERNAL_DIFF",
"GIT_EXEC_PATH",
"GIT_SEQUENCE_EDITOR",
"GIT_TEMPLATE_DIR",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"CC",
"CXX",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"RUSTC_WRAPPER",
"SBT_OPTS",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"SHELL",
"SHELLOPTS",
"SSLKEYLOGFILE"
"PS4",
"GCONV_PATH",
"IFS",
"SSLKEYLOGFILE",
"JAVA_TOOL_OPTIONS",
"_JAVA_OPTIONS",
"JDK_JAVA_OPTIONS",
"PYTHONBREAKPOINT",
"DOTNET_STARTUP_HOOKS",
"DOTNET_ADDITIONAL_DEPS",
"GLIBC_TUNABLES",
"MAVEN_OPTS",
"MAKEFLAGS",
"MFLAGS",
"SBT_OPTS",
"GRADLE_OPTS",
"ANT_OPTS",
"HGRCPATH"
]
static let blockedOverrideKeys: Set<String> = [
"ALL_PROXY",
"AWS_CONFIG_FILE",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"AZURE_AUTH_LOCATION",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
"COMPOSER_HOME",
"CORECLR_PROFILER_PATH",
"CPATH",
"CPLUS_INCLUDE_PATH",
"CURL_CA_BUNDLE",
"CURL_HOME",
"DENO_DIR",
"DOCKER_CERT_PATH",
"DOCKER_CONTEXT",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"EDITOR",
"FCEDIT",
"GEM_HOME",
"GEM_PATH",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_NAMESPACE",
"GIT_OBJECT_DIRECTORY",
"GIT_PAGER",
"GIT_PROXY_COMMAND",
"GIT_SSH",
"HOME",
"GRADLE_USER_HOME",
"ZDOTDIR",
"GIT_SSH_COMMAND",
"GIT_SSH",
"GIT_PROXY_COMMAND",
"GIT_ASKPASS",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_WORK_TREE",
"GOENV",
"GOFLAGS",
"GONOPROXY",
"GONOSUMCHECK",
"GONOSUMDB",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOPATH",
"GOPRIVATE",
"GOPROXY",
"GRADLE_USER_HOME",
"HELM_HOME",
"HGRCPATH",
"HISTFILE",
"HOME",
"HTTP_PROXY",
"HTTPS_PROXY",
"KUBECONFIG",
"LESSCLOSE",
"SSH_ASKPASS",
"LESSOPEN",
"LIBRARY_PATH",
"LUA_CPATH",
"LUA_PATH",
"MAKEFLAGS",
"MANPAGER",
"MFLAGS",
"NO_PROXY",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"OBJC_INCLUDE_PATH",
"OPENSSL_CONF",
"OPENSSL_ENGINES",
"LESSCLOSE",
"PAGER",
"MANPAGER",
"GIT_PAGER",
"EDITOR",
"VISUAL",
"FCEDIT",
"SUDO_EDITOR",
"PROMPT_COMMAND",
"HISTFILE",
"PERL5DB",
"PERL5DBCMD",
"PHP_INI_SCAN_DIR",
"OPENSSL_CONF",
"OPENSSL_ENGINES",
"PYTHONSTARTUP",
"WGETRC",
"CURL_HOME",
"CLASSPATH",
"CGO_CFLAGS",
"CGO_LDFLAGS",
"GOFLAGS",
"MAKEFLAGS",
"MFLAGS",
"CORECLR_PROFILER_PATH",
"PHPRC",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
"PHP_INI_SCAN_DIR",
"DENO_DIR",
"BUN_CONFIG_REGISTRY",
"YARN_RC_FILENAME",
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"NO_PROXY",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NODE_EXTRA_CA_CERTS",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"PIP_INDEX_URL",
"PIP_PYPI_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_CONFIG_FILE",
"PIP_FIND_LINKS",
"PIP_TRUSTED_HOST",
"PROMPT_COMMAND",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"REQUESTS_CA_BUNDLE",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"SSH_ASKPASS",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"SUDO_EDITOR",
"UV_DEFAULT_INDEX",
"UV_EXTRA_INDEX_URL",
"UV_INDEX",
"UV_INDEX_URL",
"UV_PYTHON",
"UV_EXTRA_INDEX_URL",
"UV_DEFAULT_INDEX",
"DOCKER_HOST",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"DOCKER_CONTEXT",
"LIBRARY_PATH",
"CPATH",
"C_INCLUDE_PATH",
"CPLUS_INCLUDE_PATH",
"OBJC_INCLUDE_PATH",
"NODE_EXTRA_CA_CERTS",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"GOPROXY",
"GONOSUMCHECK",
"GONOSUMDB",
"GONOPROXY",
"GOPRIVATE",
"GOENV",
"GOPATH",
"HGRCPATH",
"PYTHONUSERBASE",
"RUSTC_WRAPPER",
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"LUA_PATH",
"LUA_CPATH",
"GEM_HOME",
"GEM_PATH",
"BUNDLE_GEMFILE",
"COMPOSER_HOME",
"CARGO_BUILD_RUSTC_WRAPPER",
"XDG_CONFIG_HOME",
"YARN_RC_FILENAME",
"ZDOTDIR"
"AWS_CONFIG_FILE"
]
static let blockedOverridePrefixes: [String] = [
"CARGO_REGISTRIES_",
"GIT_CONFIG_",
"NPM_CONFIG_"
"NPM_CONFIG_",
"CARGO_REGISTRIES_"
]
static let blockedPrefixes: [String] = [
"BASH_FUNC_",
"DYLD_",
"LD_"
"LD_",
"BASH_FUNC_"
]
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.8</string>
<string>2026.4.6</string>
<key>CFBundleVersion</key>
<string>2026040801</string>
<string>2026040601</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -537,8 +537,6 @@ public struct AgentParams: Codable, Sendable {
public let besteffortdeliver: Bool?
public let lane: String?
public let extrasystemprompt: String?
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let idempotencykey: String
@@ -568,8 +566,6 @@ public struct AgentParams: Codable, Sendable {
besteffortdeliver: Bool?,
lane: String?,
extrasystemprompt: String?,
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
idempotencykey: String,
@@ -598,8 +594,6 @@ public struct AgentParams: Codable, Sendable {
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.extrasystemprompt = extrasystemprompt
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.idempotencykey = idempotencykey
@@ -630,8 +624,6 @@ public struct AgentParams: Codable, Sendable {
case besteffortdeliver = "bestEffortDeliver"
case lane
case extrasystemprompt = "extraSystemPrompt"
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case idempotencykey = "idempotencyKey"
@@ -1335,236 +1327,6 @@ public struct SessionsResolveParams: Codable, Sendable {
}
}
public struct SessionCompactionCheckpoint: Codable, Sendable {
public let checkpointid: String
public let sessionkey: String
public let sessionid: String
public let createdat: Int
public let reason: AnyCodable
public let tokensbefore: Int?
public let tokensafter: Int?
public let summary: String?
public let firstkeptentryid: String?
public let precompaction: [String: AnyCodable]
public let postcompaction: [String: AnyCodable]
public init(
checkpointid: String,
sessionkey: String,
sessionid: String,
createdat: Int,
reason: AnyCodable,
tokensbefore: Int?,
tokensafter: Int?,
summary: String?,
firstkeptentryid: String?,
precompaction: [String: AnyCodable],
postcompaction: [String: AnyCodable])
{
self.checkpointid = checkpointid
self.sessionkey = sessionkey
self.sessionid = sessionid
self.createdat = createdat
self.reason = reason
self.tokensbefore = tokensbefore
self.tokensafter = tokensafter
self.summary = summary
self.firstkeptentryid = firstkeptentryid
self.precompaction = precompaction
self.postcompaction = postcompaction
}
private enum CodingKeys: String, CodingKey {
case checkpointid = "checkpointId"
case sessionkey = "sessionKey"
case sessionid = "sessionId"
case createdat = "createdAt"
case reason
case tokensbefore = "tokensBefore"
case tokensafter = "tokensAfter"
case summary
case firstkeptentryid = "firstKeptEntryId"
case precompaction = "preCompaction"
case postcompaction = "postCompaction"
}
}
public struct SessionsCompactionListParams: Codable, Sendable {
public let key: String
public init(
key: String)
{
self.key = key
}
private enum CodingKeys: String, CodingKey {
case key
}
}
public struct SessionsCompactionGetParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionBranchParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionRestoreParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionListResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let checkpoints: [SessionCompactionCheckpoint]
public init(
ok: Bool,
key: String,
checkpoints: [SessionCompactionCheckpoint])
{
self.ok = ok
self.key = key
self.checkpoints = checkpoints
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case checkpoints
}
}
public struct SessionsCompactionGetResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let checkpoint: SessionCompactionCheckpoint
public init(
ok: Bool,
key: String,
checkpoint: SessionCompactionCheckpoint)
{
self.ok = ok
self.key = key
self.checkpoint = checkpoint
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case checkpoint
}
}
public struct SessionsCompactionBranchResult: Codable, Sendable {
public let ok: Bool
public let sourcekey: String
public let key: String
public let sessionid: String
public let checkpoint: SessionCompactionCheckpoint
public let entry: [String: AnyCodable]
public init(
ok: Bool,
sourcekey: String,
key: String,
sessionid: String,
checkpoint: SessionCompactionCheckpoint,
entry: [String: AnyCodable])
{
self.ok = ok
self.sourcekey = sourcekey
self.key = key
self.sessionid = sessionid
self.checkpoint = checkpoint
self.entry = entry
}
private enum CodingKeys: String, CodingKey {
case ok
case sourcekey = "sourceKey"
case key
case sessionid = "sessionId"
case checkpoint
case entry
}
}
public struct SessionsCompactionRestoreResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let sessionid: String
public let checkpoint: SessionCompactionCheckpoint
public let entry: [String: AnyCodable]
public init(
ok: Bool,
key: String,
sessionid: String,
checkpoint: SessionCompactionCheckpoint,
entry: [String: AnyCodable])
{
self.ok = ok
self.key = key
self.sessionid = sessionid
self.checkpoint = checkpoint
self.entry = entry
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case sessionid = "sessionId"
case checkpoint
case entry
}
}
public struct SessionsCreateParams: Codable, Sendable {
public let key: String?
public let agentid: String?

View File

@@ -624,31 +624,11 @@ public actor GatewayChannelActor {
let detailCode = details?["code"]?.value as? String
let canRetryWithDeviceToken = details?["canRetryWithDeviceToken"]?.value as? Bool ?? false
let recommendedNextStep = details?["recommendedNextStep"]?.value as? String
let requestId = details?["requestId"]?.value as? String
let reason = details?["reason"]?.value as? String
let owner = details?["owner"]?.value as? String
let title = details?["title"]?.value as? String
let userMessage = details?["userMessage"]?.value as? String
let actionLabel = details?["actionLabel"]?.value as? String
let actionCommand = details?["actionCommand"]?.value as? String
let docsURLString = details?["docsUrl"]?.value as? String
let retryableOverride = details?["retryable"]?.value as? Bool
let pauseReconnectOverride = details?["pauseReconnect"]?.value as? Bool
throw GatewayConnectAuthError(
message: msg,
detailCodeRaw: detailCode,
canRetryWithDeviceToken: canRetryWithDeviceToken,
recommendedNextStepRaw: recommendedNextStep,
requestId: requestId,
detailsReason: reason,
ownerRaw: owner,
titleOverride: title,
userMessageOverride: userMessage,
actionLabel: actionLabel,
actionCommand: actionCommand,
docsURLString: docsURLString,
retryableOverride: retryableOverride,
pauseReconnectOverride: pauseReconnectOverride)
recommendedNextStepRaw: recommendedNextStep)
}
guard let payload = res.payload else {
throw NSError(

View File

@@ -1,761 +0,0 @@
import Foundation
public struct GatewayConnectionProblem: Equatable, Sendable {
public enum Kind: String, Equatable, Sendable {
case gatewayAuthTokenMissing
case gatewayAuthTokenMismatch
case gatewayAuthTokenNotConfigured
case gatewayAuthPasswordMissing
case gatewayAuthPasswordMismatch
case gatewayAuthPasswordNotConfigured
case bootstrapTokenInvalid
case deviceTokenMismatch
case pairingRequired
case pairingRoleUpgradeRequired
case pairingScopeUpgradeRequired
case pairingMetadataUpgradeRequired
case deviceIdentityRequired
case deviceSignatureExpired
case deviceNonceRequired
case deviceNonceMismatch
case deviceSignatureInvalid
case devicePublicKeyInvalid
case deviceIdMismatch
case tailscaleIdentityMissing
case tailscaleProxyMissing
case tailscaleWhoisFailed
case tailscaleIdentityMismatch
case authRateLimited
case timeout
case connectionRefused
case reachabilityFailed
case websocketCancelled
case unknown
}
public enum Owner: String, Equatable, Sendable {
case gateway
case iphone
case both
case network
case unknown
}
public let kind: Kind
public let owner: Owner
public let title: String
public let message: String
public let actionLabel: String?
public let actionCommand: String?
public let docsURL: URL?
public let requestId: String?
public let retryable: Bool
public let pauseReconnect: Bool
public let technicalDetails: String?
public init(
kind: Kind,
owner: Owner,
title: String,
message: String,
actionLabel: String? = nil,
actionCommand: String? = nil,
docsURL: URL? = nil,
requestId: String? = nil,
retryable: Bool,
pauseReconnect: Bool,
technicalDetails: String? = nil)
{
self.kind = kind
self.owner = owner
self.title = title
self.message = message
self.actionLabel = Self.trimmedOrNil(actionLabel)
self.actionCommand = Self.trimmedOrNil(actionCommand)
self.docsURL = docsURL
self.requestId = Self.trimmedOrNil(requestId)
self.retryable = retryable
self.pauseReconnect = pauseReconnect
self.technicalDetails = Self.trimmedOrNil(technicalDetails)
}
public var needsPairingApproval: Bool {
switch self.kind {
case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, .pairingMetadataUpgradeRequired:
return true
default:
return false
}
}
public var needsCredentialUpdate: Bool {
switch self.kind {
case .gatewayAuthTokenMissing,
.gatewayAuthTokenMismatch,
.gatewayAuthTokenNotConfigured,
.gatewayAuthPasswordMissing,
.gatewayAuthPasswordMismatch,
.gatewayAuthPasswordNotConfigured,
.bootstrapTokenInvalid,
.deviceTokenMismatch:
return true
default:
return false
}
}
public var statusText: String {
switch self.kind {
case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, .pairingMetadataUpgradeRequired:
if let requestId {
return "\(self.title) (request ID: \(requestId))"
}
return self.title
default:
return self.title
}
}
private static func trimmedOrNil(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
}
public enum GatewayConnectionProblemMapper {
public static func map(error: Error, preserving previousProblem: GatewayConnectionProblem? = nil) -> GatewayConnectionProblem? {
guard let nextProblem = self.rawMap(error) else {
return nil
}
guard let previousProblem else {
return nextProblem
}
if self.shouldPreserve(previousProblem: previousProblem, over: nextProblem) {
return previousProblem
}
return nextProblem
}
public static func shouldPreserve(previousProblem: GatewayConnectionProblem, over nextProblem: GatewayConnectionProblem) -> Bool {
if nextProblem.kind == .websocketCancelled {
return previousProblem.pauseReconnect || previousProblem.requestId != nil
}
return false
}
public static func shouldPreserve(previousProblem: GatewayConnectionProblem, overDisconnectReason reason: String) -> Bool {
let normalized = reason.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !normalized.isEmpty else { return false }
if normalized.contains("cancelled") || normalized.contains("canceled") {
return previousProblem.pauseReconnect || previousProblem.requestId != nil
}
return false
}
private static func rawMap(_ error: Error) -> GatewayConnectionProblem? {
if let authError = error as? GatewayConnectAuthError {
return self.map(authError)
}
if let responseError = error as? GatewayResponseError {
return self.map(responseError)
}
return self.mapTransportError(error)
}
private static func map(_ authError: GatewayConnectAuthError) -> GatewayConnectionProblem {
let pairingCommand = self.approvalCommand(requestId: authError.requestId)
switch authError.detail {
case .authTokenMissing:
return self.problem(
kind: .gatewayAuthTokenMissing,
owner: .both,
title: authError.titleOverride ?? "Gateway token required",
message: authError.userMessageOverride
?? "This gateway requires an auth token, but this iPhone did not send one.",
actionLabel: authError.actionLabel ?? "Open Settings",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authTokenMismatch:
return self.problem(
kind: .gatewayAuthTokenMismatch,
owner: .both,
title: authError.titleOverride ?? "Gateway token is out of date",
message: authError.userMessageOverride
?? "The token on this iPhone does not match the gateway token.",
actionLabel: authError.actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"),
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: authError.retryableOverride ?? authError.canRetryWithDeviceToken,
pauseReconnect: authError.pauseReconnectOverride ?? !authError.canRetryWithDeviceToken,
authError: authError)
case .authTokenNotConfigured:
return self.problem(
kind: .gatewayAuthTokenNotConfigured,
owner: .gateway,
title: authError.titleOverride ?? "Gateway token is not configured",
message: authError.userMessageOverride
?? "This gateway is set to token auth, but no gateway token is configured on the gateway.",
actionLabel: authError.actionLabel ?? "Fix on gateway",
actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.token <new-token>",
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authPasswordMissing:
return self.problem(
kind: .gatewayAuthPasswordMissing,
owner: .both,
title: authError.titleOverride ?? "Gateway password required",
message: authError.userMessageOverride
?? "This gateway requires a password, but this iPhone did not send one.",
actionLabel: authError.actionLabel ?? "Open Settings",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authPasswordMismatch:
return self.problem(
kind: .gatewayAuthPasswordMismatch,
owner: .both,
title: authError.titleOverride ?? "Gateway password is out of date",
message: authError.userMessageOverride
?? "The saved password on this iPhone does not match the gateway password.",
actionLabel: authError.actionLabel ?? "Update password",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authPasswordNotConfigured:
return self.problem(
kind: .gatewayAuthPasswordNotConfigured,
owner: .gateway,
title: authError.titleOverride ?? "Gateway password is not configured",
message: authError.userMessageOverride
?? "This gateway is set to password auth, but no gateway password is configured on the gateway.",
actionLabel: authError.actionLabel ?? "Fix on gateway",
actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.password <new-password>",
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authBootstrapTokenInvalid:
return self.problem(
kind: .bootstrapTokenInvalid,
owner: .iphone,
title: authError.titleOverride ?? "Setup code expired",
message: authError.userMessageOverride
?? "The setup QR or bootstrap token is no longer valid.",
actionLabel: authError.actionLabel ?? "Scan QR again",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authDeviceTokenMismatch:
return self.problem(
kind: .deviceTokenMismatch,
owner: .both,
title: authError.titleOverride ?? "This iPhone's saved device token is no longer valid",
message: authError.userMessageOverride
?? "The gateway rejected the stored device token for this role.",
actionLabel: authError.actionLabel ?? "Repair pairing",
actionCommand: authError.actionCommand ?? pairingCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .pairingRequired:
return self.pairingProblem(for: authError)
case .controlUiDeviceIdentityRequired, .deviceIdentityRequired:
return self.problem(
kind: .deviceIdentityRequired,
owner: .iphone,
title: authError.titleOverride ?? "Secure device identity is required",
message: authError.userMessageOverride
?? "This connection must include a signed device identity before the gateway can bind permissions to this iPhone.",
actionLabel: authError.actionLabel ?? "Retry from the app",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .deviceAuthSignatureExpired:
return self.problem(
kind: .deviceSignatureExpired,
owner: .iphone,
title: authError.titleOverride ?? "Secure handshake expired",
message: authError.userMessageOverride ?? "The device signature is too old to use.",
actionLabel: authError.actionLabel ?? "Check iPhone time",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
requestId: authError.requestId,
retryable: true,
pauseReconnect: true,
authError: authError)
case .deviceAuthNonceRequired:
return self.problem(
kind: .deviceNonceRequired,
owner: .iphone,
title: authError.titleOverride ?? "Secure handshake is incomplete",
message: authError.userMessageOverride
?? "The gateway expected a one-time challenge response, but the nonce was missing.",
actionLabel: authError.actionLabel ?? "Retry",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
requestId: authError.requestId,
retryable: true,
pauseReconnect: true,
authError: authError)
case .deviceAuthNonceMismatch:
return self.problem(
kind: .deviceNonceMismatch,
owner: .iphone,
title: authError.titleOverride ?? "Secure handshake did not match",
message: authError.userMessageOverride ?? "The challenge response was stale or mismatched.",
actionLabel: authError.actionLabel ?? "Retry",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
requestId: authError.requestId,
retryable: true,
pauseReconnect: true,
authError: authError)
case .deviceAuthSignatureInvalid, .deviceAuthInvalid:
return self.problem(
kind: .deviceSignatureInvalid,
owner: .iphone,
title: authError.titleOverride ?? "This device identity could not be verified",
message: authError.userMessageOverride
?? "The gateway could not verify the identity this iPhone presented.",
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .deviceAuthPublicKeyInvalid:
return self.problem(
kind: .devicePublicKeyInvalid,
owner: .iphone,
title: authError.titleOverride ?? "This device identity could not be verified",
message: authError.userMessageOverride
?? "The gateway could not verify the public key this iPhone presented.",
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .deviceAuthDeviceIdMismatch:
return self.problem(
kind: .deviceIdMismatch,
owner: .iphone,
title: authError.titleOverride ?? "This device identity could not be verified",
message: authError.userMessageOverride
?? "The gateway rejected the device identity because the device ID did not match.",
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authTailscaleIdentityMissing:
return self.problem(
kind: .tailscaleIdentityMissing,
owner: .network,
title: authError.titleOverride ?? "Tailscale identity check failed",
message: authError.userMessageOverride
?? "This connection expected Tailscale identity headers, but they were not available.",
actionLabel: authError.actionLabel ?? "Turn on Tailscale",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authTailscaleProxyMissing:
return self.problem(
kind: .tailscaleProxyMissing,
owner: .network,
title: authError.titleOverride ?? "Tailscale identity check failed",
message: authError.userMessageOverride
?? "The gateway expected a Tailscale auth proxy, but it was not configured.",
actionLabel: authError.actionLabel ?? "Review Tailscale setup",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authTailscaleWhoisFailed:
return self.problem(
kind: .tailscaleWhoisFailed,
owner: .network,
title: authError.titleOverride ?? "Tailscale identity check failed",
message: authError.userMessageOverride
?? "The gateway could not verify this Tailscale client identity.",
actionLabel: authError.actionLabel ?? "Review Tailscale setup",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authTailscaleIdentityMismatch:
return self.problem(
kind: .tailscaleIdentityMismatch,
owner: .network,
title: authError.titleOverride ?? "Tailscale identity check failed",
message: authError.userMessageOverride
?? "The forwarded Tailscale identity did not match the verified identity.",
actionLabel: authError.actionLabel ?? "Review Tailscale setup",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authRateLimited:
return self.problem(
kind: .authRateLimited,
owner: .gateway,
title: authError.titleOverride ?? "Too many failed attempts",
message: authError.userMessageOverride
?? "The gateway is temporarily refusing new auth attempts after repeated failures.",
actionLabel: authError.actionLabel ?? "Wait and retry",
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
requestId: authError.requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case .authRequired, .authUnauthorized, .none:
return self.problem(
kind: .unknown,
owner: authError.ownerRaw.flatMap { self.owner(from: $0) } ?? .unknown,
title: authError.titleOverride ?? "Gateway rejected the connection",
message: authError.userMessageOverride ?? authError.message,
actionLabel: authError.actionLabel,
actionCommand: authError.actionCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: nil),
requestId: authError.requestId,
retryable: authError.retryableOverride ?? false,
pauseReconnect: authError.pauseReconnectOverride ?? authError.isNonRecoverable,
authError: authError)
}
}
private static func map(_ responseError: GatewayResponseError) -> GatewayConnectionProblem? {
let code = responseError.code.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if code == "NOT_PAIRED" || responseError.detailsReason == "not-paired" {
let authError = GatewayConnectAuthError(
message: responseError.message,
detailCodeRaw: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
canRetryWithDeviceToken: false,
recommendedNextStepRaw: nil,
requestId: self.stringValue(responseError.details["requestId"]?.value),
detailsReason: responseError.detailsReason,
ownerRaw: nil,
titleOverride: nil,
userMessageOverride: nil,
actionLabel: nil,
actionCommand: nil,
docsURLString: nil,
retryableOverride: nil,
pauseReconnectOverride: nil)
return self.map(authError)
}
return nil
}
private static func mapTransportError(_ error: Error) -> GatewayConnectionProblem? {
let nsError = error as NSError
let rawMessage = nsError.userInfo[NSLocalizedDescriptionKey] as? String ?? nsError.localizedDescription
let lower = rawMessage.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if lower.isEmpty {
return nil
}
let urlErrorCode = URLError.Code(rawValue: nsError.code)
if nsError.domain == URLError.errorDomain {
switch urlErrorCode {
case .timedOut:
return GatewayConnectionProblem(
kind: .timeout,
owner: .network,
title: "Connection timed out",
message: "The gateway did not respond before the connection timed out.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
case .cannotConnectToHost:
return GatewayConnectionProblem(
kind: .connectionRefused,
owner: .network,
title: "Gateway refused the connection",
message: "The gateway host was reachable, but it refused the connection.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
case .cannotFindHost, .dnsLookupFailed, .notConnectedToInternet, .networkConnectionLost, .internationalRoamingOff, .callIsActive, .dataNotAllowed:
return GatewayConnectionProblem(
kind: .reachabilityFailed,
owner: .network,
title: "Gateway is not reachable",
message: "OpenClaw could not reach the gateway over the current network.",
actionLabel: "Check network",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
case .cancelled:
return GatewayConnectionProblem(
kind: .websocketCancelled,
owner: .network,
title: "Connection interrupted",
message: "The connection to the gateway was interrupted before setup completed.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
default:
break
}
}
if lower.contains("timed out") {
return GatewayConnectionProblem(
kind: .timeout,
owner: .network,
title: "Connection timed out",
message: "The gateway did not respond before the connection timed out.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
}
if lower.contains("connection refused") || lower.contains("refused") {
return GatewayConnectionProblem(
kind: .connectionRefused,
owner: .network,
title: "Gateway refused the connection",
message: "The gateway host was reachable, but it refused the connection.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
}
if lower.contains("cannot find host") || lower.contains("could not connect") || lower.contains("network is unreachable") {
return GatewayConnectionProblem(
kind: .reachabilityFailed,
owner: .network,
title: "Gateway is not reachable",
message: "OpenClaw could not reach the gateway over the current network.",
actionLabel: "Check network",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
}
if lower.contains("cancelled") || lower.contains("canceled") {
return GatewayConnectionProblem(
kind: .websocketCancelled,
owner: .network,
title: "Connection interrupted",
message: "The connection to the gateway was interrupted before setup completed.",
actionLabel: "Retry",
actionCommand: nil,
docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
retryable: true,
pauseReconnect: false,
technicalDetails: rawMessage)
}
return nil
}
private static func pairingProblem(for authError: GatewayConnectAuthError) -> GatewayConnectionProblem {
let requestId = authError.requestId
let pairingCommand = self.approvalCommand(requestId: requestId)
switch authError.detailsReason {
case "role-upgrade":
return self.problem(
kind: .pairingRoleUpgradeRequired,
owner: .gateway,
title: authError.titleOverride ?? "Additional approval required",
message: authError.userMessageOverride
?? "This iPhone is already paired, but it is requesting a new role that was not previously approved.",
actionLabel: authError.actionLabel ?? "Approve on gateway",
actionCommand: authError.actionCommand ?? pairingCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case "scope-upgrade":
return self.problem(
kind: .pairingScopeUpgradeRequired,
owner: .gateway,
title: authError.titleOverride ?? "Additional permissions required",
message: authError.userMessageOverride
?? "This iPhone is already paired, but it is requesting new permissions that require approval.",
actionLabel: authError.actionLabel ?? "Approve on gateway",
actionCommand: authError.actionCommand ?? pairingCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
case "metadata-upgrade":
return self.problem(
kind: .pairingMetadataUpgradeRequired,
owner: .gateway,
title: authError.titleOverride ?? "Device approval needs refresh",
message: authError.userMessageOverride
?? "The gateway detected a change in this device's approved identity metadata and requires re-approval.",
actionLabel: authError.actionLabel ?? "Approve on gateway",
actionCommand: authError.actionCommand ?? pairingCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
default:
return self.problem(
kind: .pairingRequired,
owner: .gateway,
title: authError.titleOverride ?? "This iPhone is not approved yet",
message: authError.userMessageOverride
?? "The gateway received the connection request, but this device must be approved first.",
actionLabel: authError.actionLabel ?? "Approve on gateway",
actionCommand: authError.actionCommand ?? pairingCommand,
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
requestId: requestId,
retryable: false,
pauseReconnect: true,
authError: authError)
}
}
private static func problem(
kind: GatewayConnectionProblem.Kind,
owner: GatewayConnectionProblem.Owner,
title: String,
message: String,
actionLabel: String?,
actionCommand: String?,
docsURL: URL?,
requestId: String?,
retryable: Bool,
pauseReconnect: Bool,
authError: GatewayConnectAuthError)
-> GatewayConnectionProblem
{
GatewayConnectionProblem(
kind: kind,
owner: authError.ownerRaw.flatMap(self.owner(from:)) ?? owner,
title: title,
message: message,
actionLabel: actionLabel,
actionCommand: actionCommand,
docsURL: docsURL,
requestId: requestId,
retryable: authError.retryableOverride ?? retryable,
pauseReconnect: authError.pauseReconnectOverride ?? pauseReconnect,
technicalDetails: self.technicalDetails(for: authError))
}
private static func approvalCommand(requestId: String?) -> String {
if let requestId = self.nonEmpty(requestId) {
return "openclaw devices approve \(requestId)"
}
return "openclaw devices list"
}
private static func technicalDetails(for authError: GatewayConnectAuthError) -> String? {
var parts: [String] = []
if let detail = self.nonEmpty(authError.detailCodeRaw) {
parts.append(detail)
}
if let reason = self.nonEmpty(authError.detailsReason) {
parts.append("reason=\(reason)")
}
if let requestId = self.nonEmpty(authError.requestId) {
parts.append("requestId=\(requestId)")
}
if let nextStep = self.nonEmpty(authError.recommendedNextStepRaw) {
parts.append("next=\(nextStep)")
}
if authError.canRetryWithDeviceToken {
parts.append("deviceTokenRetry=true")
}
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
private static func docsURL(_ preferred: String?, fallback: String?) -> URL? {
if let preferred = self.nonEmpty(preferred), let url = URL(string: preferred) {
return url
}
if let fallback = self.nonEmpty(fallback), let url = URL(string: fallback) {
return url
}
return nil
}
private static func owner(from raw: String) -> GatewayConnectionProblem.Owner? {
switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "gateway":
return .gateway
case "iphone", "ios", "device":
return .iphone
case "both":
return .both
case "network":
return .network
case "unknown", "":
return .unknown
default:
return nil
}
}
private static func stringValue(_ value: Any?) -> String? {
self.nonEmpty(value as? String)
}
private static func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
}

View File

@@ -43,32 +43,12 @@ public struct GatewayConnectAuthError: LocalizedError, Sendable {
public let detailCodeRaw: String?
public let recommendedNextStepRaw: String?
public let canRetryWithDeviceToken: Bool
public let requestId: String?
public let detailsReason: String?
public let ownerRaw: String?
public let titleOverride: String?
public let userMessageOverride: String?
public let actionLabel: String?
public let actionCommand: String?
public let docsURLString: String?
public let retryableOverride: Bool?
public let pauseReconnectOverride: Bool?
public init(
message: String,
detailCodeRaw: String?,
canRetryWithDeviceToken: Bool,
recommendedNextStepRaw: String? = nil,
requestId: String? = nil,
detailsReason: String? = nil,
ownerRaw: String? = nil,
titleOverride: String? = nil,
userMessageOverride: String? = nil,
actionLabel: String? = nil,
actionCommand: String? = nil,
docsURLString: String? = nil,
retryableOverride: Bool? = nil,
pauseReconnectOverride: Bool? = nil)
recommendedNextStepRaw: String? = nil)
{
let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedDetailCode = detailCodeRaw?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -79,54 +59,19 @@ public struct GatewayConnectAuthError: LocalizedError, Sendable {
self.canRetryWithDeviceToken = canRetryWithDeviceToken
self.recommendedNextStepRaw =
trimmedRecommendedNextStep?.isEmpty == false ? trimmedRecommendedNextStep : nil
self.requestId = Self.trimmedOrNil(requestId)
self.detailsReason = Self.trimmedOrNil(detailsReason)
self.ownerRaw = Self.trimmedOrNil(ownerRaw)
self.titleOverride = Self.trimmedOrNil(titleOverride)
self.userMessageOverride = Self.trimmedOrNil(userMessageOverride)
self.actionLabel = Self.trimmedOrNil(actionLabel)
self.actionCommand = Self.trimmedOrNil(actionCommand)
self.docsURLString = Self.trimmedOrNil(docsURLString)
self.retryableOverride = retryableOverride
self.pauseReconnectOverride = pauseReconnectOverride
}
public init(
message: String,
detailCode: String?,
canRetryWithDeviceToken: Bool,
recommendedNextStep: String? = nil,
requestId: String? = nil,
detailsReason: String? = nil,
ownerRaw: String? = nil,
titleOverride: String? = nil,
userMessageOverride: String? = nil,
actionLabel: String? = nil,
actionCommand: String? = nil,
docsURLString: String? = nil,
retryableOverride: Bool? = nil,
pauseReconnectOverride: Bool? = nil)
recommendedNextStep: String? = nil)
{
self.init(
message: message,
detailCodeRaw: detailCode,
canRetryWithDeviceToken: canRetryWithDeviceToken,
recommendedNextStepRaw: recommendedNextStep,
requestId: requestId,
detailsReason: detailsReason,
ownerRaw: ownerRaw,
titleOverride: titleOverride,
userMessageOverride: userMessageOverride,
actionLabel: actionLabel,
actionCommand: actionCommand,
docsURLString: docsURLString,
retryableOverride: retryableOverride,
pauseReconnectOverride: pauseReconnectOverride)
}
private static func trimmedOrNil(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
recommendedNextStepRaw: recommendedNextStep)
}
public var detailCode: String? { self.detailCodeRaw }

View File

@@ -361,14 +361,6 @@
}
}
},
"update_plan": {
"emoji": "🗺️",
"title": "Update Plan",
"detailKeys": [
"explanation",
"plan.0.step"
]
},
"gateway": {
"emoji": "🔌",
"title": "Gateway",

View File

@@ -537,8 +537,6 @@ public struct AgentParams: Codable, Sendable {
public let besteffortdeliver: Bool?
public let lane: String?
public let extrasystemprompt: String?
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let idempotencykey: String
@@ -568,8 +566,6 @@ public struct AgentParams: Codable, Sendable {
besteffortdeliver: Bool?,
lane: String?,
extrasystemprompt: String?,
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
idempotencykey: String,
@@ -598,8 +594,6 @@ public struct AgentParams: Codable, Sendable {
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.extrasystemprompt = extrasystemprompt
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.idempotencykey = idempotencykey
@@ -630,8 +624,6 @@ public struct AgentParams: Codable, Sendable {
case besteffortdeliver = "bestEffortDeliver"
case lane
case extrasystemprompt = "extraSystemPrompt"
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case idempotencykey = "idempotencyKey"
@@ -1335,236 +1327,6 @@ public struct SessionsResolveParams: Codable, Sendable {
}
}
public struct SessionCompactionCheckpoint: Codable, Sendable {
public let checkpointid: String
public let sessionkey: String
public let sessionid: String
public let createdat: Int
public let reason: AnyCodable
public let tokensbefore: Int?
public let tokensafter: Int?
public let summary: String?
public let firstkeptentryid: String?
public let precompaction: [String: AnyCodable]
public let postcompaction: [String: AnyCodable]
public init(
checkpointid: String,
sessionkey: String,
sessionid: String,
createdat: Int,
reason: AnyCodable,
tokensbefore: Int?,
tokensafter: Int?,
summary: String?,
firstkeptentryid: String?,
precompaction: [String: AnyCodable],
postcompaction: [String: AnyCodable])
{
self.checkpointid = checkpointid
self.sessionkey = sessionkey
self.sessionid = sessionid
self.createdat = createdat
self.reason = reason
self.tokensbefore = tokensbefore
self.tokensafter = tokensafter
self.summary = summary
self.firstkeptentryid = firstkeptentryid
self.precompaction = precompaction
self.postcompaction = postcompaction
}
private enum CodingKeys: String, CodingKey {
case checkpointid = "checkpointId"
case sessionkey = "sessionKey"
case sessionid = "sessionId"
case createdat = "createdAt"
case reason
case tokensbefore = "tokensBefore"
case tokensafter = "tokensAfter"
case summary
case firstkeptentryid = "firstKeptEntryId"
case precompaction = "preCompaction"
case postcompaction = "postCompaction"
}
}
public struct SessionsCompactionListParams: Codable, Sendable {
public let key: String
public init(
key: String)
{
self.key = key
}
private enum CodingKeys: String, CodingKey {
case key
}
}
public struct SessionsCompactionGetParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionBranchParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionRestoreParams: Codable, Sendable {
public let key: String
public let checkpointid: String
public init(
key: String,
checkpointid: String)
{
self.key = key
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionListResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let checkpoints: [SessionCompactionCheckpoint]
public init(
ok: Bool,
key: String,
checkpoints: [SessionCompactionCheckpoint])
{
self.ok = ok
self.key = key
self.checkpoints = checkpoints
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case checkpoints
}
}
public struct SessionsCompactionGetResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let checkpoint: SessionCompactionCheckpoint
public init(
ok: Bool,
key: String,
checkpoint: SessionCompactionCheckpoint)
{
self.ok = ok
self.key = key
self.checkpoint = checkpoint
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case checkpoint
}
}
public struct SessionsCompactionBranchResult: Codable, Sendable {
public let ok: Bool
public let sourcekey: String
public let key: String
public let sessionid: String
public let checkpoint: SessionCompactionCheckpoint
public let entry: [String: AnyCodable]
public init(
ok: Bool,
sourcekey: String,
key: String,
sessionid: String,
checkpoint: SessionCompactionCheckpoint,
entry: [String: AnyCodable])
{
self.ok = ok
self.sourcekey = sourcekey
self.key = key
self.sessionid = sessionid
self.checkpoint = checkpoint
self.entry = entry
}
private enum CodingKeys: String, CodingKey {
case ok
case sourcekey = "sourceKey"
case key
case sessionid = "sessionId"
case checkpoint
case entry
}
}
public struct SessionsCompactionRestoreResult: Codable, Sendable {
public let ok: Bool
public let key: String
public let sessionid: String
public let checkpoint: SessionCompactionCheckpoint
public let entry: [String: AnyCodable]
public init(
ok: Bool,
key: String,
sessionid: String,
checkpoint: SessionCompactionCheckpoint,
entry: [String: AnyCodable])
{
self.ok = ok
self.key = key
self.sessionid = sessionid
self.checkpoint = checkpoint
self.entry = entry
}
private enum CodingKeys: String, CodingKey {
case ok
case key
case sessionid = "sessionId"
case checkpoint
case entry
}
}
public struct SessionsCreateParams: Codable, Sendable {
public let key: String?
public let agentid: String?

View File

@@ -1,4 +1,3 @@
import Foundation
import OpenClawKit
import Testing
@@ -12,81 +11,4 @@ import Testing
#expect(error.isNonRecoverable)
#expect(error.detail == .authBootstrapTokenInvalid)
}
@Test func connectAuthErrorPreservesStructuredMetadata() {
let error = GatewayConnectAuthError(
message: "pairing required",
detailCode: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
canRetryWithDeviceToken: false,
recommendedNextStep: "review_auth_configuration",
requestId: "req-123",
detailsReason: "scope-upgrade",
ownerRaw: "gateway",
titleOverride: "Additional permissions required",
userMessageOverride: "Approve the requested permissions on the gateway, then reconnect.",
actionLabel: "Approve on gateway",
actionCommand: "openclaw devices approve req-123",
docsURLString: "https://docs.openclaw.ai/gateway/pairing",
retryableOverride: false,
pauseReconnectOverride: true)
#expect(error.requestId == "req-123")
#expect(error.detailsReason == "scope-upgrade")
#expect(error.ownerRaw == "gateway")
#expect(error.titleOverride == "Additional permissions required")
#expect(error.actionCommand == "openclaw devices approve req-123")
#expect(error.docsURLString == "https://docs.openclaw.ai/gateway/pairing")
#expect(error.pauseReconnectOverride == true)
}
@Test func pairingProblemUsesStructuredRequestMetadata() {
let error = GatewayConnectAuthError(
message: "pairing required",
detailCode: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
canRetryWithDeviceToken: false,
requestId: "req-123",
detailsReason: "scope-upgrade")
let problem = GatewayConnectionProblemMapper.map(error: error)
#expect(problem?.kind == .pairingScopeUpgradeRequired)
#expect(problem?.requestId == "req-123")
#expect(problem?.pauseReconnect == true)
#expect(problem?.actionCommand == "openclaw devices approve req-123")
}
@Test func cancelledTransportDoesNotReplaceStructuredPairingProblem() {
let pairing = GatewayConnectAuthError(
message: "pairing required",
detailCode: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
canRetryWithDeviceToken: false,
requestId: "req-123")
let previousProblem = GatewayConnectionProblemMapper.map(error: pairing)
let cancelled = NSError(
domain: URLError.errorDomain,
code: URLError.cancelled.rawValue,
userInfo: [NSLocalizedDescriptionKey: "gateway receive: cancelled"])
let preserved = GatewayConnectionProblemMapper.map(error: cancelled, preserving: previousProblem)
#expect(preserved?.kind == .pairingRequired)
#expect(preserved?.requestId == "req-123")
}
@Test func unmappedTransportErrorClearsStaleStructuredProblem() {
let pairing = GatewayConnectAuthError(
message: "pairing required",
detailCode: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
canRetryWithDeviceToken: false,
requestId: "req-123")
let previousProblem = GatewayConnectionProblemMapper.map(error: pairing)
let unknownTransport = NSError(
domain: NSURLErrorDomain,
code: -1202,
userInfo: [NSLocalizedDescriptionKey: "certificate chain validation failed"])
let mapped = GatewayConnectionProblemMapper.map(error: unknownTransport, preserving: previousProblem)
#expect(mapped == nil)
}
}

View File

@@ -1,4 +1,4 @@
6092701439f9f56624f508eb2b240cb48375264c2667a99cb7e7823cb0ef18d1 config-baseline.json
065f474b340fc22b19358cb298131037cbb2a3411ef0b6f765072bbaafedf751 config-baseline.core.json
7f42b22b46c487d64aaac46001ba9d9096cf7bf0b1c263a54d39946303ff5018 config-baseline.channel.json
483d4f3c1d516719870ad6f2aba6779b9950f85471ee77b9994a077a7574a892 config-baseline.plugin.json
1c74540dd152c55dbda3e5dee1e37008ee3e6aabb0608e571292832c7a1c012c config-baseline.json
7e30316f2326b7d07b71d7b8a96049a74b81428921299b5c4b5aa3d080e03305 config-baseline.core.json
66edc86a9d16db1b9e9e7dd99b7032e2d9bcfb9ff210256a21f4b4f088cb3dc1 config-baseline.channel.json
d6ebc4948499b997c4a3727cf31849d4a598de9f1a4c197417dcc0b0ec1b734f config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
20b57f1d7dd9545d6812b895d896d9441e30867f00598e3eb7cab0ae916eb0f2 plugin-sdk-api-baseline.json
164c2da632598f9d84789926bd6589347420db949da3461096cfb32c82cf47c1 plugin-sdk-api-baseline.jsonl
08615a28ed3deb20a96c9cd8fd7237a4cbb209ceec93dca03b543979304459e4 plugin-sdk-api-baseline.json
683c1249dc15529d8e79bc75e9c00484551cb74126befee507fffcf786e01833 plugin-sdk-api-baseline.jsonl

View File

@@ -183,14 +183,6 @@
"source": "Doctor",
"target": "Doctor"
},
{
"source": "Memory Wiki",
"target": "Memory Wiki"
},
{
"source": "wiki",
"target": "wiki"
},
{
"source": "Polls",
"target": "投票"

View File

@@ -227,10 +227,7 @@ Quick mental model (evaluation order for group messages):
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
Replying to a bot message counts as an implicit mention when the channel
supports reply metadata. Quoting a bot message can also count as an implicit
mention on channels that expose quote metadata. Current built-in cases include
Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams.
```json5
{

View File

@@ -61,17 +61,13 @@ What the Matrix wizard actually asks for:
- optional device name
- whether to enable E2EE
- whether to configure Matrix room access now
- whether to configure Matrix invite auto-join now
- when invite auto-join is enabled, whether it should be `allowlist`, `always`, or `off`
Wizard behavior that matters:
- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut so setup can keep auth in env vars instead of copying secrets into config.
- If Matrix auth env vars already exist for the selected account, and that account does not already have auth saved in config, the wizard offers an env shortcut and only writes `enabled: true` for that account.
- When you add another Matrix account interactively, the entered account name is normalized into the account ID used in config and env vars. For example, `Ops Bot` becomes `ops-bot`.
- DM allowlist prompts accept full `@user:server` values immediately. Display names only work when live directory lookup finds one exact match; otherwise the wizard asks you to retry with a full Matrix ID.
- Room allowlist prompts accept room IDs and aliases directly. They can also resolve joined-room names live, but unresolved names are only kept as typed during setup and are ignored later by runtime allowlist resolution. Prefer `!room:server` or `#alias:server`.
- The wizard now shows an explicit warning before the invite auto-join step because `channels.matrix.autoJoin` defaults to `off`; agents will not join invited rooms or fresh DM-style invites unless you set it.
- In invite auto-join allowlist mode, use only stable invite targets: `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected.
- Runtime room/session identity uses the stable Matrix room ID. Room-declared aliases are only used as lookup inputs, not as the long-term session key or stable group identity.
- To resolve room names before saving them, use `openclaw channels resolve --channel matrix "Project Room"`.
@@ -81,8 +77,6 @@ Wizard behavior that matters:
If you leave it unset, the bot will not join invited rooms or fresh DM-style invites, so it will not appear in new groups or invited DMs unless you join manually first.
Set `autoJoin: "allowlist"` together with `autoJoinAllowlist` to restrict which invites it accepts, or set `autoJoin: "always"` if you want it to join every invite.
In `allowlist` mode, `autoJoinAllowlist` only accepts `!roomId:server`, `#alias:server`, or `*`.
</Warning>
Allowlist example:
@@ -880,8 +874,7 @@ See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layo
## Exec approvals
Matrix can act as a native approval client for a Matrix account. The native
DM/channel routing knobs still live under exec approval config:
Matrix can act as an exec approval client for a Matrix account.
- `channels.matrix.execApprovals.enabled`
- `channels.matrix.execApprovals.approvers` (optional; falls back to `channels.matrix.dm.allowFrom`)
@@ -889,14 +882,13 @@ DM/channel routing knobs still live under exec approval config:
- `channels.matrix.execApprovals.agentFilter`
- `channels.matrix.execApprovals.sessionFilter`
Approvers must be Matrix user IDs such as `@owner:example.org`. Matrix auto-enables native approvals when `enabled` is unset or `"auto"` and at least one approver can be resolved. Exec approvals use `execApprovals.approvers` first and can fall back to `channels.matrix.dm.allowFrom`. Plugin approvals authorize through `channels.matrix.dm.allowFrom`. Set `enabled: false` to disable Matrix as a native approval client explicitly. Approval requests otherwise fall back to other configured approval routes or the approval fallback policy.
Approvers must be Matrix user IDs such as `@owner:example.org`. Matrix auto-enables native exec approvals when `enabled` is unset or `"auto"` and at least one approver can be resolved, either from `execApprovals.approvers` or from `channels.matrix.dm.allowFrom`. Set `enabled: false` to disable Matrix as a native approval client explicitly. Approval requests otherwise fall back to other configured approval routes or the exec approval fallback policy.
Matrix native routing now supports both approval kinds:
Native Matrix routing is exec-only today:
- `channels.matrix.execApprovals.*` controls the native DM/channel fanout mode for Matrix approval prompts.
- Exec approvals use the exec approver set from `execApprovals.approvers` or `channels.matrix.dm.allowFrom`.
- Plugin approvals use the Matrix DM allowlist from `channels.matrix.dm.allowFrom`.
- Matrix reaction shortcuts and message updates apply to both exec and plugin approvals.
- `channels.matrix.execApprovals.*` controls native DM/channel routing for exec approvals only.
- Plugin approvals still use shared same-chat `/approve` plus any configured `approvals.plugin` forwarding.
- Matrix can still reuse `channels.matrix.dm.allowFrom` for plugin-approval authorization when it can infer approvers safely, but it does not expose a separate native plugin-approval DM/channel fanout path.
Delivery rules:
@@ -912,9 +904,9 @@ Matrix approval prompts seed reaction shortcuts on the primary approval message:
Approvers can react on that message or use the fallback slash commands: `/approve <id> allow-once`, `/approve <id> allow-always`, or `/approve <id> deny`.
Only resolved approvers can approve or deny. For exec approvals, channel delivery includes the command text, so only enable `channel` or `both` in trusted rooms.
Only resolved approvers can approve or deny. Channel delivery includes the command text, so only enable `channel` or `both` in trusted rooms.
Matrix approval prompts reuse the shared core approval planner. The Matrix-specific native surface handles room/DM routing, reactions, and message send/update/delete behavior for both exec and plugin approvals.
Matrix approval prompts reuse the shared core approval planner. The Matrix-specific native surface is transport only for exec approvals: room/DM routing and message send/update/delete behavior.
Per-account override:

View File

@@ -399,7 +399,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
- explicit app mention (`<@botId>`)
- mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
- implicit reply-to-bot thread behavior (disabled when `thread.requireExplicitMention` is `true`)
- implicit reply-to-bot thread behavior
Per-channel controls (`channels.slack.channels.<id>`; names only via startup resolution or `dangerouslyAllowNameMatching`):
@@ -423,7 +423,6 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
- Thread replies can create thread session suffixes (`:thread:<threadTs>`) when applicable.
- `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`.
- `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable).
- `channels.slack.thread.requireExplicitMention` (default `false`): when `true`, suppress implicit thread mentions so the bot only responds to explicit `@bot` mentions inside threads, even when the bot already participated in the thread. Without this, replies in a bot-participated thread bypass `requireMention` gating.
Reply threading controls:
@@ -463,11 +462,9 @@ Notes:
- `block`: append chunked preview updates.
- `progress`: show progress status text while generating, then send final text.
`channels.slack.streaming.nativeTransport` controls Slack native text streaming when `channels.slack.streaming.mode` is `partial` (default: `true`).
`channels.slack.nativeStreaming` controls Slack native text streaming when `streaming` is `partial` (default: `true`).
- A reply thread must be available for native text streaming and Slack assistant thread status to appear. Thread selection still follows `replyToMode`.
- Channel and group-chat roots can still use the normal draft preview when native streaming is unavailable.
- Top-level Slack DMs stay off-thread by default, so they do not show the thread-style preview; use thread replies or `typingReaction` if you want visible progress there.
- A reply thread must be available for native text streaming to appear. Thread selection still follows `replyToMode`. Without one, the normal draft preview is used.
- Media and non-text payloads fall back to normal delivery.
- If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads.
@@ -477,10 +474,8 @@ Use draft preview instead of Slack native text streaming:
{
channels: {
slack: {
streaming: {
mode: "partial",
nativeTransport: false,
},
streaming: "partial",
nativeStreaming: false,
},
},
}
@@ -488,9 +483,8 @@ Use draft preview instead of Slack native text streaming:
Legacy keys:
- `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming.mode`.
- boolean `channels.slack.streaming` is auto-migrated to `channels.slack.streaming.mode` and `channels.slack.streaming.nativeTransport`.
- legacy `channels.slack.nativeStreaming` is auto-migrated to `channels.slack.streaming.nativeTransport`.
- `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming`.
- boolean `channels.slack.streaming` is auto-migrated to `channels.slack.nativeStreaming`.
## Typing reaction fallback
@@ -692,7 +686,7 @@ Primary reference:
- compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed)
- channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention`
- threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `streaming.nativeTransport`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `nativeStreaming`
- ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly`
## Troubleshooting

View File

@@ -124,7 +124,6 @@ Example:
- `channels.zalouser.groups.<group>.requireMention` controls whether group replies require a mention.
- Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`).
- This applies both to allowlisted groups and open group mode.
- Quoting a bot message counts as an implicit mention for group activation.
- Authorized control commands (for example `/new`) can bypass mention gating.
- When a group message is skipped because mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message.
- Group history limit defaults to `messages.groupChat.historyLimit` (fallback `50`). You can override per account with `channels.zalouser.historyLimit`.

116
docs/cli/capability.md Normal file
View File

@@ -0,0 +1,116 @@
---
summary: "Capability-first CLI for provider-backed model, media, web, and embedding workflows"
read_when:
- Adding or modifying `openclaw capability` commands
- Designing stable headless capability automation
title: "Capability CLI"
---
# Capability CLI
`openclaw capability` is the canonical headless surface for provider-backed capabilities.
It intentionally exposes capability families, not raw gateway RPC names and not raw agent tool ids.
## Command tree
```text
openclaw capability
list
inspect
model
run
list
inspect
providers
auth login
auth logout
auth status
media
image
generate
edit
describe
describe-many
providers
audio
transcribe
providers
tts
convert
voices
providers
status
enable
disable
set-provider
video
generate
describe
providers
web
search
fetch
providers
memory
embedding
create
providers
```
## Transport
Supported transport flags:
- `--local`
- `--gateway`
Default transport is implicit auto at the command-family level:
- Stateless execution commands default to local.
- Gateway-managed state commands default to gateway.
Examples:
```bash
openclaw capability model run --prompt "hello" --json
openclaw capability media image generate --prompt "friendly lobster" --json
openclaw capability media tts status --json
openclaw capability embedding create --text "hello world" --json
```
## JSON output
Capability commands normalize JSON output under a shared envelope:
```json
{
"ok": true,
"capability": "media.image.generate",
"transport": "local",
"provider": "openai",
"model": "gpt-image-1",
"attempts": [],
"outputs": []
}
```
Top-level fields are stable:
- `ok`
- `capability`
- `transport`
- `provider`
- `model`
- `attempts`
- `outputs`
- `error`
## Notes
- `model run` reuses the agent runtime so provider/model overrides behave like normal agent execution.
- `media tts status` defaults to gateway because it reflects gateway-managed TTS state.

View File

@@ -35,9 +35,8 @@ This page describes the current CLI behavior. If commands change, update this do
- [`logs`](/cli/logs)
- [`system`](/cli/system)
- [`models`](/cli/models)
- [`infer`](/cli/infer)
- [`capability`](/cli/capability)
- [`memory`](/cli/memory)
- [`wiki`](/cli/wiki)
- [`directory`](/cli/directory)
- [`nodes`](/cli/nodes)
- [`devices`](/cli/devices)
@@ -163,19 +162,6 @@ openclaw [--dev] [--profile <name>] <command>
status
index
search
wiki
status
doctor
init
ingest
compile
lint
search
get
apply
bridge import
unsafe-local import
obsidian status|search|open|command|daily
message
send
broadcast
@@ -263,14 +249,14 @@ openclaw [--dev] [--profile <name>] <command>
fallbacks list|add|remove|clear
image-fallbacks list|add|remove|clear
scan
infer (alias: capability)
capability
list
inspect
model run|list|inspect|providers|auth login|logout|status
image generate|edit|describe|describe-many|providers
audio transcribe|providers
tts convert|voices|providers|status|enable|disable|set-provider
video generate|describe|providers
media image generate|edit|describe|describe-many|providers
media audio transcribe|providers
media tts convert|voices|providers|status|enable|disable|set-provider
media video generate|describe|providers
web search|fetch|providers
embedding create|providers
auth add|login|login-github-copilot|setup-token|paste-token

View File

@@ -1,280 +0,0 @@
---
summary: "Infer-first CLI for provider-backed model, image, audio, TTS, video, web, and embedding workflows"
read_when:
- Adding or modifying `openclaw infer` commands
- Designing stable headless capability automation
title: "Inference CLI"
---
# Inference CLI
`openclaw infer` is the canonical headless surface for provider-backed inference workflows.
It intentionally exposes capability families, not raw gateway RPC names and not raw agent tool ids.
## Turn infer into a skill
Copy and paste this to an agent:
```text
Read https://docs.openclaw.ai/cli/infer, then create a skill that routes my common workflows to `openclaw infer`.
Focus on model runs, image generation, video generation, audio transcription, TTS, web search, and embeddings.
```
A good infer-based skill should:
- map common user intents to the correct infer subcommand
- include a few canonical infer examples for the workflows it covers
- prefer `openclaw infer ...` in examples and suggestions
- avoid re-documenting the entire infer surface inside the skill body
Typical infer-focused skill coverage:
- `openclaw infer model run`
- `openclaw infer image generate`
- `openclaw infer audio transcribe`
- `openclaw infer tts convert`
- `openclaw infer web search`
- `openclaw infer embedding create`
## Why use infer
`openclaw infer` provides one consistent CLI for provider-backed inference tasks inside OpenClaw.
Benefits:
- Use the providers and models already configured in OpenClaw instead of wiring up one-off wrappers for each backend.
- Keep model, image, audio transcription, TTS, video, web, and embedding workflows under one command tree.
- Use a stable `--json` output shape for scripts, automation, and agent-driven workflows.
- Prefer a first-party OpenClaw surface when the task is fundamentally "run inference."
- Use the normal local path without requiring the gateway for most infer commands.
## Command tree
```text
openclaw infer
list
inspect
model
run
list
inspect
providers
auth login
auth logout
auth status
image
generate
edit
describe
describe-many
providers
audio
transcribe
providers
tts
convert
voices
providers
status
enable
disable
set-provider
video
generate
describe
providers
web
search
fetch
providers
embedding
create
providers
```
## Common tasks
This table maps common inference tasks to the corresponding infer command.
| Task | Command | Notes |
| ----------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------- |
| Run a text/model prompt | `openclaw infer model run --prompt "..." --json` | Uses the normal local path by default |
| Generate an image | `openclaw infer image generate --prompt "..." --json` | Use `image edit` when starting from an existing file |
| Describe an image file | `openclaw infer image describe --file ./image.png --json` | `--model` must be `<provider/model>` |
| Transcribe audio | `openclaw infer audio transcribe --file ./memo.m4a --json` | `--model` must be `<provider/model>` |
| Synthesize speech | `openclaw infer tts convert --text "..." --output ./speech.mp3 --json` | `tts status` is gateway-oriented |
| Generate a video | `openclaw infer video generate --prompt "..." --json` | |
| Describe a video file | `openclaw infer video describe --file ./clip.mp4 --json` | `--model` must be `<provider/model>` |
| Search the web | `openclaw infer web search --query "..." --json` | |
| Fetch a web page | `openclaw infer web fetch --url https://example.com --json` | |
| Create embeddings | `openclaw infer embedding create --text "..." --json` | |
## Behavior
- `openclaw infer ...` is the primary CLI surface for these workflows.
- Use `--json` when the output will be consumed by another command or script.
- Use `--provider` or `--model provider/model` when a specific backend is required.
- For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form `<provider/model>`.
- Stateless execution commands default to local.
- Gateway-managed state commands default to gateway.
- The normal local path does not require the gateway to be running.
## Model
Use `model` for provider-backed text inference and model/provider inspection.
```bash
openclaw infer model run --prompt "Reply with exactly: smoke-ok" --json
openclaw infer model run --prompt "Summarize this changelog entry" --provider openai --json
openclaw infer model providers --json
openclaw infer model inspect --name gpt-5.4 --json
```
Notes:
- `model run` reuses the agent runtime so provider/model overrides behave like normal agent execution.
- `model auth login`, `model auth logout`, and `model auth status` manage saved provider auth state.
## Image
Use `image` for generation, edit, and description.
```bash
openclaw infer image generate --prompt "friendly lobster illustration" --json
openclaw infer image generate --prompt "cinematic product photo of headphones" --json
openclaw infer image describe --file ./photo.jpg --json
openclaw infer image describe --file ./ui-screenshot.png --model openai/gpt-4.1-mini --json
```
Notes:
- Use `image edit` when starting from existing input files.
- For `image describe`, `--model` must be `<provider/model>`.
## Audio
Use `audio` for file transcription.
```bash
openclaw infer audio transcribe --file ./memo.m4a --json
openclaw infer audio transcribe --file ./team-sync.m4a --language en --prompt "Focus on names and action items" --json
openclaw infer audio transcribe --file ./memo.m4a --model openai/whisper-1 --json
```
Notes:
- `audio transcribe` is for file transcription, not realtime session management.
- `--model` must be `<provider/model>`.
## TTS
Use `tts` for speech synthesis and TTS provider state.
```bash
openclaw infer tts convert --text "hello from openclaw" --output ./hello.mp3 --json
openclaw infer tts convert --text "Your build is complete" --output ./build-complete.mp3 --json
openclaw infer tts providers --json
openclaw infer tts status --json
```
Notes:
- `tts status` defaults to gateway because it reflects gateway-managed TTS state.
- Use `tts providers`, `tts voices`, and `tts set-provider` to inspect and configure TTS behavior.
## Video
Use `video` for generation and description.
```bash
openclaw infer video generate --prompt "cinematic sunset over the ocean" --json
openclaw infer video generate --prompt "slow drone shot over a forest lake" --json
openclaw infer video describe --file ./clip.mp4 --json
openclaw infer video describe --file ./clip.mp4 --model openai/gpt-4.1-mini --json
```
Notes:
- `--model` must be `<provider/model>` for `video describe`.
## Web
Use `web` for search and fetch workflows.
```bash
openclaw infer web search --query "OpenClaw docs" --json
openclaw infer web search --query "OpenClaw infer web providers" --json
openclaw infer web fetch --url https://docs.openclaw.ai/cli/infer --json
openclaw infer web providers --json
```
Notes:
- Use `web providers` to inspect available, configured, and selected providers.
## Embedding
Use `embedding` for vector creation and embedding provider inspection.
```bash
openclaw infer embedding create --text "friendly lobster" --json
openclaw infer embedding create --text "customer support ticket: delayed shipment" --model openai/text-embedding-3-large --json
openclaw infer embedding providers --json
```
## JSON output
Infer commands normalize JSON output under a shared envelope:
```json
{
"ok": true,
"capability": "image.generate",
"transport": "local",
"provider": "openai",
"model": "gpt-image-1",
"attempts": [],
"outputs": []
}
```
Top-level fields are stable:
- `ok`
- `capability`
- `transport`
- `provider`
- `model`
- `attempts`
- `outputs`
- `error`
## Common pitfalls
```bash
# Bad
openclaw infer media image generate --prompt "friendly lobster"
# Good
openclaw infer image generate --prompt "friendly lobster"
```
```bash
# Bad
openclaw infer audio transcribe --file ./memo.m4a --model whisper-1 --json
# Good
openclaw infer audio transcribe --file ./memo.m4a --model openai/whisper-1 --json
```
## Notes
- `openclaw capability ...` is an alias for `openclaw infer ...`.

View File

@@ -15,8 +15,6 @@ Provided by the active memory plugin (default: `memory-core`; set `plugins.slots
Related:
- Memory concept: [Memory](/concepts/memory)
- Memory wiki: [Memory Wiki](/plugins/memory-wiki)
- Wiki CLI: [wiki](/cli/wiki)
- Plugins: [Plugins](/tools/plugin)
## Examples

View File

@@ -115,7 +115,7 @@ Interactive onboarding behavior with reference mode:
Non-interactive Z.AI endpoint choices:
Note: `--auth-choice zai-api-key` now auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5.1`).
Note: `--auth-choice zai-api-key` now auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5`).
If you specifically want the GLM Coding Plan endpoints, pick `zai-coding-global` or `zai-coding-cn`.
```bash

View File

@@ -1,214 +0,0 @@
---
summary: "CLI reference for `openclaw wiki` (memory-wiki vault status, search, compile, lint, apply, bridge, and Obsidian helpers)"
read_when:
- You want to use the memory-wiki CLI
- You are documenting or changing `openclaw wiki`
title: "wiki"
---
# `openclaw wiki`
Inspect and maintain the `memory-wiki` vault.
Provided by the bundled `memory-wiki` plugin.
Related:
- [Memory Wiki plugin](/plugins/memory-wiki)
- [Memory Overview](/concepts/memory)
- [CLI: memory](/cli/memory)
## What it is for
Use `openclaw wiki` when you want a compiled knowledge vault with:
- wiki-native search and page reads
- provenance-rich syntheses
- contradiction and freshness reports
- bridge imports from the active memory plugin
- optional Obsidian CLI helpers
## Common commands
```bash
openclaw wiki status
openclaw wiki doctor
openclaw wiki init
openclaw wiki ingest ./notes/alpha.md
openclaw wiki compile
openclaw wiki lint
openclaw wiki search "alpha"
openclaw wiki get entity.alpha --from 1 --lines 80
openclaw wiki apply synthesis "Alpha Summary" \
--body "Short synthesis body" \
--source-id source.alpha
openclaw wiki apply metadata entity.alpha \
--source-id source.alpha \
--status review \
--question "Still active?"
openclaw wiki bridge import
openclaw wiki unsafe-local import
openclaw wiki obsidian status
openclaw wiki obsidian search "alpha"
openclaw wiki obsidian open syntheses/alpha-summary.md
openclaw wiki obsidian command workspace:quick-switcher
openclaw wiki obsidian daily
```
## Commands
### `wiki status`
Inspect current vault mode, health, and Obsidian CLI availability.
Use this first when you are unsure whether the vault is initialized, bridge mode
is healthy, or Obsidian integration is available.
### `wiki doctor`
Run wiki health checks and surface configuration or vault problems.
Typical issues include:
- bridge mode enabled without public memory artifacts
- invalid or missing vault layout
- missing external Obsidian CLI when Obsidian mode is expected
### `wiki init`
Create the wiki vault layout and starter pages.
This initializes the root structure, including top-level indexes and cache
directories.
### `wiki ingest <path-or-url>`
Import content into the wiki source layer.
Notes:
- URL ingest is controlled by `ingest.allowUrlIngest`
- imported source pages keep provenance in frontmatter
- auto-compile can run after ingest when enabled
### `wiki compile`
Rebuild indexes, related blocks, dashboards, and compiled digests.
This writes stable machine-facing artifacts under:
- `.openclaw-wiki/cache/agent-digest.json`
- `.openclaw-wiki/cache/claims.jsonl`
If `render.createDashboards` is enabled, compile also refreshes report pages.
### `wiki lint`
Lint the vault and report:
- structural issues
- provenance gaps
- contradictions
- open questions
- low-confidence pages/claims
- stale pages/claims
Run this after meaningful wiki updates.
### `wiki search <query>`
Search wiki content.
Behavior depends on config:
- `search.backend`: `shared` or `local`
- `search.corpus`: `wiki`, `memory`, or `all`
Use `wiki search` when you want wiki-specific ranking or provenance details.
For one broad shared recall pass, prefer `openclaw memory search` when the
active memory plugin exposes shared search.
### `wiki get <lookup>`
Read a wiki page by id or relative path.
Examples:
```bash
openclaw wiki get entity.alpha
openclaw wiki get syntheses/alpha-summary.md --from 1 --lines 80
```
### `wiki apply`
Apply narrow mutations without freeform page surgery.
Supported flows include:
- create/update a synthesis page
- update page metadata
- attach source ids
- add questions
- add contradictions
- update confidence/status
- write structured claims
This command exists so the wiki can evolve safely without manually editing
managed blocks.
### `wiki bridge import`
Import public memory artifacts from the active memory plugin into bridge-backed
source pages.
Use this in `bridge` mode when you want the latest exported memory artifacts
pulled into the wiki vault.
### `wiki unsafe-local import`
Import from explicitly configured local paths in `unsafe-local` mode.
This is intentionally experimental and same-machine only.
### `wiki obsidian ...`
Obsidian helper commands for vaults running in Obsidian-friendly mode.
Subcommands:
- `status`
- `search`
- `open`
- `command`
- `daily`
These require the official `obsidian` CLI on `PATH` when
`obsidian.useOfficialCli` is enabled.
## Practical usage guidance
- Use `wiki search` + `wiki get` when provenance and page identity matter.
- Use `wiki apply` instead of hand-editing managed generated sections.
- Use `wiki lint` before trusting contradictory or low-confidence content.
- Use `wiki compile` after bulk imports or source changes when you want fresh
dashboards and compiled digests immediately.
- Use `wiki bridge import` when bridge mode depends on newly exported memory
artifacts.
## Configuration tie-ins
`openclaw wiki` behavior is shaped by:
- `plugins.entries.memory-wiki.config.vaultMode`
- `plugins.entries.memory-wiki.config.search.backend`
- `plugins.entries.memory-wiki.config.search.corpus`
- `plugins.entries.memory-wiki.config.bridge.*`
- `plugins.entries.memory-wiki.config.obsidian.*`
- `plugins.entries.memory-wiki.config.render.*`
- `plugins.entries.memory-wiki.config.context.includeCompiledDigestPrompt`
See [Memory Wiki plugin](/plugins/memory-wiki) for the full config model.

View File

@@ -41,71 +41,6 @@ Before compacting, OpenClaw automatically reminds the agent to save important
notes to [memory](/concepts/memory) files. This prevents context loss.
</Info>
Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.).
Compaction summarization preserves opaque identifiers by default (`identifierPolicy: "strict"`). You can override this with `identifierPolicy: "off"` or provide custom text with `identifierPolicy: "custom"` and `identifierInstructions`.
You can optionally specify a different model for compaction summarization via `agents.defaults.compaction.model`. This is useful when your primary model is a local or small model and you want compaction summaries produced by a more capable model. The override accepts any `provider/model-id` string:
```json
{
"agents": {
"defaults": {
"compaction": {
"model": "openrouter/anthropic/claude-sonnet-4-6"
}
}
}
}
```
This also works with local models, for example a second Ollama model dedicated to summarization or a fine-tuned compaction specialist:
```json
{
"agents": {
"defaults": {
"compaction": {
"model": "ollama/llama3.1:8b"
}
}
}
}
```
When unset, compaction uses the agents primary model.
## Pluggable compaction providers
Plugins can register a custom compaction provider via `registerCompactionProvider()` on the plugin API. When a provider is registered and configured, OpenClaw delegates summarization to it instead of the built-in LLM pipeline.
To use a registered provider, set the provider id in your config:
```json
{
"agents": {
"defaults": {
"compaction": {
"provider": "my-provider"
}
}
}
}
```
Setting a `provider` automatically forces `mode: "safeguard"`. Providers receive the same compaction instructions and identifier-preservation policy as the built-in path, and OpenClaw still preserves recent-turn and split-turn suffix context after provider output. If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization.
## Auto-compaction (default on)
When a session nears or exceeds the models context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.
Youll see:
- `🧹 Auto-compaction complete` in verbose mode
- `/status` showing `🧹 Compactions: <count>`
Before compaction, OpenClaw can run a **silent memory flush** turn to store
durable notes to disk. See [Memory](/concepts/memory) for details and config.
## Manual compaction
Type `/compact` in any chat to force a compaction. Add instructions to guide

View File

@@ -115,8 +115,6 @@ engine is used automatically.
A plugin can register a context engine using the plugin API:
```ts
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
export default function register(api) {
api.registerContextEngine("my-engine", () => ({
info: {
@@ -130,15 +128,12 @@ export default function register(api) {
return { ingested: true };
},
async assemble({ sessionId, messages, tokenBudget, availableTools, citationsMode }) {
async assemble({ sessionId, messages, tokenBudget }) {
// Return messages that fit the budget
return {
messages: buildContext(messages, tokenBudget),
estimatedTokens: countTokens(messages),
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
}),
systemPromptAddition: "Use lcm_grep to search history...",
};
},
@@ -253,13 +248,7 @@ OpenClaw resolves when it needs a context engine.
- **Memory plugins** (`plugins.slots.memory`) are separate from context engines.
Memory plugins provide search/retrieval; context engines control what the
model sees. They can work together — a context engine might use memory
plugin data during assembly. Plugin engines that want the active memory
prompt path should prefer `buildMemorySystemPromptAddition(...)` from
`openclaw/plugin-sdk/core`, which converts the active memory prompt sections
into a ready-to-prepend `systemPromptAddition`. If an engine needs lower-level
control, it can still pull raw lines from
`openclaw/plugin-sdk/memory-host-core` via
`buildActiveMemoryPromptSection(...)`.
plugin data during assembly.
- **Session pruning** (trimming old tool results in-memory) still runs
regardless of which context engine is active.

View File

@@ -40,26 +40,6 @@ The agent has two tools for working with memory:
Both tools are provided by the active memory plugin (default: `memory-core`).
## Memory Wiki companion plugin
If you want durable memory to behave more like a maintained knowledge base than
just raw notes, use the bundled `memory-wiki` plugin.
`memory-wiki` compiles durable knowledge into a wiki vault with:
- deterministic page structure
- structured claims and evidence
- contradiction and freshness tracking
- generated dashboards
- compiled digests for agent/runtime consumers
- wiki-native tools like `wiki_search`, `wiki_get`, `wiki_apply`, and `wiki_lint`
It does not replace the active memory plugin. The active memory plugin still
owns recall, promotion, and dreaming. `memory-wiki` adds a provenance-rich
knowledge layer beside it.
See [Memory Wiki](/plugins/memory-wiki).
## Memory search
When an embedding provider is configured, `memory_search` uses **hybrid
@@ -93,15 +73,6 @@ multi-agent awareness. Plugin install.
</Card>
</CardGroup>
## Knowledge wiki layer
<CardGroup cols={1}>
<Card title="Memory Wiki" icon="book" href="/plugins/memory-wiki">
Compiles durable memory into a provenance-rich wiki vault with claims,
dashboards, bridge mode, and Obsidian-friendly workflows.
</Card>
</CardGroup>
## Automatic memory flush
Before [compaction](/concepts/compaction) summarizes your conversation, OpenClaw
@@ -146,7 +117,6 @@ openclaw memory index --force # Rebuild the index
- [Builtin Memory Engine](/concepts/memory-builtin) -- default SQLite backend
- [QMD Memory Engine](/concepts/memory-qmd) -- advanced local-first sidecar
- [Honcho Memory](/concepts/memory-honcho) -- AI-native cross-session memory
- [Memory Wiki](/plugins/memory-wiki) -- compiled knowledge vault and wiki-native tools
- [Memory Search](/concepts/memory-search) -- search pipeline, providers, and
tuning
- [Dreaming (experimental)](/concepts/dreaming) -- background promotion

View File

@@ -360,7 +360,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- or `npm install -g @google/gemini-cli`
- Enable: `openclaw plugins enable google`
- Login: `openclaw models auth login --provider google-gemini-cli --set-default`
- Default model: `google-gemini-cli/gemini-3-flash-preview`
- Default model: `google-gemini-cli/gemini-3.1-pro-preview`
- Note: you do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores
tokens in auth profiles on the gateway host.
- If requests fail after login, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host.
@@ -371,7 +371,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `zai`
- Auth: `ZAI_API_KEY`
- Example model: `zai/glm-5.1`
- Example model: `zai/glm-5`
- CLI: `openclaw onboard --auth-choice zai-api-key`
- Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*`
- `zai-api-key` auto-detects the matching Z.AI endpoint; `zai-coding-global`, `zai-coding-cn`, `zai-global`, and `zai-cn` force a specific surface

View File

@@ -37,27 +37,12 @@ QA Lab page where an operator or automation loop can give the agent a QA
mission, observe real channel behavior, and record what worked, failed, or
stayed blocked.
For faster QA Lab UI iteration without rebuilding the Docker image each time,
start the stack with a bind-mounted QA Lab bundle:
```bash
pnpm openclaw qa docker-build-image
pnpm qa:lab:build
pnpm qa:lab:up:fast
pnpm qa:lab:watch
```
`qa:lab:up:fast` keeps the Docker services on a prebuilt image and bind-mounts
`extensions/qa-lab/web/dist` into the `qa-lab` container. `qa:lab:watch`
rebuilds that bundle on change, and the browser auto-reloads when the QA Lab
asset hash changes.
## Repo-backed seeds
Seed assets live in `qa/`:
- `qa/scenarios/index.md`
- `qa/scenarios/*.md`
- `qa/QA_KICKOFF_TASK.md`
- `qa/seed-scenarios.json`
These are intentionally in git so the QA plan is visible to both humans and the
agent. The baseline list should stay broad enough to cover:

View File

@@ -126,14 +126,13 @@ Modes:
Slack-only:
- `channels.slack.streaming.nativeTransport` toggles Slack native streaming API calls when `channels.slack.streaming.mode="partial"` (default: `true`).
- Slack native streaming and Slack assistant thread status require a reply thread target; top-level DMs do not show that thread-style preview.
- `channels.slack.nativeStreaming` toggles Slack native streaming API calls when `streaming=partial` (default: `true`).
Legacy key migration:
- Telegram: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
- Discord: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
- Slack: `streamMode` auto-migrates to `streaming.mode`; boolean `streaming` auto-migrates to `streaming.mode` plus `streaming.nativeTransport`; legacy `nativeStreaming` auto-migrates to `streaming.nativeTransport`.
- Slack: `streamMode` auto-migrates to `streaming` enum; boolean `streaming` auto-migrates to `nativeStreaming`.
### Runtime behavior

View File

@@ -43,7 +43,7 @@ The prompt is intentionally compact and uses fixed sections:
- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
- **Current Date & Time**: user-local time, timezone, and time format.
- **Reply Tags**: optional reply tag syntax for supported providers.
- **Heartbeats**: heartbeat prompt and ack behavior, when heartbeats are enabled for the default agent.
- **Heartbeats**: heartbeat prompt and ack behavior.
- **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line).
- **Reasoning**: current visibility level + /reasoning toggle hint.
@@ -103,12 +103,10 @@ Bootstrap files are trimmed and appended under **Project Context** so the model
- `BOOTSTRAP.md` (only on brand-new workspaces)
- `MEMORY.md` when present, otherwise `memory.md` as a lowercase fallback
All of these files are **injected into the context window** on every turn unless
a file-specific gate applies. `HEARTBEAT.md` is omitted on normal runs when
heartbeats are disabled for the default agent or
`agents.defaults.heartbeat.includeSystemPromptSection` is false. Keep injected
files concise — especially `MEMORY.md`, which can grow over time and lead to
unexpectedly high context usage and more frequent compaction.
All of these files are **injected into the context window** on every turn, which
means they consume tokens. Keep them concise — especially `MEMORY.md`, which can
grow over time and lead to unexpectedly high context usage and more frequent
compaction.
> **Note:** `memory/*.md` daily files are **not** injected automatically. They
> are accessed on demand via the `memory_search` and `memory_get` tools, so they

View File

@@ -76,10 +76,6 @@
"source": "/plugins/agent-tools",
"destination": "/plugins/building-plugins#registering-agent-tools"
},
{
"source": "/cli/capability",
"destination": "/cli/infer"
},
{
"source": "/tools/capability-cookbook",
"destination": "/plugins/architecture"

View File

@@ -214,10 +214,8 @@ The bundled OpenAI plugin also registers a default for `codex-cli`:
The bundled Google plugin also registers a default for `google-gemini-cli`:
- `command: "gemini"`
- `args: ["--output-format", "json", "--prompt", "{prompt}"]`
- `resumeArgs: ["--resume", "{sessionId}", "--output-format", "json", "--prompt", "{prompt}"]`
- `imageArg: "@"`
- `imagePathScope: "workspace"`
- `args: ["--prompt", "--output-format", "json"]`
- `resumeArgs: ["--resume", "{sessionId}", "--prompt", "--output-format", "json"]`
- `modelArg: "--model"`
- `sessionMode: "existing"`
- `sessionIdFields: ["session_id", "sessionId"]`
@@ -253,9 +251,8 @@ opt into a generated MCP config overlay with `bundleMcp: true`.
Current bundled behavior:
- `claude-cli`: generated strict MCP config file
- `codex-cli`: inline config overrides for `mcp_servers`
- `google-gemini-cli`: generated Gemini system settings file
- `codex-cli`: no bundle MCP overlay
- `google-gemini-cli`: no bundle MCP overlay
When bundle MCP is enabled, OpenClaw:
@@ -263,8 +260,8 @@ When bundle MCP is enabled, OpenClaw:
- authenticates the bridge with a per-session token (`OPENCLAW_MCP_TOKEN`)
- scopes tool access to the current session, account, and channel context
- loads enabled bundle-MCP servers for the current workspace
- merges them with any existing backend MCP config/settings shape
- rewrites the launch config using the backend-owned integration mode from the owning extension
- merges them with any existing backend `--mcp-config`
- rewrites the CLI args to pass `--strict-mcp-config --mcp-config <generated-file>`
If no MCP servers are enabled, OpenClaw still injects a strict config when a
backend opts into bundle MCP so background runs stay isolated.

View File

@@ -1,6 +1,6 @@
---
title: "Configuration Reference"
summary: "Gateway config reference for core OpenClaw keys, defaults, and links to dedicated subsystem references"
summary: "Complete reference for every OpenClaw config key, defaults, and channel settings"
read_when:
- You need exact field-level config semantics or defaults
- You are validating channel, model, gateway, or tool config blocks
@@ -8,21 +8,7 @@ read_when:
# Configuration Reference
Core config reference for `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
This page covers the main OpenClaw config surfaces and links out when a subsystem has its own deeper reference. It does **not** try to inline every channel/plugin-owned command catalog or every deep memory/QMD knob on one page.
Code truth:
- `openclaw config schema` prints the live JSON Schema used for validation and Control UI, with bundled/plugin/channel metadata merged in when available
- `config.schema.lookup` returns one path-scoped schema node for drill-down tooling
- `pnpm config:docs:check` / `pnpm config:docs:gen` validate the config-doc baseline hash against the current schema surface
Dedicated deep references:
- [Memory configuration reference](/reference/memory-config) for `agents.defaults.memorySearch.*`, `memory.qmd.*`, `memory.citations`, and dreaming config under `plugins.entries.memory-core.config.dreaming`
- [Slash Commands](/tools/slash-commands) for the current built-in + bundled command catalog
- owning channel/plugin pages for channel-specific command surfaces
Every field available in `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
Config format is **JSON5** (comments + trailing commas allowed). All fields are optional — OpenClaw uses safe defaults when omitted.
@@ -440,10 +426,8 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
typingReaction: "hourglass_flowing_sand",
textChunkLimit: 4000,
chunkMode: "length",
streaming: {
mode: "partial", // off | partial | block | progress
nativeTransport: true, // use Slack native streaming API when mode=partial
},
streaming: "partial", // off | partial | block | progress (preview mode)
nativeStreaming: true, // use Slack native streaming API when streaming=partial
mediaMaxMb: 20,
execApprovals: {
enabled: "auto", // true | false | "auto"
@@ -468,14 +452,13 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
resolve the secret value.
- `configWrites: false` blocks Slack-initiated config writes.
- Optional `channels.slack.defaultAccount` overrides default account selection when it matches a configured account id.
- `channels.slack.streaming.mode` is the canonical Slack stream mode key. `channels.slack.streaming.nativeTransport` controls Slack's native streaming transport. Legacy `streamMode`, boolean `streaming`, and `nativeStreaming` values are auto-migrated.
- `channels.slack.streaming` is the canonical stream mode key. Legacy `streamMode` and boolean `streaming` values are auto-migrated.
- Use `user:<id>` (DM) or `channel:<id>` for delivery targets.
**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
**Thread session isolation:** `thread.historyScope` is per-thread (default) or shared across channel. `thread.inheritParent` copies parent channel transcript to new threads.
- Slack native streaming plus the Slack assistant-style "is typing..." thread status require a reply thread target. Top-level DMs stay off-thread by default, so they use `typingReaction` or normal delivery instead of the thread-style preview.
- `typingReaction` adds a temporary reaction to the inbound Slack message while a reply is running, then removes it on completion. Use a Slack emoji shortcode such as `"hourglass_flowing_sand"`.
- `channels.slack.execApprovals`: Slack-native exec approval delivery and approver authorization. Same schema as Discord: `enabled` (`true`/`false`/`"auto"`), `approvers` (Slack user IDs), `agentFilter`, `sessionFilter`, and `target` (`"dm"`, `"channel"`, or `"both"`).
@@ -831,18 +814,12 @@ Include your own number in `allowFrom` to enable self-chat mode (ignores native
{
commands: {
native: "auto", // register native commands when supported
nativeSkills: "auto", // register native skill commands when supported
text: true, // parse /commands in chat messages
bash: false, // allow ! (alias: /bash)
bashForegroundMs: 2000,
config: false, // allow /config
mcp: false, // allow /mcp
plugins: false, // allow /plugins
debug: false, // allow /debug
restart: true, // allow /restart + gateway restart tool
ownerAllowFrom: ["discord:123456789012345678"],
ownerDisplay: "raw", // raw | hash
ownerDisplaySecret: "${OWNER_ID_HASH_SECRET}",
restart: false, // allow /restart + gateway restart tool
allowFrom: {
"*": ["user1"],
discord: ["user:123"],
@@ -854,32 +831,16 @@ Include your own number in `allowFrom` to enable self-chat mode (ignores native
<Accordion title="Command details">
- This block configures command surfaces. For the current built-in + bundled command catalog, see [Slash Commands](/tools/slash-commands).
- This page is a **config-key reference**, not the full command catalog. Channel/plugin-owned commands such as QQ Bot `/bot-ping` `/bot-help` `/bot-logs`, LINE `/card`, device-pair `/pair`, memory `/dreaming`, phone-control `/phone`, and Talk `/voice` are documented in their channel/plugin pages plus [Slash Commands](/tools/slash-commands).
- Text commands must be **standalone** messages with leading `/`.
- `native: "auto"` turns on native commands for Discord/Telegram, leaves Slack off.
- `nativeSkills: "auto"` turns on native skill commands for Discord/Telegram, leaves Slack off.
- Override per channel: `channels.discord.commands.native` (bool or `"auto"`). `false` clears previously registered commands.
- Override native skill registration per channel with `channels.<provider>.commands.nativeSkills`.
- `channels.telegram.customCommands` adds extra Telegram bot menu entries.
- `bash: true` enables `! <cmd>` for host shell. Requires `tools.elevated.enabled` and sender in `tools.elevated.allowFrom.<channel>`.
- `config: true` enables `/config` (reads/writes `openclaw.json`). For gateway `chat.send` clients, persistent `/config set|unset` writes also require `operator.admin`; read-only `/config show` stays available to normal write-scoped operator clients.
- `mcp: true` enables `/mcp` for OpenClaw-managed MCP server config under `mcp.servers`.
- `plugins: true` enables `/plugins` for plugin discovery, install, and enable/disable controls.
- `channels.<provider>.configWrites` gates config mutations per channel (default: true).
- For multi-account channels, `channels.<provider>.accounts.<id>.configWrites` also gates writes that target that account (for example `/allowlist --config --account <id>` or `/config set channels.<provider>.accounts.<id>...`).
- `restart: false` disables `/restart` and gateway restart tool actions. Default: `true`.
- `ownerAllowFrom` is the explicit owner allowlist for owner-only commands/tools. It is separate from `allowFrom`.
- `ownerDisplay: "hash"` hashes owner ids in the system prompt. Set `ownerDisplaySecret` to control hashing.
- `allowFrom` is per-provider. When set, it is the **only** authorization source (channel allowlists/pairing and `useAccessGroups` are ignored).
- `useAccessGroups: false` allows commands to bypass access-group policies when `allowFrom` is not set.
- Command docs map:
- built-in + bundled catalog: [Slash Commands](/tools/slash-commands)
- channel-specific command surfaces: [Channels](/channels)
- QQ Bot commands: [QQ Bot](/channels/qqbot)
- pairing commands: [Pairing](/channels/pairing)
- LINE card command: [LINE](/channels/line)
- memory dreaming: [Dreaming](/concepts/dreaming)
</Accordion>
@@ -1199,7 +1160,6 @@ Periodic heartbeat runs.
defaults: {
compaction: {
mode: "safeguard", // default | safeguard
provider: "my-provider", // id of a registered compaction provider plugin (optional)
timeoutSeconds: 900,
reserveTokensFloor: 24000,
identifierPolicy: "strict", // strict | off | custom
@@ -1220,7 +1180,6 @@ Periodic heartbeat runs.
```
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
- `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction).
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`.
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
@@ -1943,7 +1902,7 @@ Batches rapid text-only messages from the same sender into a single agent turn.
}
```
- `auto` controls the default auto-TTS mode: `off`, `always`, `inbound`, or `tagged`. `/tts on|off` can override local prefs, and `/tts status` shows the effective state.
- `auto` controls auto-TTS. `/tts off|always|inbound|tagged` overrides per session.
- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
- `modelOverrides` is enabled by default; `modelOverrides.allowProvider` defaults to `false` (opt-in).
- API keys fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
@@ -2298,7 +2257,7 @@ Experimental built-in tool flags. Default off unless a runtime-specific auto-ena
Notes:
- `planTool`: enables the structured `update_plan` tool for non-trivial multi-step work tracking.
- Default: `false` for non-OpenAI providers. OpenAI and OpenAI Codex runs auto-enable it when unset; set `false` to disable that auto-enable.
- Default: `false` for non-OpenAI providers. OpenAI and OpenAI Codex runs auto-enable it.
- When enabled, the system prompt also adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`.
### `agents.defaults.subagents`
@@ -2390,7 +2349,6 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi
- `models.providers.*.models.*.contextWindow`: native model context window metadata.
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`.
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
- `models.providers.*.models.*.compat.requiresStringContent`: optional compatibility hint for string-only OpenAI-compatible chat endpoints. When `true`, OpenClaw flattens pure text `messages[].content` arrays into plain strings before sending the request.
- `plugins.entries.amazon-bedrock.config.discovery`: Bedrock auto-discovery settings root.
- `plugins.entries.amazon-bedrock.config.discovery.enabled`: turn implicit discovery on/off.
- `plugins.entries.amazon-bedrock.config.discovery.region`: AWS region for discovery.
@@ -2715,12 +2673,6 @@ See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM
- `enabled`: master dreaming switch (default `false`).
- `frequency`: cron cadence for each full dreaming sweep (`"0 3 * * *"` by default).
- phase policy and thresholds are implementation details (not user-facing config keys).
- Full memory config lives in [Memory configuration reference](/reference/memory-config):
- `agents.defaults.memorySearch.*`
- `memory.backend`
- `memory.citations`
- `memory.qmd.*`
- `plugins.entries.memory-core.config.dreaming`
- Enabled Claude bundle plugins can also contribute embedded Pi defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches.
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine.

View File

@@ -72,8 +72,6 @@ Schema tooling notes:
- `openclaw config schema` prints the same JSON Schema family used by Control UI
and config validation.
- Treat that schema output as the canonical machine-readable contract for
`openclaw.json`; this overview and the configuration reference summarize it.
- Field `title` and `description` values are carried into the schema output for
editor and form tooling.
- Nested object, wildcard (`*`), and array-item (`[]`) entries inherit the same
@@ -86,8 +84,6 @@ Schema tooling notes:
summaries for drill-down tooling.
- Runtime plugin/channel schemas are merged in when the gateway can load the
current manifest registry.
- `pnpm config:docs:check` detects drift between docs-facing config baseline
artifacts and the current schema surface.
When validation fails:

View File

@@ -66,7 +66,6 @@ cat ~/.openclaw/openclaw.json
- Talk config migration from legacy flat `talk.*` fields into `talk.provider` + `talk.providers.<provider>`.
- Browser migration checks for legacy Chrome extension configs and Chrome MCP readiness.
- OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`).
- Codex OAuth shadowing warnings (`models.providers.openai-codex`).
- OAuth TLS prerequisites check for OpenAI Codex OAuth profiles.
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
- Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders``contracts`).
@@ -213,16 +212,6 @@ doctor prints platform-specific fix guidance. On macOS with a Homebrew Node, the
fix is usually `brew postinstall ca-certificates`. With `--deep`, the probe runs
even if the gateway is healthy.
### 2c) Codex OAuth provider overrides
If you previously added legacy OpenAI transport settings under
`models.providers.openai-codex`, they can shadow the built-in Codex OAuth
provider path that newer releases use automatically. Doctor warns when it sees
those old transport settings alongside Codex OAuth so you can remove or rewrite
the stale transport override and get the built-in routing/fallback behavior
back. Custom proxies and header-only overrides are still supported and do not
trigger this warning.
### 3) Legacy state migrations (disk layout)
Doctor can migrate older on-disk layouts into the current structure:

View File

@@ -54,10 +54,7 @@ Example config:
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
- The heartbeat prompt is sent **verbatim** as the user message. The system
prompt includes a “Heartbeat” section only when heartbeats are enabled for the
default agent, and the run is flagged internally.
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md`
from bootstrap context so the model does not see heartbeat-only instructions.
prompt includes a “Heartbeat” section and the run is flagged internally.
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
Outside the window, heartbeats are skipped until the next tick inside the window.
@@ -333,11 +330,6 @@ If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.
On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is
enabled for the default agent. Disabling the heartbeat cadence with `0m` or
setting `includeSystemPromptSection: false` omits it from normal bootstrap
context.
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
That skip is reported as `reason=empty-heartbeat-file`.

View File

@@ -155,30 +155,9 @@ Behavior note for local/proxied `/v1` backends:
- hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`)
are not injected on these custom proxy URLs
Compatibility notes for stricter OpenAI-compatible backends:
- Some servers accept only string `messages[].content` on Chat Completions, not
structured content-part arrays. Set
`models.providers.<provider>.models[].compat.requiresStringContent: true` for
those endpoints.
- Some smaller or stricter local backends are unstable with OpenClaw's full
agent-runtime prompt shape, especially when tool schemas are included. If the
backend works for tiny direct `/v1/chat/completions` calls but fails on normal
OpenClaw agent turns, try
`models.providers.<provider>.models[].compat.supportsTools: false` first.
- If the backend still fails only on larger OpenClaw runs, the remaining issue
is usually upstream model/server capacity or a backend bug, not OpenClaw's
transport layer.
## Troubleshooting
- Gateway can reach the proxy? `curl http://127.0.0.1:1234/v1/models`.
- LM Studio model unloaded? Reload; cold start is a common “hanging” cause.
- Context errors? Lower `contextWindow` or raise your server limit.
- OpenAI-compatible server returns `messages[].content ... expected a string`?
Add `compat.requiresStringContent: true` on that model entry.
- Direct tiny `/v1/chat/completions` calls work, but `openclaw infer model run`
fails on Gemma or another local model? Disable tool schemas first with
`compat.supportsTools: false`, then retest. If the server still crashes only
on larger OpenClaw prompts, treat it as an upstream server/model limitation.
- Safety: local models skip provider-side filters; keep agents narrow and compaction on to limit prompt injection blast radius.

View File

@@ -381,18 +381,16 @@ implemented in `src/gateway/server-methods/*.ts`.
#### Approval families
- `exec.approval.request`, `exec.approval.get`, `exec.approval.list`, and
`exec.approval.resolve` cover one-shot exec approval requests plus pending
approval lookup/replay.
- `exec.approval.request` and `exec.approval.resolve` cover one-shot exec
approval requests.
- `exec.approval.waitDecision` waits on one pending exec approval and returns
the final decision (or `null` on timeout).
- `exec.approvals.get` and `exec.approvals.set` manage gateway exec approval
policy snapshots.
- `exec.approvals.node.get` and `exec.approvals.node.set` manage node-local exec
approval policy via node relay commands.
- `plugin.approval.request`, `plugin.approval.list`,
`plugin.approval.waitDecision`, and `plugin.approval.resolve` cover
plugin-defined approval flows.
- `plugin.approval.request`, `plugin.approval.waitDecision`, and
`plugin.approval.resolve` cover plugin-defined approval flows.
#### Other major families

View File

@@ -59,61 +59,6 @@ Related:
- [/reference/token-use](/reference/token-use)
- [/help/faq#why-am-i-seeing-http-429-ratelimiterror-from-anthropic](/help/faq#why-am-i-seeing-http-429-ratelimiterror-from-anthropic)
## Local OpenAI-compatible backend passes direct probes but agent runs fail
Use this when:
- `curl ... /v1/models` works
- tiny direct `/v1/chat/completions` calls work
- OpenClaw model runs fail only on normal agent turns
```bash
curl http://127.0.0.1:1234/v1/models
curl http://127.0.0.1:1234/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"<id>","messages":[{"role":"user","content":"hi"}],"stream":false}'
openclaw infer model run --model <provider/model> --prompt "hi" --json
openclaw logs --follow
```
Look for:
- direct tiny calls succeed, but OpenClaw runs fail only on larger prompts
- backend errors about `messages[].content` expecting a string
- backend crashes that appear only with larger prompt-token counts or full agent
runtime prompts
Common signatures:
- `messages[...].content: invalid type: sequence, expected a string` → backend
rejects structured Chat Completions content parts. Fix: set
`models.providers.<provider>.models[].compat.requiresStringContent: true`.
- direct tiny requests succeed, but OpenClaw agent runs fail with backend/model
crashes (for example Gemma on some `inferrs` builds) → OpenClaw transport is
likely already correct; the backend is failing on the larger agent-runtime
prompt shape.
- failures shrink after disabling tools but do not disappear → tool schemas were
part of the pressure, but the remaining issue is still upstream model/server
capacity or a backend bug.
Fix options:
1. Set `compat.requiresStringContent: true` for string-only Chat Completions backends.
2. Set `compat.supportsTools: false` for models/backends that cannot handle
OpenClaw's tool schema surface reliably.
3. Lower prompt pressure where possible: smaller workspace bootstrap, shorter
session history, lighter local model, or a backend with stronger long-context
support.
4. If tiny direct requests keep passing while OpenClaw agent turns still crash
inside the backend, treat it as an upstream server/model limitation and file
a repro there with the accepted payload shape.
Related:
- [/gateway/local-models](/gateway/local-models)
- [/gateway/configuration#models](/gateway/configuration#models)
- [/gateway/configuration-reference#openai-compatible-endpoints](/gateway/configuration-reference#openai-compatible-endpoints)
## No replies
If channels are up but nothing answers, check routing and policy before reconnecting anything.

View File

@@ -701,7 +701,7 @@ for usage/billing and raise limits as needed.
- npm: `npm install -g @google/gemini-cli`
2. Enable the plugin: `openclaw plugins enable google`
3. Login: `openclaw models auth login --provider google-gemini-cli --set-default`
4. Default model after login: `google-gemini-cli/gemini-3-flash-preview`
4. Default model after login: `google-gemini-cli/gemini-3.1-pro-preview`
5. If requests fail, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host
This stores OAuth tokens in auth profiles on the gateway host. Details: [Model providers](/concepts/model-providers).
@@ -1816,8 +1816,8 @@ for usage/billing and raise limits as needed.
- `config.schema.lookup`: inspect one config subtree with its shallow schema node, matched UI hint, and immediate child summaries before writing
- `config.get`: fetch the current snapshot + hash
- `config.patch`: safe partial update (preferred for most RPC edits); hot-reloads when possible and restarts when required
- `config.apply`: validate + replace the full config; hot-reloads when possible and restarts when required
- `config.patch`: safe partial update (preferred for most RPC edits)
- `config.apply`: validate + replace the full config, then restart
- The owner-only `gateway` runtime tool still refuses to rewrite `tools.exec.ask` / `tools.exec.security`; legacy `tools.bash.*` aliases normalize to the same protected exec paths
</Accordion>
@@ -2254,7 +2254,7 @@ for usage/billing and raise limits as needed.
Quickest setup:
1. Install Ollama from `https://ollama.com/download`
2. Pull a local model such as `ollama pull gemma4`
2. Pull a local model such as `ollama pull glm-4.7-flash`
3. If you want cloud models too, run `ollama signin`
4. Run `openclaw onboard` and choose `Ollama`
5. Pick `Local` or `Cloud + Local`

View File

@@ -21,32 +21,6 @@ Use these when a task is clearly tied to a script; otherwise prefer the CLI.
Auth monitoring is covered in [Authentication](/gateway/authentication). The scripts under `scripts/` are optional extras for systemd/Termux phone workflows.
## GitHub read helper
Use `scripts/gh-read` when you want `gh` to use a GitHub App installation token for repo-scoped read calls while leaving normal `gh` on your personal login for write actions.
Required env:
- `OPENCLAW_GH_READ_APP_ID`
- `OPENCLAW_GH_READ_PRIVATE_KEY_FILE`
Optional env:
- `OPENCLAW_GH_READ_INSTALLATION_ID` when you want to skip repo-based installation lookup
- `OPENCLAW_GH_READ_PERMISSIONS` as a comma-separated override for the read permission subset to request
Repo resolution order:
- `gh ... -R owner/repo`
- `GH_REPO`
- `git remote origin`
Examples:
- `scripts/gh-read pr view 123`
- `scripts/gh-read run list -R openclaw/openclaw`
- `scripts/gh-read api repos/openclaw/openclaw/pulls/123`
## When adding scripts
- Keep scripts focused and documented.

View File

@@ -47,7 +47,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
### Unit / integration (default)
- Command: `pnpm test`
- Config: ten sequential shard runs (`vitest.full-*.config.ts`) over the existing scoped Vitest projects
- Config: five sequential shard runs (`vitest.full-*.config.ts`) over the existing scoped Vitest projects
- Files: core/unit inventories under `src/**/*.test.ts`, `packages/**/*.test.ts`, `test/**/*.test.ts`, and the whitelisted `ui` node tests covered by `vitest.unit.config.ts`
- Scope:
- Pure unit tests
@@ -58,7 +58,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- No real keys required
- Should be fast and stable
- Projects note:
- Untargeted `pnpm test` now runs eleven smaller shard configs (`core-unit-src`, `core-unit-security`, `core-unit-ui`, `core-unit-support`, `core-support-boundary`, `core-contracts`, `core-bundled`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
- Untargeted `pnpm test` now runs eight smaller shard configs (`core-unit-src`, `core-unit-security`, `core-unit-support`, `core-contracts`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
- `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical.
- `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax.
- `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun.
@@ -253,17 +253,17 @@ openclaw models list
openclaw models list --json
```
## Live: CLI backend smoke (Claude, Codex, Gemini, or other local CLIs)
## Live: CLI backend smoke (Codex CLI or other local CLIs)
- Test: `src/gateway/gateway-cli-backend.live.test.ts`
- Goal: validate the Gateway + agent pipeline using a local CLI backend, without touching your default config.
- Backend-specific smoke defaults live with the owning extension's `cli-backend.ts` definition.
- Enable:
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
- `OPENCLAW_LIVE_CLI_BACKEND=1`
- Defaults:
- Default provider/model: `claude-cli/claude-sonnet-4-6`
- Command/args/image behavior come from the owning CLI backend plugin metadata.
- Model: `codex-cli/gpt-5.4`
- Command: `codex`
- Args: `["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]`
- Overrides (optional):
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.4"`
- `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/codex"`
@@ -272,7 +272,6 @@ openclaw models list --json
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_ARG="--image"` to pass image file paths as CLI args instead of prompt injection.
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE="repeat"` (or `"list"`) to control how image args are passed when `IMAGE_ARG` is set.
- `OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1` to send a second turn and validate resume flow.
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL_SWITCH_PROBE=0` to disable the default Claude Sonnet -> Opus same-session continuity probe (set to `1` to force it on when the selected model supports a switch target).
Example:
@@ -288,21 +287,11 @@ Docker recipe:
pnpm test:docker:live-cli-backend
```
Single-provider Docker recipes:
```bash
pnpm test:docker:live-cli-backend:claude
pnpm test:docker:live-cli-backend:codex
pnpm test:docker:live-cli-backend:gemini
```
Notes:
- The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`.
- It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user.
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude, Codex, and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
- Claude's default smoke also patches the session from Sonnet to Opus and verifies the resumed session still remembers an earlier note.
- For `codex-cli`, it installs the Linux `@openai/codex` package into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
## Live: ACP bind smoke (`/acp spawn ... --bind here`)
@@ -316,15 +305,12 @@ Notes:
- `pnpm test:live src/gateway/gateway-acp-bind.live.test.ts`
- `OPENCLAW_LIVE_ACP_BIND=1`
- Defaults:
- ACP agents in Docker: `claude,codex,gemini`
- ACP agent for direct `pnpm test:live ...`: `claude`
- ACP agent: `claude`
- Synthetic channel: Slack DM-style conversation context
- ACP backend: `acpx`
- Overrides:
- `OPENCLAW_LIVE_ACP_BIND_AGENT=claude`
- `OPENCLAW_LIVE_ACP_BIND_AGENT=codex`
- `OPENCLAW_LIVE_ACP_BIND_AGENT=gemini`
- `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini`
- `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND='npx -y @agentclientprotocol/claude-agent-acp@<version>'`
- Notes:
- This lane uses the gateway `chat.send` surface with admin-only synthetic originating-route fields so tests can attach message-channel context without pretending to deliver externally.
@@ -344,20 +330,10 @@ Docker recipe:
pnpm test:docker:live-acp-bind
```
Single-agent Docker recipes:
```bash
pnpm test:docker:live-acp-bind:claude
pnpm test:docker:live-acp-bind:codex
pnpm test:docker:live-acp-bind:gemini
```
Docker notes:
- The Docker runner lives at `scripts/test-live-acp-bind-docker.sh`.
- By default, it runs the ACP bind smoke against all supported live CLI agents in sequence: `claude`, `codex`, then `gemini`.
- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini` to narrow the matrix.
- It sources `~/.profile`, stages the matching CLI auth material into the container, installs `acpx` into a writable npm prefix, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) if missing.
- It sources `~/.profile`, stages the matching CLI auth material into the container, installs `acpx` into a writable npm prefix, then installs the requested live CLI (`@anthropic-ai/claude-code` or `@openai/codex`) if missing.
- Inside Docker, the runner sets `OPENCLAW_LIVE_ACP_BIND_ACPX_COMMAND=$HOME/.npm-global/bin/acpx` so acpx keeps provider env vars from the sourced profile available to the child harness CLI.
### Recommended live recipes
@@ -450,7 +426,7 @@ Live tests discover credentials the same way the CLI does. Practical implication
- Per-agent auth profiles: `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` (this is what “profile keys” means in the live tests)
- Config: `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`)
- Legacy state dir: `~/.openclaw/credentials/` (copied into the staged live home when present, but not the main profile-key store)
- Live local runs copy the active config, per-agent `auth-profiles.json` files, legacy `credentials/`, and supported external CLI auth dirs into a temp test home by default; staged live homes skip `workspace/` and `sandboxes/`, and `agents.*.workspace` / `agentDir` path overrides are stripped so probes stay off your real host workspace.
- Live local runs copy the active config, per-agent `auth-profiles.json` files, legacy `credentials/`, and supported external CLI auth dirs into a temp test home by default; `agents.*.workspace` / `agentDir` path overrides are stripped in that staged config so probes stay off your real host workspace.
If you want to rely on env keys (e.g. exported in your `~/.profile`), run local tests after `source ~/.profile`, or use the Docker runners below (they can mount `~/.profile` into the container).

View File

@@ -42,21 +42,6 @@ If you see:
`HTTP 429: rate_limit_error: Extra usage is required for long context requests`,
go to [/gateway/troubleshooting#anthropic-429-extra-usage-required-for-long-context](/gateway/troubleshooting#anthropic-429-extra-usage-required-for-long-context).
## Local OpenAI-compatible backend works directly but fails in OpenClaw
If your local or self-hosted `/v1` backend answers small direct
`/v1/chat/completions` probes but fails on `openclaw infer model run` or normal
agent turns:
1. If the error mentions `messages[].content` expecting a string, set
`models.providers.<provider>.models[].compat.requiresStringContent: true`.
2. If the backend still fails only on OpenClaw agent turns, set
`models.providers.<provider>.models[].compat.supportsTools: false` and retry.
3. If tiny direct calls still work but larger OpenClaw prompts crash the
backend, treat the remaining issue as an upstream model/server limitation and
continue in the deep runbook:
[/gateway/troubleshooting#local-openai-compatible-backend-passes-direct-probes-but-agent-runs-fail](/gateway/troubleshooting#local-openai-compatible-backend-passes-direct-probes-but-agent-runs-fail)
## Plugin install fails with missing openclaw extensions
If install fails with `package.json missing openclaw.extensions`, the plugin package

View File

@@ -1120,8 +1120,7 @@ authoring plugins:
`openclaw/plugin-sdk/secret-input`, and
`openclaw/plugin-sdk/webhook-ingress` for shared setup/auth/reply/webhook
wiring. `channel-inbound` is the shared home for debounce, mention matching,
inbound mention-policy helpers, envelope formatting, and inbound envelope
context helpers.
envelope formatting, and inbound envelope context helpers.
`channel-setup` is the narrow optional-install setup seam.
`setup-runtime` is the runtime-safe setup surface used by `setupEntry` /
deferred startup, including the import-safe setup patch adapters.
@@ -1134,9 +1133,6 @@ authoring plugins:
`openclaw/plugin-sdk/channel-config-schema`,
`openclaw/plugin-sdk/telegram-command-config`,
`openclaw/plugin-sdk/channel-policy`,
`openclaw/plugin-sdk/approval-gateway-runtime`,
`openclaw/plugin-sdk/approval-handler-adapter-runtime`,
`openclaw/plugin-sdk/approval-handler-runtime`,
`openclaw/plugin-sdk/approval-runtime`,
`openclaw/plugin-sdk/config-runtime`,
`openclaw/plugin-sdk/infra-runtime`,
@@ -1155,9 +1151,9 @@ authoring plugins:
assistant-visible-text stripping, markdown render/chunking helpers, redaction
helpers, directive-tag helpers, and safe-text utilities.
- Approval-specific channel seams should prefer one `approvalCapability`
contract on the plugin. Core then reads approval auth, delivery, render,
native-routing, and lazy native-handler behavior through that one capability
instead of mixing approval behavior into unrelated plugin fields.
contract on the plugin. Core then reads approval auth, delivery, render, and
native-routing behavior through that one capability instead of mixing
approval behavior into unrelated plugin fields.
- `openclaw/plugin-sdk/channel-runtime` is deprecated and remains only as a
compatibility shim for older plugins. New code should import the narrower
generic primitives instead, and repo code should not add new imports of the
@@ -1497,23 +1493,14 @@ Use this when your plugin needs to replace or extend the default context
pipeline rather than just add memory search or hooks.
```ts
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
export default function (api) {
api.registerContextEngine("lossless-claw", () => ({
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
async ingest() {
return { ingested: true };
},
async assemble({ messages, availableTools, citationsMode }) {
return {
messages,
estimatedTokens: 0,
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
}),
};
async assemble({ messages }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
@@ -1526,10 +1513,7 @@ If your engine does **not** own the compaction algorithm, keep `compact()`
implemented and delegate it explicitly:
```ts
import {
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
} from "openclaw/plugin-sdk/core";
import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core";
export default function (api) {
api.registerContextEngine("my-memory-engine", () => ({
@@ -1541,15 +1525,8 @@ export default function (api) {
async ingest() {
return { ingested: true };
},
async assemble({ messages, availableTools, citationsMode }) {
return {
messages,
estimatedTokens: 0,
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
}),
};
async assemble({ messages }) {
return { messages, estimatedTokens: 0 };
},
async compact(params) {
return await delegateCompactionToRuntime(params);

View File

@@ -1,357 +0,0 @@
---
summary: "memory-wiki: compiled knowledge vault with provenance, claims, dashboards, and bridge mode"
read_when:
- You want persistent knowledge beyond plain MEMORY.md notes
- You are configuring the bundled memory-wiki plugin
- You want to understand wiki_search, wiki_get, or bridge mode
title: "Memory Wiki"
---
# Memory Wiki
`memory-wiki` is a bundled plugin that turns durable memory into a compiled
knowledge vault.
It does **not** replace the active memory plugin. The active memory plugin still
owns recall, promotion, indexing, and dreaming. `memory-wiki` sits beside it
and compiles durable knowledge into a navigable wiki with deterministic pages,
structured claims, provenance, dashboards, and machine-readable digests.
Use it when you want memory to behave more like a maintained knowledge layer and
less like a pile of Markdown files.
## What it adds
- A dedicated wiki vault with deterministic page layout
- Structured claim and evidence metadata, not just prose
- Page-level provenance, confidence, contradictions, and open questions
- Compiled digests for agent/runtime consumers
- Wiki-native search/get/apply/lint tools
- Optional bridge mode that imports public artifacts from the active memory plugin
- Optional Obsidian-friendly render mode and CLI integration
## How it fits with memory
Think of the split like this:
| Layer | Owns |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Active memory plugin (`memory-core`, QMD, Honcho, etc.) | Recall, semantic search, promotion, dreaming, memory runtime |
| `memory-wiki` | Compiled wiki pages, provenance-rich syntheses, dashboards, wiki-specific search/get/apply |
If the active memory plugin exposes shared recall artifacts, OpenClaw can search
both layers in one pass with `memory_search corpus=all`.
When you need wiki-specific ranking, provenance, or direct page access, use the
wiki-native tools instead.
## Vault modes
`memory-wiki` supports three vault modes:
### `isolated`
Own vault, own sources, no dependency on `memory-core`.
Use this when you want the wiki to be its own curated knowledge store.
### `bridge`
Reads public memory artifacts and memory events from the active memory plugin
through public plugin SDK seams.
Use this when you want the wiki to compile and organize the memory plugin's
exported artifacts without reaching into private plugin internals.
Bridge mode can index:
- exported memory artifacts
- dream reports
- daily notes
- memory root files
- memory event logs
### `unsafe-local`
Explicit same-machine escape hatch for local private paths.
This mode is intentionally experimental and non-portable. Use it only when you
understand the trust boundary and specifically need local filesystem access that
bridge mode cannot provide.
## Vault layout
The plugin initializes a vault like this:
```text
<vault>/
AGENTS.md
WIKI.md
index.md
inbox.md
entities/
concepts/
syntheses/
sources/
reports/
_attachments/
_views/
.openclaw-wiki/
```
Managed content stays inside generated blocks. Human note blocks are preserved.
The main page groups are:
- `sources/` for imported raw material and bridge-backed pages
- `entities/` for durable things, people, systems, projects, and objects
- `concepts/` for ideas, abstractions, patterns, and policies
- `syntheses/` for compiled summaries and maintained rollups
- `reports/` for generated dashboards
## Structured claims and evidence
Pages can carry structured `claims` frontmatter, not just freeform text.
Each claim can include:
- `id`
- `text`
- `status`
- `confidence`
- `evidence[]`
- `updatedAt`
Evidence entries can include:
- `sourceId`
- `path`
- `lines`
- `weight`
- `note`
- `updatedAt`
This is what makes the wiki act more like a belief layer than a passive note
dump. Claims can be tracked, scored, contested, and resolved back to sources.
## Compile pipeline
The compile step reads wiki pages, normalizes summaries, and emits stable
machine-facing artifacts under:
- `.openclaw-wiki/cache/agent-digest.json`
- `.openclaw-wiki/cache/claims.jsonl`
These digests exist so agents and runtime code do not have to scrape Markdown
pages.
Compiled output also powers:
- first-pass wiki indexing for search/get flows
- claim-id lookup back to owning pages
- compact prompt supplements
- report/dashboard generation
## Dashboards and health reports
When `render.createDashboards` is enabled, compile maintains dashboards under
`reports/`.
Built-in reports include:
- `reports/open-questions.md`
- `reports/contradictions.md`
- `reports/low-confidence.md`
- `reports/claim-health.md`
- `reports/stale-pages.md`
These reports track things like:
- contradiction note clusters
- competing claim clusters
- claims missing structured evidence
- low-confidence pages and claims
- stale or unknown freshness
- pages with unresolved questions
## Search and retrieval
`memory-wiki` supports two search backends:
- `shared`: use the shared memory search flow when available
- `local`: search the wiki locally
It also supports three corpora:
- `wiki`
- `memory`
- `all`
Important behavior:
- `wiki_search` and `wiki_get` use compiled digests as a first pass when possible
- claim ids can resolve back to the owning page
- contested/stale/fresh claims influence ranking
- provenance labels can survive into results
Practical rule:
- use `memory_search corpus=all` for one broad recall pass
- use `wiki_search` + `wiki_get` when you care about wiki-specific ranking,
provenance, or page-level belief structure
## Agent tools
The plugin registers these tools:
- `wiki_status`
- `wiki_search`
- `wiki_get`
- `wiki_apply`
- `wiki_lint`
What they do:
- `wiki_status`: current vault mode, health, Obsidian CLI availability
- `wiki_search`: search wiki pages and, when configured, shared memory corpora
- `wiki_get`: read a wiki page by id/path or fall back to shared memory corpus
- `wiki_apply`: narrow synthesis/metadata mutations without freeform page surgery
- `wiki_lint`: structural checks, provenance gaps, contradictions, open questions
The plugin also registers a non-exclusive memory corpus supplement, so shared
`memory_search` and `memory_get` can reach the wiki when the active memory
plugin supports corpus selection.
## Prompt and context behavior
When `context.includeCompiledDigestPrompt` is enabled, memory prompt sections
append a compact compiled snapshot from `agent-digest.json`.
That snapshot is intentionally small and high-signal:
- top pages only
- top claims only
- contradiction count
- question count
- confidence/freshness qualifiers
This is opt-in because it changes prompt shape and is mainly useful for context
engines or legacy prompt assembly that explicitly consume memory supplements.
## Configuration
Put config under `plugins.entries.memory-wiki.config`:
```json5
{
plugins: {
entries: {
"memory-wiki": {
enabled: true,
config: {
vaultMode: "isolated",
vault: {
path: "~/.openclaw/wiki/main",
renderMode: "obsidian",
},
obsidian: {
enabled: true,
useOfficialCli: true,
vaultName: "OpenClaw Wiki",
openAfterWrites: false,
},
bridge: {
enabled: false,
readMemoryArtifacts: true,
indexDreamReports: true,
indexDailyNotes: true,
indexMemoryRoot: true,
followMemoryEvents: true,
},
ingest: {
autoCompile: true,
maxConcurrentJobs: 1,
allowUrlIngest: true,
},
search: {
backend: "shared",
corpus: "wiki",
},
context: {
includeCompiledDigestPrompt: false,
},
render: {
preserveHumanBlocks: true,
createBacklinks: true,
createDashboards: true,
},
},
},
},
},
}
```
Key toggles:
- `vaultMode`: `isolated`, `bridge`, `unsafe-local`
- `vault.renderMode`: `native` or `obsidian`
- `bridge.readMemoryArtifacts`: import active memory plugin public artifacts
- `bridge.followMemoryEvents`: include event logs in bridge mode
- `search.backend`: `shared` or `local`
- `search.corpus`: `wiki`, `memory`, or `all`
- `context.includeCompiledDigestPrompt`: append compact digest snapshot to memory prompt sections
- `render.createBacklinks`: generate deterministic related blocks
- `render.createDashboards`: generate dashboard pages
## CLI
`memory-wiki` also exposes a top-level CLI surface:
```bash
openclaw wiki status
openclaw wiki doctor
openclaw wiki init
openclaw wiki ingest ./notes/alpha.md
openclaw wiki compile
openclaw wiki lint
openclaw wiki search "alpha"
openclaw wiki get entity.alpha
openclaw wiki apply synthesis "Alpha Summary" --body "..." --source-id source.alpha
openclaw wiki bridge import
openclaw wiki obsidian status
```
See [CLI: wiki](/cli/wiki) for the full command reference.
## Obsidian support
When `vault.renderMode` is `obsidian`, the plugin writes Obsidian-friendly
Markdown and can optionally use the official `obsidian` CLI.
Supported workflows include:
- status probing
- vault search
- opening a page
- invoking an Obsidian command
- jumping to the daily note
This is optional. The wiki still works in native mode without Obsidian.
## Recommended workflow
1. Keep your active memory plugin for recall/promotion/dreaming.
2. Enable `memory-wiki`.
3. Start with `isolated` mode unless you explicitly want bridge mode.
4. Use `wiki_search` / `wiki_get` when provenance matters.
5. Use `wiki_apply` for narrow syntheses or metadata updates.
6. Run `wiki_lint` after meaningful changes.
7. Turn on dashboards if you want stale/contradiction visibility.
## Related docs
- [Memory Overview](/concepts/memory)
- [CLI: memory](/cli/memory)
- [CLI: wiki](/cli/wiki)
- [Plugin SDK overview](/plugins/sdk-overview)

View File

@@ -60,34 +60,22 @@ Most channel plugins do not need approval-specific code.
- Core owns same-chat `/approve`, shared approval button payloads, and generic fallback delivery.
- Prefer one `approvalCapability` object on the channel plugin when the channel needs approval-specific behavior.
- `ChannelPlugin.approvals` is removed. Put approval delivery/native/render/auth facts on `approvalCapability`.
- `plugin.auth` is login/logout only; core no longer reads approval auth hooks from that object.
- `approvalCapability.authorizeActorAction` and `approvalCapability.getActionAvailabilityState` are the canonical approval-auth seam.
- Use `approvalCapability.getActionAvailabilityState` for same-chat approval auth availability.
- If your channel exposes native exec approvals, use `approvalCapability.getExecInitiatingSurfaceState` for the initiating-surface/native-client state when it differs from same-chat approval auth. Core uses that exec-specific hook to distinguish `enabled` vs `disabled`, decide whether the initiating channel supports native exec approvals, and include the channel in native-client fallback guidance. `createApproverRestrictedNativeApprovalCapability(...)` fills this in for the common case.
- If your channel exposes native exec approvals, implement `approvalCapability.getActionAvailabilityState` even when the native transport lives entirely under `approvalCapability.native`. Core uses that availability hook to distinguish `enabled` vs `disabled`, decide whether the initiating channel supports native approvals, and include the channel in native-client fallback guidance.
- Use `outbound.shouldSuppressLocalPayloadPrompt` or `outbound.beforeDeliverPayload` for channel-specific payload lifecycle behavior such as hiding duplicate local approval prompts or sending typing indicators before delivery.
- Use `approvalCapability.delivery` only for native approval routing or fallback suppression.
- Use `approvalCapability.nativeRuntime` for channel-owned native approval facts. Keep it lazy on hot channel entrypoints with `createLazyChannelApprovalNativeRuntimeAdapter(...)`, which can import your runtime module on demand while still letting core assemble the approval lifecycle.
- Use `approvalCapability.render` only when a channel truly needs custom approval payloads instead of the shared renderer.
- Use `approvalCapability.describeExecApprovalSetup` when the channel wants the disabled-path reply to explain the exact config knobs needed to enable native exec approvals. The hook receives `{ channel, channelLabel, accountId }`; named-account channels should render account-scoped paths such as `channels.<channel>.accounts.<id>.execApprovals.*` instead of top-level defaults.
- If a channel can infer stable owner-like DM identities from existing config, use `createResolvedApproverActionAuthAdapter` from `openclaw/plugin-sdk/approval-runtime` to restrict same-chat `/approve` without adding approval-specific core logic.
- If a channel needs native approval delivery, keep channel code focused on target normalization plus transport/presentation facts. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, and `createApproverRestrictedNativeApprovalCapability` from `openclaw/plugin-sdk/approval-runtime`. Put the channel-specific facts behind `approvalCapability.nativeRuntime`, ideally via `createChannelApprovalNativeRuntimeAdapter(...)` or `createLazyChannelApprovalNativeRuntimeAdapter(...)`, so core can assemble the handler and own request filtering, routing, dedupe, expiry, gateway subscription, and routed-elsewhere notices. `nativeRuntime` is split into a few smaller seams:
- `availability` — whether the account is configured and whether a request should be handled
- `presentation` — map the shared approval view model into pending/resolved/expired native payloads or final actions
- `transport` — prepare targets plus send/update/delete native approval messages
- `interactions` — optional bind/unbind/clear-action hooks for native buttons or reactions
- `observe` — optional delivery diagnostics hooks
- If the channel needs runtime-owned objects such as a client, token, Bolt app, or webhook receiver, register them through `openclaw/plugin-sdk/channel-runtime-context`. The generic runtime-context registry lets core bootstrap capability-driven handlers from channel startup state without adding approval-specific wrapper glue.
- Reach for the lower-level `createChannelApprovalHandler` or `createChannelNativeApprovalRuntime` only when the capability-driven seam is not expressive enough yet.
- If a channel needs native approval delivery, keep channel code focused on target normalization and transport hooks. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, `createApproverRestrictedNativeApprovalCapability`, and `createChannelNativeApprovalRuntime` from `openclaw/plugin-sdk/approval-runtime` so core owns request filtering, routing, dedupe, expiry, and gateway subscription.
- Native approval channels must route both `accountId` and `approvalKind` through those helpers. `accountId` keeps multi-account approval policy scoped to the right bot account, and `approvalKind` keeps exec vs plugin approval behavior available to the channel without hardcoded branches in core.
- Core now owns approval reroute notices too. Channel plugins should not send their own "approval went to DMs / another channel" follow-up messages from `createChannelNativeApprovalRuntime`; instead, expose accurate origin + approver-DM routing through the shared approval capability helpers and let core aggregate actual deliveries before posting any notice back to the initiating chat.
- Preserve the delivered approval id kind end-to-end. Native clients should not
guess or rewrite exec vs plugin approval routing from channel-local state.
- Different approval kinds can intentionally expose different native surfaces.
Current bundled examples:
- Slack keeps native approval routing available for both exec and plugin ids.
- Matrix keeps the same native DM/channel routing and reaction UX for exec
and plugin approvals, while still letting auth differ by approval kind.
- Matrix keeps native DM/channel routing for exec approvals only and leaves
plugin approvals on the shared same-chat `/approve` path.
- `createApproverRestrictedNativeApprovalAdapter` still exists as a compatibility wrapper, but new code should prefer the capability builder and expose `approvalCapability` on the plugin.
For hot channel entrypoints, prefer the narrower runtime subpaths when you only
@@ -96,12 +84,8 @@ need one part of that family:
- `openclaw/plugin-sdk/approval-auth-runtime`
- `openclaw/plugin-sdk/approval-client-runtime`
- `openclaw/plugin-sdk/approval-delivery-runtime`
- `openclaw/plugin-sdk/approval-gateway-runtime`
- `openclaw/plugin-sdk/approval-handler-adapter-runtime`
- `openclaw/plugin-sdk/approval-handler-runtime`
- `openclaw/plugin-sdk/approval-native-runtime`
- `openclaw/plugin-sdk/approval-reply-runtime`
- `openclaw/plugin-sdk/channel-runtime-context`
Likewise, prefer `openclaw/plugin-sdk/setup-runtime`,
`openclaw/plugin-sdk/setup-adapter-runtime`,
@@ -168,87 +152,6 @@ surfaces:
Auth-only channels can usually stop at the default path: core handles approvals and the plugin just exposes outbound/auth capabilities. Native approval channels such as Matrix, Slack, Telegram, and custom chat transports should use the shared native helpers instead of rolling their own approval lifecycle.
## Inbound mention policy
Keep inbound mention handling split in two layers:
- plugin-owned evidence gathering
- shared policy evaluation
Use `openclaw/plugin-sdk/channel-inbound` for the shared layer.
Good fit for plugin-local logic:
- reply-to-bot detection
- quoted-bot detection
- thread-participation checks
- service/system-message exclusions
- platform-native caches needed to prove bot participation
Good fit for the shared helper:
- `requireMention`
- explicit mention result
- implicit mention allowlist
- command bypass
- final skip decision
Preferred flow:
1. Compute local mention facts.
2. Pass those facts into `resolveInboundMentionDecision({ facts, policy })`.
3. Use `decision.effectiveWasMentioned`, `decision.shouldBypassMention`, and `decision.shouldSkip` in your inbound gate.
```typescript
import {
implicitMentionKindWhen,
matchesMentionWithExplicit,
resolveInboundMentionDecision,
} from "openclaw/plugin-sdk/channel-inbound";
const mentionMatch = matchesMentionWithExplicit(text, {
mentionRegexes,
mentionPatterns,
});
const facts = {
canDetectMention: true,
wasMentioned: mentionMatch.matched,
hasAnyMention: mentionMatch.hasExplicitMention,
implicitMentionKinds: [
...implicitMentionKindWhen("reply_to_bot", isReplyToBot),
...implicitMentionKindWhen("quoted_bot", isQuoteOfBot),
],
};
const decision = resolveInboundMentionDecision({
facts,
policy: {
isGroup,
requireMention,
allowedImplicitMentionKinds: requireExplicitMention ? [] : ["reply_to_bot", "quoted_bot"],
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
if (decision.shouldSkip) return;
```
`api.runtime.channel.mentions` exposes the same shared mention helpers for
bundled channel plugins that already depend on runtime injection:
- `buildMentionRegexes`
- `matchesMentionPatterns`
- `matchesMentionWithExplicit`
- `implicitMentionKindWhen`
- `resolveInboundMentionDecision`
The older `resolveMentionGating*` helpers remain on
`openclaw/plugin-sdk/channel-inbound` as compatibility exports only. New code
should use `resolveInboundMentionDecision({ facts, policy })`.
## Walkthrough
<Steps>

View File

@@ -67,32 +67,6 @@ Current bundled provider examples:
## How to migrate
<Steps>
<Step title="Migrate approval-native handlers to capability facts">
Approval-capable channel plugins now expose native approval behavior through
`approvalCapability.nativeRuntime` plus the shared runtime-context registry.
Key changes:
- Replace `approvalCapability.handler.loadRuntime(...)` with
`approvalCapability.nativeRuntime`
- Move approval-specific auth/delivery off legacy `plugin.auth` /
`plugin.approvals` wiring and onto `approvalCapability`
- `ChannelPlugin.approvals` has been removed from the public channel-plugin
contract; move delivery/native/render fields onto `approvalCapability`
- `plugin.auth` remains for channel login/logout flows only; approval auth
hooks there are no longer read by core
- Register channel-owned runtime objects such as clients, tokens, or Bolt
apps through `openclaw/plugin-sdk/channel-runtime-context`
- Do not send plugin-owned reroute notices from native approval handlers;
core now owns routed-elsewhere notices from actual delivery results
- When passing `channelRuntime` into `createChannelManager(...)`, provide a
real `createPluginRuntime().channel` surface. Partial stubs are rejected.
See `/plugins/sdk-channel-plugins` for the current approval capability
layout.
</Step>
<Step title="Audit Windows wrapper fallback behavior">
If your plugin uses `openclaw/plugin-sdk/windows-spawn`, unresolved Windows
`.cmd`/`.bat` wrappers now fail closed unless you explicitly pass
@@ -227,12 +201,8 @@ Current bundled provider examples:
| `plugin-sdk/approval-auth-runtime` | Approval auth helpers | Approver resolution, same-chat action auth |
| `plugin-sdk/approval-client-runtime` | Approval client helpers | Native exec approval profile/filter helpers |
| `plugin-sdk/approval-delivery-runtime` | Approval delivery helpers | Native approval capability/delivery adapters |
| `plugin-sdk/approval-gateway-runtime` | Approval gateway helpers | Shared approval gateway-resolution helper |
| `plugin-sdk/approval-handler-adapter-runtime` | Approval adapter helpers | Lightweight native approval adapter loading helpers for hot channel entrypoints |
| `plugin-sdk/approval-handler-runtime` | Approval handler helpers | Broader approval handler runtime helpers; prefer the narrower adapter/gateway seams when they are enough |
| `plugin-sdk/approval-native-runtime` | Approval target helpers | Native approval target/account binding helpers |
| `plugin-sdk/approval-reply-runtime` | Approval reply helpers | Exec/plugin approval reply payload helpers |
| `plugin-sdk/channel-runtime-context` | Channel runtime-context helpers | Generic channel runtime-context register/get/watch helpers |
| `plugin-sdk/security-runtime` | Security helpers | Shared trust, DM gating, external-content, and secret-collection helpers |
| `plugin-sdk/ssrf-policy` | SSRF policy helpers | Host allowlist and private-network policy helpers |
| `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers |
@@ -279,8 +249,7 @@ Current bundled provider examples:
| `plugin-sdk/provider-onboard` | Provider onboarding patches | Onboarding config helpers |
| `plugin-sdk/provider-http` | Provider HTTP helpers | Generic provider HTTP/endpoint capability helpers |
| `plugin-sdk/provider-web-fetch` | Provider web-fetch helpers | Web-fetch provider registration/cache helpers |
| `plugin-sdk/provider-web-search-contract` | Provider web-search contract helpers | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
| `plugin-sdk/provider-web-search` | Provider web-search helpers | Web-search provider registration/cache/runtime helpers |
| `plugin-sdk/provider-web-search` | Provider web-search helpers | Web-search provider registration/cache/config helpers |
| `plugin-sdk/provider-tools` | Provider tool/schema compat helpers | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
| `plugin-sdk/provider-usage` | Provider usage helpers | `fetchClaudeUsage`, `fetchGeminiUsage`, `fetchGithubCopilotUsage`, and other provider usage helpers |
| `plugin-sdk/provider-stream` | Provider stream wrapper helpers | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |

View File

@@ -108,7 +108,7 @@ explicitly promotes one as public.
| `plugin-sdk/group-access` | Shared group-access decision helpers |
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
| `plugin-sdk/channel-inbound` | Inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
| `plugin-sdk/channel-inbound` | Debounce, mention matching, envelope helpers |
| `plugin-sdk/channel-send-result` | Reply result types |
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
@@ -133,10 +133,8 @@ explicitly promotes one as public.
| `plugin-sdk/provider-model-shared` | `ProviderReplayFamily`, `buildProviderReplayFamilyHooks`, `normalizeModelCompat`, shared replay-policy builders, provider-endpoint helpers, and model-id normalization helpers such as `normalizeNativeXaiModelId` |
| `plugin-sdk/provider-catalog-shared` | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` |
| `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers |
| `plugin-sdk/provider-web-fetch-contract` | Narrow web-fetch config/selection contract helpers such as `enablePluginInConfig` and `WebFetchProviderPlugin` |
| `plugin-sdk/provider-web-fetch` | Web-fetch provider registration/cache helpers |
| `plugin-sdk/provider-web-search-contract` | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
| `plugin-sdk/provider-web-search` | Web-search provider registration/cache/runtime helpers |
| `plugin-sdk/provider-web-search` | Web-search provider registration/cache/config helpers |
| `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
| `plugin-sdk/provider-usage` | `fetchClaudeUsage` and similar |
| `plugin-sdk/provider-stream` | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, and shared Anthropic/Bedrock/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers |
@@ -151,9 +149,6 @@ explicitly promotes one as public.
| `plugin-sdk/approval-auth-runtime` | Approver resolution and same-chat action-auth helpers |
| `plugin-sdk/approval-client-runtime` | Native exec approval profile/filter helpers |
| `plugin-sdk/approval-delivery-runtime` | Native approval capability/delivery adapters |
| `plugin-sdk/approval-gateway-runtime` | Shared approval gateway-resolution helper |
| `plugin-sdk/approval-handler-adapter-runtime` | Lightweight native approval adapter loading helpers for hot channel entrypoints |
| `plugin-sdk/approval-handler-runtime` | Broader approval handler runtime helpers; prefer the narrower adapter/gateway seams when they are enough |
| `plugin-sdk/approval-native-runtime` | Native approval target + account-binding helpers |
| `plugin-sdk/approval-reply-runtime` | Exec/plugin approval reply payload helpers |
| `plugin-sdk/command-auth-native` | Native command auth + native session-target helpers |
@@ -161,7 +156,6 @@ explicitly promotes one as public.
| `plugin-sdk/command-surface` | Command-body normalization and command-surface helpers |
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
| `plugin-sdk/security-runtime` | Shared trust, DM gating, external-content, and secret-collection helpers |
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
| `plugin-sdk/ssrf-runtime` | Pinned-dispatcher, SSRF-guarded fetch, and SSRF policy helpers |
@@ -175,7 +169,6 @@ explicitly promotes one as public.
| --- | --- |
| `plugin-sdk/runtime` | Broad runtime/logging/backup/plugin-install helpers |
| `plugin-sdk/runtime-env` | Narrow runtime env, logger, timeout, retry, and backoff helpers |
| `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers |
| `plugin-sdk/runtime-store` | `createPluginRuntimeStore` |
| `plugin-sdk/plugin-runtime` | Shared plugin command/hook/http/interactive helpers |
| `plugin-sdk/hook-runtime` | Shared webhook/internal hook pipeline helpers |
@@ -390,7 +383,6 @@ AI CLI backend such as `codex-cli`.
| Method | What it registers |
| ------------------------------------------ | ------------------------------------- |
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time) |
| `api.registerMemoryCapability(capability)` | Unified memory capability |
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
| `api.registerMemoryRuntime(runtime)` | Memory runtime adapter |
@@ -401,13 +393,8 @@ AI CLI backend such as `codex-cli`.
| ---------------------------------------------- | ---------------------------------------------- |
| `api.registerMemoryEmbeddingProvider(adapter)` | Memory embedding adapter for the active plugin |
- `registerMemoryCapability` is the preferred exclusive memory-plugin API.
- `registerMemoryCapability` may also expose `publicArtifacts.listArtifacts(...)`
so companion plugins can consume exported memory artifacts through
`openclaw/plugin-sdk/memory-host-core` instead of reaching into a specific
memory plugin's private layout.
- `registerMemoryPromptSection`, `registerMemoryFlushPlan`, and
`registerMemoryRuntime` are legacy-compatible exclusive memory-plugin APIs.
`registerMemoryRuntime` are exclusive to memory plugins.
- `registerMemoryEmbeddingProvider` lets the active memory plugin register one
or more embedding adapter ids (for example `openai`, `gemini`, or a custom
plugin-defined id).

View File

@@ -330,46 +330,6 @@ api.runtime.tools.registerMemoryCli(/* ... */);
Channel-specific runtime helpers (available when a channel plugin is loaded).
`api.runtime.channel.mentions` is the shared inbound mention-policy surface for
bundled channel plugins that use runtime injection:
```typescript
const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, {
mentionRegexes,
mentionPatterns,
});
const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({
facts: {
canDetectMention: true,
wasMentioned: mentionMatch.matched,
implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen(
"reply_to_bot",
isReplyToBot,
),
},
policy: {
isGroup,
requireMention,
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
```
Available mention helpers:
- `buildMentionRegexes`
- `matchesMentionPatterns`
- `matchesMentionWithExplicit`
- `implicitMentionKindWhen`
- `resolveInboundMentionDecision`
`api.runtime.channel.mentions` intentionally does not expose the older
`resolveMentionGating*` compatibility helpers. Prefer the normalized
`{ facts, policy }` path.
## Storing runtime references
Use `createPluginRuntimeStore` to store the runtime reference for use outside

View File

@@ -35,7 +35,7 @@ openclaw onboard --auth-choice zai-cn
```json5
{
env: { ZAI_API_KEY: "sk-..." },
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
agents: { defaults: { model: { primary: "zai/glm-5" } } },
}
```
@@ -64,5 +64,5 @@ OpenClaw currently seeds the bundled `zai` provider with these GLM refs:
## Notes
- GLM versions and availability can change; check Z.AI's docs for the latest.
- Default bundled model ref is `zai/glm-5.1`.
- Default bundled model ref is `zai/glm-5`.
- For provider details, see [/providers/zai](/providers/zai).

View File

@@ -52,7 +52,7 @@ An alternative provider `google-gemini-cli` uses PKCE OAuth instead of an API
key. This is an unofficial integration; some users report account
restrictions. Use at your own risk.
- Default model: `google-gemini-cli/gemini-3-flash-preview`
- Default model: `google-gemini-cli/gemini-3.1-pro-preview`
- Alias: `gemini-cli`
- Install prerequisite: local Gemini CLI available as `gemini`
- Homebrew: `brew install gemini-cli`

View File

@@ -42,7 +42,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
- [Google (Gemini)](/providers/google)
- [Groq (LPU inference)](/providers/groq)
- [Hugging Face (Inference)](/providers/huggingface)
- [inferrs (local models)](/providers/inferrs)
- [Kilocode](/providers/kilocode)
- [LiteLLM (unified gateway)](/providers/litellm)
- [MiniMax](/providers/minimax)

View File

@@ -1,173 +0,0 @@
---
summary: "Run OpenClaw through inferrs (OpenAI-compatible local server)"
read_when:
- You want to run OpenClaw against a local inferrs server
- You are serving Gemma or another model through inferrs
- You need the exact OpenClaw compat flags for inferrs
title: "inferrs"
---
# inferrs
[inferrs](https://github.com/ericcurtin/inferrs) can serve local models behind an
OpenAI-compatible `/v1` API. OpenClaw works with `inferrs` through the generic
`openai-completions` path.
`inferrs` is currently best treated as a custom self-hosted OpenAI-compatible
backend, not a dedicated OpenClaw provider plugin.
## Quick start
1. Start `inferrs` with a model.
Example:
```bash
inferrs serve gg-hf-gg/gemma-4-E2B-it \
--host 127.0.0.1 \
--port 8080 \
--device metal
```
2. Verify the server is reachable.
```bash
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/models
```
3. Add an explicit OpenClaw provider entry and point your default model at it.
## Full config example
This example uses Gemma 4 on a local `inferrs` server.
```json5
{
agents: {
defaults: {
model: { primary: "inferrs/gg-hf-gg/gemma-4-E2B-it" },
models: {
"inferrs/gg-hf-gg/gemma-4-E2B-it": {
alias: "Gemma 4 (inferrs)",
},
},
},
},
models: {
mode: "merge",
providers: {
inferrs: {
baseUrl: "http://127.0.0.1:8080/v1",
apiKey: "inferrs-local",
api: "openai-completions",
models: [
{
id: "gg-hf-gg/gemma-4-E2B-it",
name: "Gemma 4 E2B (inferrs)",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 131072,
maxTokens: 4096,
compat: {
requiresStringContent: true,
},
},
],
},
},
},
}
```
## Why `requiresStringContent` matters
Some `inferrs` Chat Completions routes accept only string
`messages[].content`, not structured content-part arrays.
If OpenClaw runs fail with an error like:
```text
messages[1].content: invalid type: sequence, expected a string
```
set:
```json5
compat: {
requiresStringContent: true
}
```
OpenClaw will flatten pure text content parts into plain strings before sending
the request.
## Gemma and tool-schema caveat
Some current `inferrs` + Gemma combinations accept small direct
`/v1/chat/completions` requests but still fail on full OpenClaw agent-runtime
turns.
If that happens, try this first:
```json5
compat: {
requiresStringContent: true,
supportsTools: false
}
```
That disables OpenClaw's tool schema surface for the model and can reduce prompt
pressure on stricter local backends.
If tiny direct requests still work but normal OpenClaw agent turns continue to
crash inside `inferrs`, the remaining issue is usually upstream model/server
behavior rather than OpenClaw's transport layer.
## Manual smoke test
Once configured, test both layers:
```bash
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"gg-hf-gg/gemma-4-E2B-it","messages":[{"role":"user","content":"What is 2 + 2?"}],"stream":false}'
openclaw infer model run \
--model inferrs/gg-hf-gg/gemma-4-E2B-it \
--prompt "What is 2 + 2? Reply with one short sentence." \
--json
```
If the first command works but the second fails, use the troubleshooting notes
below.
## Troubleshooting
- `curl /v1/models` fails: `inferrs` is not running, not reachable, or not
bound to the expected host/port.
- `messages[].content ... expected a string`: set
`compat.requiresStringContent: true`.
- Direct tiny `/v1/chat/completions` calls pass, but `openclaw infer model run`
fails: try `compat.supportsTools: false`.
- OpenClaw no longer gets schema errors, but `inferrs` still crashes on larger
agent turns: treat it as an upstream `inferrs` or model limitation and reduce
prompt pressure or switch local backend/model.
## Proxy-style behavior
`inferrs` is treated as a proxy-style OpenAI-compatible `/v1` backend, not a
native OpenAI endpoint.
- native OpenAI-only request shaping does not apply here
- no `service_tier`, no Responses `store`, no prompt-cache hints, and no
OpenAI reasoning-compat payload shaping
- hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`)
are not injected on custom `inferrs` base URLs
## See also
- [Local models](/gateway/local-models)
- [Gateway troubleshooting](/gateway/troubleshooting#local-openai-compatible-backend-passes-direct-probes-but-agent-runs-fail)
- [Model providers](/concepts/model-providers)

View File

@@ -33,15 +33,15 @@ openclaw onboard --mistral-api-key "$MISTRAL_API_KEY"
OpenClaw currently ships this bundled Mistral catalog:
| Model ref | Input | Context | Max output | Notes |
| -------------------------------- | ----------- | ------- | ---------- | ---------------------------------------------------------------- |
| `mistral/mistral-large-latest` | text, image | 262,144 | 16,384 | Default model |
| `mistral/mistral-medium-2508` | text, image | 262,144 | 8,192 | Mistral Medium 3.1 |
| `mistral/mistral-small-latest` | text, image | 128,000 | 16,384 | Mistral Small 4; adjustable reasoning via API `reasoning_effort` |
| `mistral/pixtral-large-latest` | text, image | 128,000 | 32,768 | Pixtral |
| `mistral/codestral-latest` | text | 256,000 | 4,096 | Coding |
| `mistral/devstral-medium-latest` | text | 262,144 | 32,768 | Devstral 2 |
| `mistral/magistral-small` | text | 128,000 | 40,000 | Reasoning-enabled |
| Model ref | Input | Context | Max output | Notes |
| -------------------------------- | ----------- | ------- | ---------- | ------------------------ |
| `mistral/mistral-large-latest` | text, image | 262,144 | 16,384 | Default model |
| `mistral/mistral-medium-2508` | text, image | 262,144 | 8,192 | Mistral Medium 3.1 |
| `mistral/mistral-small-latest` | text, image | 128,000 | 16,384 | Smaller multimodal model |
| `mistral/pixtral-large-latest` | text, image | 128,000 | 32,768 | Pixtral |
| `mistral/codestral-latest` | text | 256,000 | 4,096 | Coding |
| `mistral/devstral-medium-latest` | text | 262,144 | 32,768 | Devstral 2 |
| `mistral/magistral-small` | text | 128,000 | 40,000 | Reasoning-enabled |
## Config snippet (audio transcription with Voxtral)
@@ -58,17 +58,6 @@ OpenClaw currently ships this bundled Mistral catalog:
}
```
## Adjustable reasoning (`mistral-small-latest`)
`mistral/mistral-small-latest` maps to Mistral Small 4 and supports [adjustable reasoning](https://docs.mistral.ai/capabilities/reasoning/adjustable) on the Chat Completions API via `reasoning_effort` (`none` minimizes extra thinking in the output; `high` surfaces full thinking traces before the final answer).
OpenClaw maps the session **thinking** level to Mistrals API:
- **off** / **minimal**`none`
- **low** / **medium** / **high** / **xhigh** / **adaptive**`high`
Other bundled Mistral catalog models do not use this parameter; keep using `magistral-*` models when you want Mistrals native reasoning-first behavior.
## Notes
- Mistral auth uses `MISTRAL_API_KEY`.

View File

@@ -54,7 +54,7 @@ model as `provider/model`.
- `anthropic-vertex` - implicit Anthropic on Google Vertex support when Vertex credentials are available; no separate onboarding auth choice
- `copilot-proxy` - local VS Code Copilot Proxy bridge; use `openclaw onboard --auth-choice copilot-proxy`
- `google-gemini-cli` - unofficial Gemini CLI OAuth flow; requires a local `gemini` install (`brew install gemini-cli` or `npm install -g @google/gemini-cli`); default model `google-gemini-cli/gemini-3-flash-preview`; use `openclaw onboard --auth-choice google-gemini-cli` or `openclaw models auth login --provider google-gemini-cli --set-default`
- `google-gemini-cli` - unofficial Gemini CLI OAuth flow; requires a local `gemini` install (`brew install gemini-cli` or `npm install -g @google/gemini-cli`); default model `google-gemini-cli/gemini-3.1-pro-preview`; use `openclaw onboard --auth-choice google-gemini-cli` or `openclaw models auth login --provider google-gemini-cli --set-default`
For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration,
see [Model providers](/concepts/model-providers).

View File

@@ -1,14 +1,14 @@
---
summary: "Use NVIDIA's OpenAI-compatible API in OpenClaw"
read_when:
- You want to use open models in OpenClaw for free
- You want to use NVIDIA models in OpenClaw
- You need NVIDIA_API_KEY setup
title: "NVIDIA"
---
# NVIDIA
NVIDIA provides an OpenAI-compatible API at `https://integrate.api.nvidia.com/v1` for open models for free. Authenticate with an API key from [build.nvidia.com](https://build.nvidia.com/settings/api-keys).
NVIDIA provides an OpenAI-compatible API at `https://integrate.api.nvidia.com/v1` for Nemotron and NeMo models. Authenticate with an API key from [NVIDIA NGC](https://catalog.ngc.nvidia.com/).
## CLI setup
@@ -17,7 +17,7 @@ Export the key once, then run onboarding and set an NVIDIA model:
```bash
export NVIDIA_API_KEY="nvapi-..."
openclaw onboard --auth-choice skip
openclaw models set nvidia/nvidia/nemotron-3-super-120b-a12b
openclaw models set nvidia/nvidia/llama-3.1-nemotron-70b-instruct
```
If you still pass `--token`, remember it lands in shell history and `ps` output; prefer the env var when possible.
@@ -37,7 +37,7 @@ If you still pass `--token`, remember it lands in shell history and `ps` output;
},
agents: {
defaults: {
model: { primary: "nvidia/nvidia/nemotron-3-super-120b-a12b" },
model: { primary: "nvidia/nvidia/llama-3.1-nemotron-70b-instruct" },
},
},
}
@@ -45,15 +45,14 @@ If you still pass `--token`, remember it lands in shell history and `ps` output;
## Model IDs
| Model ref | Name | Context | Max output |
| ------------------------------------------ | ---------------------------- | ------- | ---------- |
| `nvidia/nvidia/nemotron-3-super-120b-a12b` | NVIDIA Nemotron 3 Super 120B | 262,144 | 8,192 |
| `nvidia/moonshotai/kimi-k2.5` | Kimi K2.5 | 262,144 | 8,192 |
| `nvidia/minimaxai/minimax-m2.5` | Minimax M2.5 | 196,608 | 8,192 |
| `nvidia/z-ai/glm5` | GLM 5 | 202,752 | 8,192 |
| Model ref | Name | Context | Max output |
| ---------------------------------------------------- | ---------------------------------------- | ------- | ---------- |
| `nvidia/nvidia/llama-3.1-nemotron-70b-instruct` | NVIDIA Llama 3.1 Nemotron 70B Instruct | 131,072 | 4,096 |
| `nvidia/meta/llama-3.3-70b-instruct` | Meta Llama 3.3 70B Instruct | 131,072 | 4,096 |
| `nvidia/nvidia/mistral-nemo-minitron-8b-8k-instruct` | NVIDIA Mistral NeMo Minitron 8B Instruct | 8,192 | 2,048 |
## Notes
- OpenAI-compatible `/v1` endpoint; use an API key from [build.nvidia.com](https://build.nvidia.com/).
- OpenAI-compatible `/v1` endpoint; use an API key from NVIDIA NGC.
- Provider auto-enables when `NVIDIA_API_KEY` is set.
- The bundled catalog is static; costs default to `0` in source.

View File

@@ -57,7 +57,7 @@ openclaw onboard --non-interactive \
2. Pull a local model if you want local inference:
```bash
ollama pull gemma4
ollama pull glm-4.7-flash
# or
ollama pull gpt-oss:20b
# or
@@ -78,12 +78,12 @@ openclaw onboard
- `Local`: local models only
- `Cloud + Local`: local models plus cloud models
- Cloud models such as `kimi-k2.5:cloud`, `minimax-m2.7:cloud`, and `glm-5.1:cloud` do **not** require a local `ollama pull`
- Cloud models such as `kimi-k2.5:cloud`, `minimax-m2.5:cloud`, and `glm-5:cloud` do **not** require a local `ollama pull`
OpenClaw currently suggests:
- local default: `gemma4`
- cloud defaults: `kimi-k2.5:cloud`, `minimax-m2.7:cloud`, `glm-5.1:cloud`
- local default: `glm-4.7-flash`
- cloud defaults: `kimi-k2.5:cloud`, `minimax-m2.5:cloud`, `glm-5:cloud`
5. If you prefer manual setup, enable Ollama for OpenClaw directly (any value works; Ollama doesn't require a real key):
@@ -99,7 +99,7 @@ openclaw config set models.providers.ollama.apiKey "ollama-local"
```bash
openclaw models list
openclaw models set ollama/gemma4
openclaw models set ollama/glm-4.7-flash
```
7. Or set the default in config:
@@ -108,7 +108,7 @@ openclaw models set ollama/gemma4
{
agents: {
defaults: {
model: { primary: "ollama/gemma4" },
model: { primary: "ollama/glm-4.7-flash" },
},
},
}
@@ -229,7 +229,7 @@ Once configured, all your Ollama models are available:
## Cloud models
Cloud models let you run cloud-hosted models (for example `kimi-k2.5:cloud`, `minimax-m2.7:cloud`, `glm-5.1:cloud`) alongside your local models.
Cloud models let you run cloud-hosted models (for example `kimi-k2.5:cloud`, `minimax-m2.5:cloud`, `glm-5:cloud`) alongside your local models.
To use cloud models, select **Cloud + Local** mode during setup. The wizard checks whether you are signed in and opens a browser sign-in flow when needed. If authentication cannot be verified, the wizard falls back to local model defaults.
@@ -355,7 +355,7 @@ To add models:
```bash
ollama list # See what's installed
ollama pull gemma4
ollama pull glm-4.7-flash
ollama pull gpt-oss:20b
ollama pull llama3.3 # Or another model
```

View File

@@ -36,7 +36,7 @@ openclaw onboard --auth-choice zai-cn
```json5
{
env: { ZAI_API_KEY: "sk-..." },
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
agents: { defaults: { model: { primary: "zai/glm-5" } } },
}
```
@@ -65,7 +65,7 @@ OpenClaw currently seeds the bundled `zai` provider with:
## Notes
- GLM models are available as `zai/<model>` (example: `zai/glm-5`).
- Default bundled model ref: `zai/glm-5.1`
- Default bundled model ref: `zai/glm-5`
- Unknown `glm-5*` ids still forward-resolve on the bundled provider path by
synthesizing provider-owned metadata from the `glm-4.7` template when the id
matches the current GLM-5 family shape.

View File

@@ -1,530 +0,0 @@
# QA Refactor
Status: foundational migration landed.
## Goal
Move OpenClaw QA from a split-definition model to a single source of truth:
- scenario metadata
- prompts sent to the model
- setup and teardown
- harness logic
- assertions and success criteria
- artifacts and report hints
The desired end state is a generic QA harness that loads powerful scenario definition files instead of hardcoding most behavior in TypeScript.
## Current State
Primary source of truth now lives in `qa/scenarios/index.md` plus one file per
scenario under `qa/scenarios/*.md`.
Implemented:
- `qa/scenarios/index.md`
- canonical QA pack metadata
- operator identity
- kickoff mission
- `qa/scenarios/*.md`
- one markdown file per scenario
- scenario metadata
- handler bindings
- scenario-specific execution config
- `extensions/qa-lab/src/scenario-catalog.ts`
- markdown pack parser + zod validation
- `extensions/qa-lab/src/qa-agent-bootstrap.ts`
- plan rendering from the markdown pack
- `extensions/qa-lab/src/qa-agent-workspace.ts`
- seeds generated compatibility files plus `QA_SCENARIOS.md`
- `extensions/qa-lab/src/suite.ts`
- selects executable scenarios through markdown-defined handler bindings
- QA bus protocol + UI
- generic inline attachments for image/video/audio/file rendering
Remaining split surfaces:
- `extensions/qa-lab/src/suite.ts`
- still owns most executable custom handler logic
- `extensions/qa-lab/src/report.ts`
- still derives report structure from runtime outputs
So the source-of-truth split is fixed, but execution is still mostly handler-backed rather than fully declarative.
## What The Real Scenario Surface Looks Like
Reading the current suite shows a few distinct scenario classes.
### Simple interaction
- channel baseline
- DM baseline
- threaded follow-up
- model switch
- approval followthrough
- reaction/edit/delete
### Config and runtime mutation
- config patch skill disable
- config apply restart wake-up
- config restart capability flip
- runtime inventory drift check
### Filesystem and repo assertions
- source/docs discovery report
- build Lobster Invaders
- generated image artifact lookup
### Memory orchestration
- memory recall
- memory tools in channel context
- memory failure fallback
- session memory ranking
- thread memory isolation
- memory dreaming sweep
### Tool and plugin integration
- MCP plugin-tools call
- skill visibility
- skill hot install
- native image generation
- image roundtrip
- image understanding from attachment
### Multi-turn and multi-actor
- subagent handoff
- subagent fanout synthesis
- restart recovery style flows
These categories matter because they drive DSL requirements. A flat list of prompt + expected text is not enough.
## Direction
### Single source of truth
Use `qa/scenarios/index.md` plus `qa/scenarios/*.md` as the authored source of
truth.
The pack should stay:
- human-readable in review
- machine-parseable
- rich enough to drive:
- suite execution
- QA workspace bootstrap
- QA Lab UI metadata
- docs/discovery prompts
- report generation
### Preferred authoring format
Use markdown as the top-level format, with structured YAML inside it.
Recommended shape:
- YAML frontmatter
- id
- title
- surface
- tags
- docs refs
- code refs
- model/provider overrides
- prerequisites
- prose sections
- objective
- notes
- debugging hints
- fenced YAML blocks
- setup
- steps
- assertions
- cleanup
This gives:
- better PR readability than giant JSON
- richer context than pure YAML
- strict parsing and zod validation
Raw JSON is acceptable only as an intermediate generated form.
## Proposed Scenario File Shape
Example:
````md
---
id: image-generation-roundtrip
title: Image generation roundtrip
surface: image
tags: [media, image, roundtrip]
models:
primary: openai/gpt-5.4
requires:
tools: [image_generate]
plugins: [openai, qa-channel]
docsRefs:
- docs/help/testing.md
- docs/concepts/model-providers.md
codeRefs:
- extensions/qa-lab/src/suite.ts
- src/gateway/chat-attachments.ts
---
# Objective
Verify generated media is reattached on the follow-up turn.
# Setup
```yaml scenario.setup
- action: config.patch
patch:
agents:
defaults:
imageGenerationModel:
primary: openai/gpt-image-1
- action: session.create
key: agent:qa:image-roundtrip
```
# Steps
```yaml scenario.steps
- action: agent.send
session: agent:qa:image-roundtrip
message: |
Image generation check: generate a QA lighthouse image and summarize it in one short sentence.
- action: artifact.capture
kind: generated-image
promptSnippet: Image generation check
saveAs: lighthouseImage
- action: agent.send
session: agent:qa:image-roundtrip
message: |
Roundtrip image inspection check: describe the generated lighthouse attachment in one short sentence.
attachments:
- fromArtifact: lighthouseImage
```
# Expect
```yaml scenario.expect
- assert: outbound.textIncludes
value: lighthouse
- assert: requestLog.matches
where:
promptIncludes: Roundtrip image inspection check
imageInputCountGte: 1
- assert: artifact.exists
ref: lighthouseImage
```
````
## Runner Capabilities The DSL Must Cover
Based on the current suite, the generic runner needs more than prompt execution.
### Environment and setup actions
- `bus.reset`
- `gateway.waitHealthy`
- `channel.waitReady`
- `session.create`
- `thread.create`
- `workspace.writeSkill`
### Agent turn actions
- `agent.send`
- `agent.wait`
- `bus.injectInbound`
- `bus.injectOutbound`
### Config and runtime actions
- `config.get`
- `config.patch`
- `config.apply`
- `gateway.restart`
- `tools.effective`
- `skills.status`
### File and artifact actions
- `file.write`
- `file.read`
- `file.delete`
- `file.touchTime`
- `artifact.captureGeneratedImage`
- `artifact.capturePath`
### Memory and cron actions
- `memory.indexForce`
- `memory.searchCli`
- `doctor.memory.status`
- `cron.list`
- `cron.run`
- `cron.waitCompletion`
- `sessionTranscript.write`
### MCP actions
- `mcp.callTool`
### Assertions
- `outbound.textIncludes`
- `outbound.inThread`
- `outbound.notInRoot`
- `tool.called`
- `tool.notPresent`
- `skill.visible`
- `skill.disabled`
- `file.contains`
- `memory.contains`
- `requestLog.matches`
- `sessionStore.matches`
- `cron.managedPresent`
- `artifact.exists`
## Variables and Artifact References
The DSL must support saved outputs and later references.
Examples from the current suite:
- create a thread, then reuse `threadId`
- create a session, then reuse `sessionKey`
- generate an image, then attach the file on the next turn
- generate a wake marker string, then assert that it appears later
Needed capabilities:
- `saveAs`
- `${vars.name}`
- `${artifacts.name}`
- typed references for paths, session keys, thread ids, markers, tool outputs
Without variable support, the harness will keep leaking scenario logic back into TypeScript.
## What Should Stay As Escape Hatches
A fully pure declarative runner is not realistic in phase 1.
Some scenarios are inherently orchestration-heavy:
- memory dreaming sweep
- config apply restart wake-up
- config restart capability flip
- generated image artifact resolution by timestamp/path
- discovery-report evaluation
These should use explicit custom handlers for now.
Recommended rule:
- 85-90% declarative
- explicit `customHandler` steps for the hard remainder
- named and documented custom handlers only
- no anonymous inline code in the scenario file
That keeps the generic engine clean while still allowing progress.
## Architecture Change
### Current
Scenario markdown already is the source of truth for:
- suite execution
- workspace bootstrap files
- QA Lab UI scenario catalog
- report metadata
- discovery prompts
Generated compatibility:
- seeded workspace still includes `QA_KICKOFF_TASK.md`
- seeded workspace still includes `QA_SCENARIO_PLAN.md`
- seeded workspace now also includes `QA_SCENARIOS.md`
## Refactor Plan
### Phase 1: loader and schema
Done.
- added `qa/scenarios/index.md`
- split scenarios into `qa/scenarios/*.md`
- added parser for named markdown YAML pack content
- validated with zod
- switched consumers to the parsed pack
- removed repo-level `qa/seed-scenarios.json` and `qa/QA_KICKOFF_TASK.md`
### Phase 2: generic engine
- split `extensions/qa-lab/src/suite.ts` into:
- loader
- engine
- action registry
- assertion registry
- custom handlers
- keep existing helper functions as engine operations
Deliverable:
- engine executes simple declarative scenarios
Start with scenarios that are mostly prompt + wait + assert:
- threaded follow-up
- image understanding from attachment
- skill visibility and invocation
- channel baseline
Deliverable:
- first real markdown-defined scenarios shipping through the generic engine
### Phase 4: migrate medium scenarios
- image generation roundtrip
- memory tools in channel context
- session memory ranking
- subagent handoff
- subagent fanout synthesis
Deliverable:
- variables, artifacts, tool assertions, request-log assertions proven out
### Phase 5: keep hard scenarios on custom handlers
- memory dreaming sweep
- config apply restart wake-up
- config restart capability flip
- runtime inventory drift
Deliverable:
- same authoring format, but with explicit custom-step blocks where needed
### Phase 6: delete hardcoded scenario map
Once the pack coverage is good enough:
- remove most scenario-specific TypeScript branching from `extensions/qa-lab/src/suite.ts`
## Fake Slack / Rich Media Support
The current QA bus is text-first.
Relevant files:
- `extensions/qa-channel/src/protocol.ts`
- `extensions/qa-lab/src/bus-state.ts`
- `extensions/qa-lab/src/bus-queries.ts`
- `extensions/qa-lab/src/bus-server.ts`
- `extensions/qa-lab/web/src/ui-render.ts`
Today the QA bus supports:
- text
- reactions
- threads
It does not yet model inline media attachments.
### Needed transport contract
Add a generic QA bus attachment model:
```ts
type QaBusAttachment = {
id: string;
kind: "image" | "video" | "audio" | "file";
mimeType: string;
fileName?: string;
inline?: boolean;
url?: string;
contentBase64?: string;
width?: number;
height?: number;
durationMs?: number;
altText?: string;
transcript?: string;
};
```
Then add `attachments?: QaBusAttachment[]` to:
- `QaBusMessage`
- `QaBusInboundMessageInput`
- `QaBusOutboundMessageInput`
### Why generic first
Do not build a Slack-only media model.
Instead:
- one generic QA transport model
- multiple renderers on top of it
- current QA Lab chat
- future fake Slack web
- any other fake transport views
This prevents duplicate logic and lets media scenarios stay transport-agnostic.
### UI work needed
Update the QA UI to render:
- inline image preview
- inline audio player
- inline video player
- file attachment chip
The current UI can already render threads and reactions, so attachment rendering should layer onto the same message card model.
### Scenario work enabled by media transport
Once attachments flow through QA bus, we can add richer fake-chat scenarios:
- inline image reply in fake Slack
- audio attachment understanding
- video attachment understanding
- mixed attachment ordering
- thread reply with media retained
## Recommendation
The next implementation chunk should be:
1. add markdown scenario loader + zod schema
2. generate the current catalog from markdown
3. migrate a few simple scenarios first
4. add generic QA bus attachment support
5. render inline image in the QA UI
6. then expand to audio and video
This is the smallest path that proves both goals:
- generic markdown-defined QA
- richer fake messaging surfaces
## Open Questions
- whether scenario files should allow embedded markdown prompt templates with variable interpolation
- whether setup/cleanup should be named sections or just ordered action lists
- whether artifact references should be strongly typed in schema or string-based
- whether custom handlers should live in one registry or per-surface registries
- whether the generated JSON compatibility file should remain checked in during migration

View File

@@ -275,21 +275,6 @@ Implementation: `ensurePiCompactionReserveTokens()` in `src/agents/pi-settings.t
---
## Pluggable compaction providers
Plugins can register a compaction provider via `registerCompactionProvider()` on the plugin API. When `agents.defaults.compaction.provider` is set to a registered provider id, the safeguard extension delegates summarization to that provider instead of the built-in `summarizeInStages` pipeline.
- `provider`: id of a registered compaction provider plugin. Leave unset for default LLM summarization.
- Setting a `provider` forces `mode: "safeguard"`.
- Providers receive the same compaction instructions and identifier-preservation policy as the built-in path.
- The safeguard still preserves recent-turn and split-turn suffix context after provider output.
- If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization automatically.
- Abort/timeout signals are re-thrown (not swallowed) to respect caller cancellation.
Source: `src/plugins/compaction-provider.ts`, `src/agents/pi-hooks/compaction-safeguard.ts`.
---
## User-visible surfaces
You can observe compaction and session state via:

View File

@@ -13,7 +13,7 @@ title: "Tests"
- `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic.
- `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`.
- `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed.
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute eleven sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-ui.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-support-boundary.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-bundled.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process.
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute eight sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process.
- Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes.
- Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites.
- `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests.

View File

@@ -774,19 +774,6 @@ Security and trust notes:
Custom `mcpServers` still work as before. The built-in plugin-tools bridge is an
additional opt-in convenience, not a replacement for generic MCP server config.
### Runtime timeout configuration
The bundled `acpx` plugin defaults embedded runtime turns to a 120-second
timeout. This gives slower harnesses such as Gemini CLI enough time to complete
ACP startup and initialization. Override it if your host needs a different
runtime limit:
```bash
openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
```
Restart the gateway after changing this value.
## Permission configuration
ACP sessions run non-interactively — there is no TTY to approve or deny file-write and shell-exec permission prompts. The acpx plugin provides two config keys that control how permissions are handled:

View File

@@ -557,8 +557,8 @@ Shared behavior:
- Slack approvers can be explicit (`execApprovals.approvers`) or inferred from `commands.ownerAllowFrom`
- Slack native buttons preserve approval id kind, so `plugin:` ids can resolve plugin approvals
without a second Slack-local fallback layer
- Matrix native DM/channel routing and reaction shortcuts handle both exec and plugin approvals;
plugin authorization still comes from `channels.matrix.dm.allowFrom`
- Matrix native DM/channel routing is exec-only; Matrix plugin approvals stay on the shared
same-chat `/approve` and optional `approvals.plugin` forwarding paths
- the requester does not need to be an approver
- the originating chat can approve directly with `/approve` when that chat already supports commands and replies
- native Discord approval buttons route by approval id kind: `plugin:` ids go

View File

@@ -68,9 +68,7 @@ Use `action: "list"` to inspect available providers and models at runtime:
| `count` | number | Number of images to generate (14) |
| `filename` | string | Output filename hint |
Not all providers support all parameters. When a fallback provider supports a nearby geometry option instead of the exact requested one, OpenClaw remaps to the closest supported size, aspect ratio, or resolution before submission. Truly unsupported overrides are still reported in the tool result.
Tool results report the applied settings. When OpenClaw remaps geometry during provider fallback, the returned `size`, `aspectRatio`, and `resolution` values reflect what was actually sent, and `details.normalization` captures the requested-to-applied translation.
Not all providers support all parameters. The tool passes what each provider supports, ignores the rest, and reports dropped overrides in the tool result.
## Configuration
@@ -106,10 +104,6 @@ Notes:
- Auto-detection is auth-aware. A provider default only enters the candidate list
when OpenClaw can actually authenticate that provider.
- Auto-detection is enabled by default. Set
`agents.defaults.mediaGenerationAutoProviderFallback: false` if you want image
generation to use only the explicit `model`, `primary`, and `fallbacks`
entries.
- Use `action: "list"` to inspect the currently registered providers, their
default models, and auth env-var hints.

View File

@@ -131,12 +131,8 @@ Direct generation example:
| `filename` | string | Output filename hint |
Not all providers support all parameters. OpenClaw still validates hard limits
such as input counts before submission. When a provider supports duration but
uses a shorter maximum than the requested value, OpenClaw automatically clamps
to the closest supported duration. Truly unsupported optional hints are ignored
with a warning when the selected provider or model cannot honor them.
Tool results report the applied settings. When OpenClaw clamps duration during provider fallback, the returned `durationSeconds` reflects the submitted value and `details.normalization.durationSeconds` shows the requested-to-applied mapping.
such as input counts before submission, but unsupported optional hints are
ignored with a warning when the selected provider or model cannot honor them.
## Async behavior for the shared provider-backed path
@@ -198,10 +194,6 @@ When generating music, OpenClaw tries providers in this order:
If a provider fails, the next candidate is tried automatically. If all fail, the
error includes details from each attempt.
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want
music generation to use only the explicit `model`, `primary`, and `fallbacks`
entries.
## Provider notes
- Google uses Lyria 3 batch generation. The current bundled flow supports

View File

@@ -39,10 +39,7 @@ They run immediately, are stripped before the model sees the message, and the re
mcp: false,
plugins: false,
debug: false,
restart: true,
ownerAllowFrom: ["discord:123456789012345678"],
ownerDisplay: "raw",
ownerDisplaySecret: "${OWNER_ID_HASH_SECRET}",
restart: false,
allowFrom: {
"*": ["user1"],
discord: ["user:123"],
@@ -67,10 +64,6 @@ They run immediately, are stripped before the model sees the message, and the re
- `commands.mcp` (default `false`) enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus install + enable/disable controls).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.restart` (default `true`) enables `/restart` plus gateway restart tool actions.
- `commands.ownerAllowFrom` (optional) sets the explicit owner allowlist for owner-only command/tool surfaces. This is separate from `commands.allowFrom`.
- `commands.ownerDisplay` controls how owner ids appear in the system prompt: `raw` or `hash`.
- `commands.ownerDisplaySecret` optionally sets the HMAC secret used when `commands.ownerDisplay="hash"`.
- `commands.allowFrom` (optional) sets a per-provider allowlist for command authorization. When configured, it is the
only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups`
are ignored). Use `"*"` for a global default; provider-specific keys override it.
@@ -78,94 +71,65 @@ They run immediately, are stripped before the model sees the message, and the re
## Command list
Current source-of-truth:
Text + native (when enabled):
- core built-ins come from `src/auto-reply/commands-registry.shared.ts`
- generated dock commands come from `src/auto-reply/commands-registry.data.ts`
- plugin commands come from plugin `registerCommand()` calls
- actual availability on your gateway still depends on config flags, channel surface, and installed/enabled plugins
- `/help`
- `/commands`
- `/tools [compact|verbose]` (show what the current agent can use right now; `verbose` adds descriptions)
- `/skill <name> [input]` (run a skill by name)
- `/status` (show current status; includes provider usage/quota for the current model provider when available)
- `/tasks` (list background tasks for the current session; shows active and recent task details with agent-local fallback counts)
- `/allowlist` (list/add/remove allowlist entries)
- `/approve <id> <decision>` (resolve exec approval prompts; use the pending approval message for the available decisions)
- `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
- `/btw <question>` (ask an ephemeral side question about the current session without changing future session context; see [/tools/btw](/tools/btw))
- `/export-session [path]` (alias: `/export`) (export current session to HTML with full system prompt)
- `/whoami` (show your sender id; alias: `/id`)
- `/session idle <duration|off>` (manage inactivity auto-unfocus for focused thread bindings)
- `/session max-age <duration|off>` (manage hard max-age auto-unfocus for focused thread bindings)
- `/subagents list|kill|log|info|send|steer|spawn` (inspect, control, or spawn sub-agent runs for the current session)
- `/acp spawn|cancel|steer|close|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|sessions` (inspect and control ACP runtime sessions)
- `/agents` (list thread-bound agents for this session)
- `/focus <target>` (Discord: bind this thread, or a new thread, to a session/subagent target)
- `/unfocus` (Discord: remove the current thread binding)
- `/kill <id|#|all>` (immediately abort one or all running sub-agents for this session; no confirmation message)
- `/steer <id|#> <message>` (steer a running sub-agent immediately: in-run when possible, otherwise abort current work and restart on the steer message)
- `/tell <id|#> <message>` (alias for `/steer`)
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
- `/mcp show|get|set|unset` (manage OpenClaw MCP server config, owner-only; requires `commands.mcp: true`)
- `/plugins list|show|get|install|enable|disable` (inspect discovered plugins, install new ones, and toggle enablement; owner-only for writes; requires `commands.plugins: true`)
- `/plugin` is an alias for `/plugins`.
- `/plugin install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
- Enable/disable writes still reply with a restart hint. On a watched foreground gateway, OpenClaw may perform that restart automatically right after the write.
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tools/tts))
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
- `/stop`
- `/restart`
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
- `/dock-discord` (alias: `/dock_discord`) (switch replies to Discord)
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
- `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only)
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
- `/fast status|on|off` (omitting the arg shows the current effective fast-mode state)
- `/verbose on|full|off` (alias: `/v`)
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
- `/elevated on|off|ask|full` (alias: `/elev`; `full` skips exec approvals)
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` (send `/exec` to show current)
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
- `/dreaming [on|off|status|help]` (toggle global dreaming or show status; see [Dreaming](/concepts/dreaming))
### Core built-in commands
Text-only:
Built-in commands available today:
- `/new [model]` starts a new session; `/reset` is the reset alias.
- `/compact [instructions]` compacts the session context. See [/concepts/compaction](/concepts/compaction).
- `/stop` aborts the current run.
- `/session idle <duration|off>` and `/session max-age <duration|off>` manage thread-binding expiry.
- `/think <off|minimal|low|medium|high|xhigh>` sets the thinking level. Aliases: `/thinking`, `/t`.
- `/verbose on|off|full` toggles verbose output. Alias: `/v`.
- `/fast [status|on|off]` shows or sets fast mode.
- `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`.
- `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`.
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
- `/model [name|#|status]` shows or sets the model.
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists providers or models for a provider.
- `/queue <mode>` manages queue behavior (`steer`, `interrupt`, `followup`, `collect`, `steer-backlog`) plus options like `debounce:2s cap:25 drop:summarize`.
- `/help` shows the short help summary.
- `/commands` shows the generated command catalog.
- `/tools [compact|verbose]` shows what the current agent can use right now.
- `/status` shows runtime status, including provider usage/quota when available.
- `/tasks` lists active/recent background tasks for the current session.
- `/context [list|detail|json]` explains how context is assembled.
- `/export-session [path]` exports the current session to HTML. Alias: `/export`.
- `/whoami` shows your sender id. Alias: `/id`.
- `/skill <name> [input]` runs a skill by name.
- `/allowlist [list|add|remove] ...` manages allowlist entries. Text-only.
- `/approve <id> <decision>` resolves exec approval prompts.
- `/btw <question>` asks a side question without changing future session context. See [/tools/btw](/tools/btw).
- `/subagents list|kill|log|info|send|steer|spawn` manages sub-agent runs for the current session.
- `/acp spawn|cancel|steer|close|sessions|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|help` manages ACP sessions and runtime options.
- `/focus <target>` binds the current Discord thread or Telegram topic/conversation to a session target.
- `/unfocus` removes the current binding.
- `/agents` lists thread-bound agents for the current session.
- `/kill <id|#|all>` aborts one or all running sub-agents.
- `/steer <id|#> <message>` sends steering to a running sub-agent. Alias: `/tell`.
- `/config show|get|set|unset` reads or writes `openclaw.json`. Owner-only. Requires `commands.config: true`.
- `/mcp show|get|set|unset` reads or writes OpenClaw-managed MCP server config under `mcp.servers`. Owner-only. Requires `commands.mcp: true`.
- `/plugins list|inspect|show|get|install|enable|disable` inspects or mutates plugin state. `/plugin` is an alias. Owner-only for writes. Requires `commands.plugins: true`.
- `/debug show|set|unset|reset` manages runtime-only config overrides. Owner-only. Requires `commands.debug: true`.
- `/usage off|tokens|full|cost` controls the per-response usage footer or prints a local cost summary.
- `/tts on|off|status|provider|limit|summary|audio|help` controls TTS. See [/tools/tts](/tools/tts).
- `/restart` restarts OpenClaw when enabled. Default: enabled; set `commands.restart: false` to disable it.
- `/activation mention|always` sets group activation mode.
- `/send on|off|inherit` sets send policy. Owner-only.
- `/bash <command>` runs a host shell command. Text-only. Alias: `! <command>`. Requires `commands.bash: true` plus `tools.elevated` allowlists.
- `!poll [sessionId]` checks a background bash job.
- `!stop [sessionId]` stops a background bash job.
### Generated dock commands
Dock commands are generated from channel plugins with native-command support. Current bundled set:
- `/dock-discord` (alias: `/dock_discord`)
- `/dock-mattermost` (alias: `/dock_mattermost`)
- `/dock-slack` (alias: `/dock_slack`)
- `/dock-telegram` (alias: `/dock_telegram`)
### Bundled plugin commands
Bundled plugins can add more slash commands. Current bundled commands in this repo:
- `/dreaming [on|off|status|help]` toggles memory dreaming. See [Dreaming](/concepts/dreaming).
- `/pair [qr|status|pending|approve|cleanup|notify]` manages device pairing/setup flow. See [Pairing](/channels/pairing).
- `/phone status|arm <camera|screen|writes|all> [duration]|disarm` temporarily arms high-risk phone node commands.
- `/voice status|list [limit]|set <voiceId|name>` manages Talk voice config. On Discord, the native command name is `/talkvoice`.
- `/card ...` sends LINE rich card presets. See [LINE](/channels/line).
- QQBot-only commands:
- `/bot-ping`
- `/bot-version`
- `/bot-help`
- `/bot-upgrade`
- `/bot-logs`
### Dynamic skill commands
User-invocable skills are also exposed as slash commands:
- `/skill <name> [input]` always works as the generic entrypoint.
- skills may also appear as direct commands like `/prose` when the skill/plugin registers them.
- native skill-command registration is controlled by `commands.nativeSkills` and `channels.<provider>.commands.nativeSkills`.
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
- `! <command>` (host-only; one at a time; use `!poll` + `!stop` for long-running jobs)
- `!poll` (check output / status; accepts optional `sessionId`; `/bash poll` also works)
- `!stop` (stop the running bash job; accepts optional `sessionId`; `/bash stop` also works)
Notes:
@@ -176,8 +140,6 @@ Notes:
- In multi-account channels, config-targeted `/allowlist --account <id>` and `/config set channels.<provider>.accounts.<id>...` also honor the target account's `configWrites`.
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from OpenClaw session logs.
- `/restart` is enabled by default; set `commands.restart: false` to disable it.
- `/plugins install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
- `/plugins enable|disable` updates plugin config and may prompt for a restart.
- Discord-only native command: `/vc join|leave|status` controls voice channels (requires `channels.discord.voice` and native commands; not available as text).
- Discord thread-binding commands (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`) require effective thread bindings to be enabled (`session.threadBindings.enabled` and/or `channels.discord.threadBindings.enabled`).
- ACP command reference and runtime behavior: [ACP Agents](/tools/acp-agents).

View File

@@ -59,7 +59,7 @@ so that provider must also be authenticated if you enable summaries.
## Is it enabled by default?
No. AutoTTS is **off** by default. Enable it in config with
`messages.tts.auto` or locally with `/tts on`.
`messages.tts.auto` or per session with `/tts always` (alias: `/tts on`).
When `messages.tts.provider` is unset, OpenClaw picks the first configured
speech provider in registry auto-select order.
@@ -411,7 +411,9 @@ Discord note: `/tts` is a built-in Discord command, so OpenClaw registers
```
/tts off
/tts on
/tts always
/tts inbound
/tts tagged
/tts status
/tts provider openai
/tts limit 2000
@@ -423,9 +425,7 @@ Notes:
- Commands require an authorized sender (allowlist/owner rules still apply).
- `commands.text` or native command registration must be enabled.
- Config `messages.tts.auto` accepts `off|always|inbound|tagged`.
- `/tts on` writes the local TTS preference to `always`; `/tts off` writes it to `off`.
- Use config when you want `inbound` or `tagged` defaults.
- `off|always|inbound|tagged` are persession toggles (`/tts on` is an alias for `/tts always`).
- `limit` and `summary` are stored in local prefs, not the main config.
- `/tts audio` generates a one-off audio reply (does not toggle TTS on).
- `/tts status` includes fallback visibility for the latest attempt:

View File

@@ -154,9 +154,7 @@ and the shared live sweep.
| `model` | string | Provider/model override (e.g. `runway/gen4.5`) |
| `filename` | string | Output filename hint |
Not all providers support all parameters. OpenClaw already normalizes duration to the closest provider-supported value, and it also remaps translated geometry hints such as size-to-aspect-ratio when a fallback provider exposes a different control surface. Truly unsupported overrides are ignored on a best-effort basis and reported as warnings in the tool result. Hard capability limits (such as too many reference inputs) fail before submission.
Tool results report the applied settings. When OpenClaw remaps duration or geometry during provider fallback, the returned `durationSeconds`, `size`, `aspectRatio`, and `resolution` values reflect what was submitted, and `details.normalization` captures the requested-to-applied translation.
Not all providers support all parameters. Unsupported overrides are ignored on a best-effort basis and reported as warnings in the tool result. Hard capability limits (such as too many reference inputs) fail before submission.
Reference inputs also select the runtime mode:
@@ -184,10 +182,6 @@ When generating a video, OpenClaw resolves the model in this order:
If a provider fails, the next candidate is tried automatically. If all candidates fail, the error includes details from each attempt.
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want
video generation to use only the explicit `model`, `primary`, and `fallbacks`
entries.
```json5
{
agents: {

View File

@@ -32,8 +32,7 @@
},
"timeoutSeconds": {
"type": "number",
"minimum": 0.001,
"default": 120
"minimum": 0.001
},
"queueOwnerTtlSeconds": {
"type": "number",
@@ -107,7 +106,7 @@
},
"timeoutSeconds": {
"label": "Prompt Timeout Seconds",
"help": "Timeout for each embedded runtime turn. Defaults to 120 seconds so slower Gemini CLI ACP startups have room to initialize.",
"help": "Optional timeout for each embedded runtime turn.",
"advanced": true
},
"queueOwnerTtlSeconds": {

View File

@@ -1,13 +1,10 @@
{
"name": "@openclaw/acpx",
"version": "2026.4.8",
"version": "2026.4.6",
"description": "OpenClaw ACP runtime backend",
"type": "module",
"dependencies": {
"acpx": "0.5.2"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
"acpx": "0.5.0"
},
"openclaw": {
"extensions": [

View File

@@ -1,5 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
export default definePluginEntry({
id: "acpx",
@@ -7,7 +6,8 @@ export default definePluginEntry({
description: "Lightweight ACPX setup hooks",
register(api) {
api.registerAutoEnableProbe(({ config }) => {
const backendRaw = normalizeLowercaseStringOrEmpty(config.acp?.backend);
const backendRaw =
typeof config.acp?.backend === "string" ? config.acp.backend.trim().toLowerCase() : "";
const configured =
config.acp?.enabled === true ||
config.acp?.dispatch?.enabled === true ||

View File

@@ -1,60 +0,0 @@
declare module "acpx/runtime" {
export const ACPX_BACKEND_ID: string;
export type AcpRuntimeDoctorReport = import("../runtime-api.js").AcpRuntimeDoctorReport;
export type AcpRuntimeEnsureInput = import("../runtime-api.js").AcpRuntimeEnsureInput;
export type AcpRuntimeEvent = import("../runtime-api.js").AcpRuntimeEvent;
export type AcpRuntimeHandle = import("../runtime-api.js").AcpRuntimeHandle;
export type AcpRuntimeCapabilities = import("../runtime-api.js").AcpRuntimeCapabilities;
export type AcpRuntimeStatus = import("../runtime-api.js").AcpRuntimeStatus;
export type AcpRuntimeTurnInput = import("../runtime-api.js").AcpRuntimeTurnInput;
export type AcpAgentRegistry = {
resolve(agent: string): string | undefined;
list(): string[];
};
export type AcpSessionRecord = Record<string, unknown>;
export type AcpSessionStore = {
load(sessionId: string): Promise<AcpSessionRecord | undefined>;
save(record: AcpSessionRecord): Promise<void>;
};
export type AcpRuntimeOptions = {
cwd: string;
sessionStore: AcpSessionStore;
agentRegistry: AcpAgentRegistry;
mcpServers?: unknown;
permissionMode?: unknown;
nonInteractivePermissions?: unknown;
timeoutMs?: number;
};
export class AcpxRuntime {
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
isHealthy(): boolean;
probeAvailability(): Promise<void>;
doctor(): Promise<AcpRuntimeDoctorReport>;
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
getCapabilities(input?: {
handle?: AcpRuntimeHandle;
}): AcpRuntimeCapabilities | Promise<AcpRuntimeCapabilities>;
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
close(input: {
handle: AcpRuntimeHandle;
reason?: string;
discardPersistentState?: boolean;
}): Promise<void>;
}
export function createAcpRuntime(...args: unknown[]): AcpxRuntime;
export function createAgentRegistry(params: { overrides?: unknown }): AcpAgentRegistry;
export function createFileSessionStore(params: { stateDir: string }): AcpSessionStore;
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
}

55
extensions/acpx/src/acpx-runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,55 @@
declare module "acpx/runtime" {
export const ACPX_BACKEND_ID: string;
export type AcpRuntimeDoctorReport =
import("../../../src/acp/runtime/types.js").AcpRuntimeDoctorReport;
export type AcpRuntimeEnsureInput =
import("../../../src/acp/runtime/types.js").AcpRuntimeEnsureInput;
export type AcpRuntimeEvent = import("../../../src/acp/runtime/types.js").AcpRuntimeEvent;
export type AcpRuntimeHandle = import("../../../src/acp/runtime/types.js").AcpRuntimeHandle;
export type AcpRuntimeTurnInput = import("../../../src/acp/runtime/types.js").AcpRuntimeTurnInput;
export type AcpRuntimeStatus = import("../../../src/acp/runtime/types.js").AcpRuntimeStatus;
export type AcpRuntimeCapabilities =
import("../../../src/acp/runtime/types.js").AcpRuntimeCapabilities;
export type AcpSessionStore = {
load(sessionId: string): Promise<unknown>;
save(record: unknown): Promise<void>;
};
export type AcpAgentRegistry = {
resolve(agentId: string): string;
list(): string[];
};
export type AcpRuntimeOptions = {
cwd: string;
sessionStore: AcpSessionStore;
agentRegistry: AcpAgentRegistry;
permissionMode: string;
mcpServers?: unknown[];
nonInteractivePermissions?: unknown;
timeoutMs?: number;
};
export class AcpxRuntime {
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
isHealthy(): boolean;
probeAvailability(): Promise<void>;
doctor(): Promise<AcpRuntimeDoctorReport>;
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
getCapabilities(input?: { handle?: AcpRuntimeHandle }): AcpRuntimeCapabilities;
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
close(input: { handle: AcpRuntimeHandle; reason: string }): Promise<void>;
}
export function createAcpRuntime(...args: unknown[]): unknown;
export function createAgentRegistry(...args: unknown[]): AcpAgentRegistry;
export function createFileSessionStore(...args: unknown[]): AcpSessionStore;
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
}

View File

@@ -8,8 +8,6 @@ export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
export const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
export type McpServerConfig = {
command: string;
args?: string[];
@@ -94,7 +92,7 @@ export const AcpxPluginConfigSchema = z.strictObject({
timeoutSeconds: z
.number({ error: "timeoutSeconds must be a number >= 0.001" })
.min(0.001, { error: "timeoutSeconds must be a number >= 0.001" })
.default(DEFAULT_ACPX_TIMEOUT_SECONDS),
.optional(),
queueOwnerTtlSeconds: z
.number({ error: "queueOwnerTtlSeconds must be a number >= 0" })
.min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" })

View File

@@ -15,21 +15,9 @@ describe("embedded acpx plugin config", () => {
expect(resolved.stateDir).toBe(path.join(workspaceDir, "state"));
expect(resolved.permissionMode).toBe("approve-reads");
expect(resolved.nonInteractivePermissions).toBe("fail");
expect(resolved.timeoutSeconds).toBe(120);
expect(resolved.agents).toEqual({});
});
it("keeps explicit timeoutSeconds config", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
timeoutSeconds: 300,
},
workspaceDir: "/tmp/openclaw-acpx",
});
expect(resolved.timeoutSeconds).toBe(300);
});
it("accepts agent command overrides", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
@@ -74,9 +62,6 @@ describe("embedded acpx plugin config", () => {
properties: expect.objectContaining({
cwd: expect.any(Object),
stateDir: expect.any(Object),
timeoutSeconds: expect.objectContaining({
default: 120,
}),
agents: expect.any(Object),
mcpServers: expect.any(Object),
}),

View File

@@ -1,9 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { z } from "openclaw/plugin-sdk/zod";
import { AcpxPluginConfigSchema, DEFAULT_ACPX_TIMEOUT_SECONDS } from "./config-schema.js";
import { AcpxPluginConfigSchema } from "./config-schema.js";
import type {
AcpxPluginConfig,
AcpxPermissionMode,
@@ -224,7 +223,7 @@ export function resolveAcpxPluginConfig(params: {
});
const agents = Object.fromEntries(
Object.entries(normalized.agents ?? {}).map(([name, entry]) => [
normalizeLowercaseStringOrEmpty(name),
name.trim().toLowerCase(),
entry.command.trim(),
]),
);
@@ -238,7 +237,7 @@ export function resolveAcpxPluginConfig(params: {
pluginToolsMcpBridge,
strictWindowsCmdWrapper:
normalized.strictWindowsCmdWrapper ?? DEFAULT_STRICT_WINDOWS_CMD_WRAPPER,
timeoutSeconds: normalized.timeoutSeconds ?? DEFAULT_ACPX_TIMEOUT_SECONDS,
timeoutSeconds: normalized.timeoutSeconds,
queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS,
legacyCompatibilityConfig: {
strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper,

View File

@@ -1,6 +0,0 @@
export function formatErrorMessage(error) {
if (error instanceof Error) {
return error.message || error.name || "Error";
}
return String(error);
}

View File

@@ -4,7 +4,6 @@ import { spawn } from "node:child_process";
import path from "node:path";
import { createInterface } from "node:readline";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "./error-format.mjs";
import { splitCommandLine } from "./mcp-command-line.mjs";
function decodePayload(argv) {
@@ -95,7 +94,7 @@ function main() {
child.stdout.pipe(process.stdout);
child.on("error", (error) => {
process.stderr.write(`${formatErrorMessage(error)}\n`);
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
});

Some files were not shown because too many files have changed in this diff Show More