Compare commits

..

422 Commits

Author SHA1 Message Date
Vincent Koc
1f267de142 docs(changelog): note cold provider setup guidance 2026-04-25 23:16:59 -07:00
Vincent Koc
585784643e fix(providers): keep setup guidance cold 2026-04-25 23:16:59 -07:00
Peter Steinberger
b979f2964c fix: warn on low disk before runtime dependency staging 2026-04-26 07:16:26 +01:00
Ayaan Zaidi
e633f43c53 fix: strip antml thinking tags in streaming (#69288) (thanks @xialonglee) 2026-04-26 11:46:06 +05:30
Ayaan Zaidi
4bfa7d17a3 refactor(agents): dedupe thinking tag scanner 2026-04-26 11:46:06 +05:30
xialonglee
d7da3d470e fix(agents): strip antml thinking tags in streaming 2026-04-26 11:46:06 +05:30
Peter Steinberger
40e5d9adc7 fix(plugins): update external plugins in recorded root 2026-04-26 07:14:30 +01:00
Vincent Koc
1b99f8aedb docs(diffs): rewrite with Steps, Tabs, ParamField, and AccordionGroup for clearer mode and security guidance 2026-04-25 23:11:13 -07:00
Vincent Koc
eb769ee4ec docs(context-engine): rewrite with Steps, Tabs, AccordionGroup, and ParamField for engine lifecycle clarity 2026-04-25 23:09:26 -07:00
Peter Steinberger
7c6c0a8d54 fix: avoid persisted-auth channel startup probes 2026-04-26 07:09:21 +01:00
Ayaan Zaidi
1ed8c41f33 fix: clear deselected model fallbacks (#71596) (thanks @rubencu) 2026-04-26 11:39:17 +05:30
Ayaan Zaidi
6cc74595e3 refactor(configure): distill fallback selection merge 2026-04-26 11:39:17 +05:30
Ruben Cuevas
1377baee1a fix(configure): clear deselected model fallbacks 2026-04-26 11:39:17 +05:30
Vincent Koc
ce04866019 docs(tasks): rewrite with Tabs for quick start and AccordionGroup for CLI reference and runtime relationships 2026-04-25 23:07:58 -07:00
Peter Steinberger
57c1c7d886 fix(protocol): refresh generated swift models 2026-04-26 07:07:28 +01:00
Vincent Koc
48d83b7566 docs(cron-jobs): rewrite around Steps, Tabs, ParamField, AccordionGroup, and Warning callouts 2026-04-25 23:05:57 -07:00
Peter Steinberger
5a89330c33 fix(installer): fall back from stale nvm dir 2026-04-26 07:04:40 +01:00
Peter Steinberger
e67093f333 chore(plugin-sdk): refresh api baseline 2026-04-26 07:03:25 +01:00
Peter Steinberger
d613c8e29b refactor(tts): resolve voice delivery from channel capabilities 2026-04-26 07:03:25 +01:00
Vincent Koc
2784710f4d docs(acp-agents): rewrite around Steps, Tabs, ParamField, and AccordionGroup for operator runbook clarity 2026-04-25 23:03:14 -07:00
Peter Steinberger
ee2ab9a644 fix(plugins): install optional plugin dependencies 2026-04-26 07:00:16 +01:00
Peter Steinberger
54f4c45e5d fix: stabilize model run probes 2026-04-26 06:59:22 +01:00
Peter Steinberger
6ff7a30b9f fix(providers): guard optional provider index installs 2026-04-26 06:56:42 +01:00
Vincent Koc
cd89adf0ac fix(logging): rotate file logs instead of suppressing 2026-04-25 22:55:33 -07:00
Vincent Koc
e54f5c4068 docs(clawhub): rewrite around Steps Tabs and AccordionGroup
The clawhub doc was 358 lines mixing two install-command bullet
blocks, a 'beginner-friendly' prose walkthrough, six sequential
flat CLI command sections (Auth, Search, Install, Update, List,
Publish skills, Publish plugins, Delete, Sync), and a workflow
section that repeated the same commands a third time.

Restructure for scan-first reading without losing reference detail:

- Wrap Quick start in a 4-step Steps component (search -> install
  -> use -> publish optional CLI install).
- Convert the duplicate native-OpenClaw skills/plugins blocks into
  a single Tabs component with one tab per surface, keeping the
  validation/safety notes inline.
- Convert the service-features bullet list to a 7-row table.
- Move reporting and moderation rules into a 2-panel
  AccordionGroup.
- Convert the eight CLI command sections into one AccordionGroup
  (Auth / Search / Install update list / Publish skills / Publish
  plugins / Delete undelete / Sync) so the flat command catalog
  collapses.
- Convert the global-options bullet list into ParamField
  definitions.
- Consolidate the duplicate workflows section into a single Tabs
  component (Search / Install / Update all / Publish single /
  Sync many / Publish plugin from GitHub).
- Move versioning, lockfile, sync fallback, storage, and telemetry
  notes into a dedicated AccordionGroup.
- Convert the env vars bullet list into a 5-row table.
- Drop the duplicate 'How it works / What you can do / Quick start
  (non-technical)' prose; the same content lives in the new Quick
  start Steps and 'What ClawHub is' summary.
- Sentence-case the Related list and add a missing 'Plugins' link.
- Add sidebarTitle for explicit nav.

CLI flags, command parameters, registry semantics, lockfile path,
moderation thresholds (1-week account age, 20-report cap, 3-report
auto-hide), telemetry env var, and required plugin package.json
metadata are unchanged. Pure restructure plus Mintlify upgrades.
2026-04-25 22:53:54 -07:00
Vincent Koc
50c427efc8 fix(providers): export provider index install types 2026-04-25 22:52:21 -07:00
Vincent Koc
62a5963d24 feat(providers): add provider index install metadata 2026-04-25 22:52:21 -07:00
Vincent Koc
194818960c fix(providers): keep setup flow on cold metadata 2026-04-25 22:52:21 -07:00
Vincent Koc
fd35ba2cad docs(exec-approvals): rewrite around ParamField, Steps, and tables
The exec-approvals doc was 379 lines mixing inspection commands as
free bullets, bullet-list policy knobs (security/ask/askFallback/
strictInlineEval), a long YOLO-mode walkthrough split between two
shell blocks, and a stray dangling HTML comment from a prior split
to the advanced page.

Restructure for scan-first reading without losing operational detail:

- Convert 'Inspecting the effective policy' command bullets into a
  command/result table.
- Convert each policy knob (security, ask, askFallback,
  strictInlineEval) into a ParamField definition so type/values are
  visually distinct.
- Wrap the persistent gateway-host YOLO setup in a Steps component
  (config -> approvals file).
- Move the YOLO 'pick which layer' note and 'auto vs YOLO'
  distinctions into a Warning callout instead of buried inline
  bullets.
- Convert the YOLO layer summary into a 3-row layer/setting table.
- Move the local-only exec-policy limitations into a Note callout.
- Convert allowlist entry fields (id, lastUsedAt, lastUsedCommand,
  lastResolvedPath) into a 4-row table.
- Surface Auto-allow trust caveats as a Warning callout.
- Drop the dangling HTML comment '<!-- moved to /tools/exec-approvals-advanced -->'.
- Sentence-case 'YOLO mode (no-approval)' replacing the inverted
  quote variant ('No-approval YOLO mode').
- Add sidebarTitle for explicit nav.

Trust model, schema example, host approvals JSON shape, allowlist
glob rules, ask-fallback semantics, system-event names, deny-rerun
guard, and the trailing CardGroup of related entries are unchanged.
Pure restructure plus Mintlify component upgrades.
2026-04-25 22:51:59 -07:00
Peter Steinberger
db0864ad41 fix(installer): warn about duplicate global installs 2026-04-26 06:50:33 +01:00
Shakker
d5eae0d959 fix: keep agent provider status on plugin index 2026-04-26 06:49:48 +01:00
Vincent Koc
bf2c992a86 docs(subagents): rewrite around AccordionGroup, ParamField, and Steps
The sub-agents doc was 412 lines of dense bullet lists describing
spawn behavior, tool params, thread binding flow, allowlist rules,
auto-archive behavior, announce semantics, and the sessions_history
sanitization pipeline.

Restructure for scan-first reading without losing reference detail:

- Move spawn-behavior bullets into an AccordionGroup with four
  panels (Non-blocking + push-based; Manual-spawn delivery
  resilience; Completion handoff metadata; Modes and ACP runtime).
- Convert sessions_spawn tool params into ParamField definitions so
  type/default/required render visually.
- Wrap the thread-binding flow in a Steps component (spawn -> bind
  -> route -> inspect timeouts -> detach).
- Convert manual thread controls into a 5-row table.
- Convert allowlist fields (allowAgents, requireAgentId) into
  ParamField definitions.
- Convert announce-context fields into a 6-row source/field table.
- Surface the cost-budget guidance, sessions_spawn delivery-param
  exclusion, operational guidance, and the PAIRING_REQUIRED caller
  caveat as Note/Warning callouts where they were buried inline.
- Sentence-case 'Tool Policy' to 'Tool policy' (heading-case fix).
- Sentence-case 'Configuration Reference' link.
- Alphabetize the Related list and add 'Background tasks' which was
  referenced inline but missing from Related.
- Add sidebarTitle for explicit nav.

Tool surface, depth tables, slash commands, defaults, allowlist
semantics, sandbox-inheritance guard, per-depth tool policy,
auto-archive timing, announce status sourcing, sessions_history
normalization steps, concurrency lane, recovery rules, and
limitations are unchanged. Pure restructure plus Mintlify upgrades.
2026-04-25 22:49:14 -07:00
Peter Steinberger
e69c2853b2 fix(cli): handle Volta shim respawns 2026-04-26 06:48:50 +01:00
Peter Steinberger
e4e69c5bc6 fix: retry systemd unit activation after reload 2026-04-26 06:47:29 +01:00
Vincent Koc
2b29594611 docs(skills): rewrite around tables, ParamField, and AccordionGroup
The skills doc was 409 lines of nested bullet lists describing
precedence, allowlist rules, gating fields, installer specs, and
config overrides. Heavy reference content but no Mintlify structure.

Restructure for scan-first reading without losing reference detail:

- Convert 'Locations and precedence' from a numbered list + arrow
  string into a 6-row precedence table.
- Convert 'Per-agent vs shared skills' bullet/paragraph mix into a
  scope/path/visibility table.
- Move agent-allowlist rules into an AccordionGroup so the example
  config is the headline and the rules collapse on demand.
- Convert ClawHub install/update/sync bullets into a 3-row command
  table.
- Convert SKILL.md frontmatter optional keys (homepage, user-invocable,
  disable-model-invocation, command-dispatch, command-tool,
  command-arg-mode) into ParamField definitions.
- Convert metadata.openclaw fields (always, emoji, homepage, os,
  requires.bins, requires.anyBins, requires.env, requires.config,
  primaryEnv, install) into ParamField definitions.
- Move installer-selection rules and per-installer details (Go, download)
  into an AccordionGroup so the gating section reads as the canonical
  schema plus collapsible operational notes.
- Convert skills.entries config-override rules (enabled, apiKey, env,
  config, allowBundled) into ParamField definitions.
- Surface security caveats as a Warning callout up top instead of a
  bullet list.
- Move 'Looking for more skills?' into the trailing Related list and
  drop the dangling --- separator.
- Sentence-case headings (Format, Gating, Config overrides, Token
  impact) and the Related entries (Creating skills, Skills config,
  Slash commands).
- Drop the redundant 'Skill Workshop' Title-Case heading variant.
- Add sidebarTitle 'Skills' for explicit nav.

Skill source paths, frontmatter parser rules, gating semantics,
installer selection logic, sandboxing notes, env-injection scope,
snapshot/refresh behaviour, remote-node behaviour, and token-impact
formula are unchanged. Pure restructure plus Mintlify components.
2026-04-25 22:45:35 -07:00
Vincent Koc
d54d2d6b9b docs(voice-call): rewrite around Steps Tabs and provider Tabs
The voice-call plugin doc was 664 lines with a flat install/setup
walkthrough, three flat 'Realtime' / 'Streaming' / 'TTS' provider
config blocks each shown twice, an italicised webhook-security
section in Title Case, and a duplicate-Voice Call body H1.

Restructure for scan-first reading without losing operational detail:

- Wrap Quick start in a Steps component (install -> configure ->
  verify -> smoke), with the 'install from npm' vs 'install from
  local folder' choice as a nested Tabs.
- Surface the public-webhook-URL constraint as a Warning at the top
  of Quick start so readers see it before they hit setup.
- Move provider exposure caveats, streaming connection caps, and
  legacy config migration notes into a single AccordionGroup so
  the Configuration section reads as the canonical config plus
  collapsible operational details.
- Convert the Realtime, Streaming, and TTS provider examples to
  Tabs with one tab per provider (Google/OpenAI for realtime;
  OpenAI/xAI for streaming; Core/ElevenLabs/OpenAI override for TTS),
  removing the previous duplicate-block-per-provider pattern.
- Convert the realtime tool-policy bullet list to a 3-row table.
- Convert the agent tool action list and gateway RPC list into
  small tables (action -> args).
- Surface inboundPolicy caller-ID weakness, microsoft-not-supported
  for telephony, and realtime+streaming exclusivity as Warning
  callouts where they were previously buried inline.
- Sentence-case 'Webhook security' (was Title Case), drop the
  duplicate body H1, and refresh the Related list to alphabetical
  sentence-case.

Provider names, env vars, defaults, models, voice ids, command
flags, and field semantics are unchanged. Pure restructure plus
Mintlify component upgrades.
2026-04-25 22:42:47 -07:00
VACInc
78c7292c95 fix: keep telegram tool progress without preview (#71825) (thanks @VACInc)
* fix(telegram): keep default tool progress without preview

* fix: keep telegram tool progress without preview (#71825) (thanks @VACInc)

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-26 11:11:18 +05:30
hcl
c5c40b22af fix(codex): translate minimal thinking for modern models
Fixes #71946
2026-04-25 22:40:53 -07:00
Peter Steinberger
036b422fc6 fix(installer): load nvm before node detection 2026-04-26 06:39:26 +01:00
Peter Steinberger
cbf9c60f1d fix(installer): handle macos gum node failures 2026-04-26 06:37:33 +01:00
Peter Steinberger
be8a3617d9 fix: verify updated gateway version after package restart 2026-04-26 06:37:26 +01:00
Peter Steinberger
142577d9b2 fix(browser): recover stale remote CDP reads safely 2026-04-26 06:35:53 +01:00
Peter Steinberger
eca9f46824 fix: honor node systemd unit activation 2026-04-26 06:35:01 +01:00
Peter Steinberger
33b6962273 fix(installer): fail fast on missing Homebrew Node 2026-04-26 06:33:08 +01:00
Peter Steinberger
257e767e5b fix(telegram): include native quote excerpts for replies 2026-04-26 06:32:46 +01:00
Vincent Koc
639cd50261 fix(models): preserve provider index catalog fallback (#71985)
* fix(models): preserve provider index catalog fallback

* fix(models): mark provider index rows as previews
2026-04-25 22:31:52 -07:00
Shakker
a57d681db9 fix: keep plugin command status on cold index 2026-04-26 06:28:54 +01:00
Vincent Koc
6e3eeb526f docs(video-generation): rewrite around Steps, ParamField, AZ providers
The video-generation page was 454 lines with a 3-step Quick start
written as flat numbered prose, four separate parameter tables (Required,
Content inputs, Style controls, Advanced), the task lifecycle as a
numbered list, and a Related list mixing alphabetic and recency order.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a Steps component (auth -> default model ->
  ask the agent).
- Convert all four parameter tables into ParamField definitions grouped
  under their existing sub-section headings (Required / Content inputs /
  Style controls / Advanced), so types, defaults, and required flags
  show as visual chips and long descriptions wrap cleanly.
- Convert the task lifecycle from a numbered list to a 4-row table for
  at-a-glance scanning.
- Convert Yes/No checkmarks in both the Supported providers and
  Capability matrix tables to ✓ and em-dash, matching the rest of the
  media docs.
- Convert the bullet list under Actions into a 3-row table.
- Sentence-case Related entries and alphabetize the Related list.
- Add sidebarTitle so the nav reads 'Video generation' explicitly.

Schema fields, defaults, model refs, env vars, capability declarations,
fallback rules, and provider notes are unchanged. AccordionGroup of 14
provider notes was already alphabetized and is preserved verbatim.
2026-04-25 22:27:56 -07:00
Peter Steinberger
503a3aa125 fix: defer bedrock discovery sdk import 2026-04-26 06:27:09 +01:00
Peter Steinberger
9f4b155c47 fix(docker): include patch files in runtime image 2026-04-26 06:26:37 +01:00
Peter Steinberger
0e58654dba fix(agents): silence empty group model turns 2026-04-26 06:25:59 +01:00
Vincent Koc
d531760898 docs(music-generation): rewrite around Steps, Tabs, and provider Accordion
The music-generation page was 291 lines with two side-by-side
'Quick start' subsections (shared provider-backed vs. ComfyUI
workflow), a flat parameter table, two prose paragraphs explaining
async behaviour and task lifecycle, and a 'Provider notes' bullet
list mixed with a separate 'Choosing the right path' section.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a top-level Tabs with two child Steps blocks
  (Shared provider-backed | ComfyUI workflow), so readers pick a path
  first and only see the matching steps.
- Convert the tool parameter list to ParamField definitions with
  type signatures and required flags surfaced visually.
- Convert the four async-behaviour bullets to a labelled bullet list
  and the four-state task lifecycle to a table for at-a-glance
  scanning.
- Change Capability matrix Yes/No values to checkmarks/em-dashes for
  alignment with the rest of the media docs.
- Convert the 'Provider notes' free-form paragraphs into an
  AccordionGroup keyed by provider (ComfyUI / Google Lyria 3 /
  MiniMax), keeping wording faithful.
- Sentence-case Related entries and add sidebarTitle so the nav reads
  'Music generation' explicitly.

Provider rows already alphabetized in the supported providers table
(ComfyUI / Google / MiniMax), kept that order. Wording, model refs,
defaults, env vars, and capability declarations are unchanged.
2026-04-25 22:24:58 -07:00
Peter Steinberger
af8648e00e fix(installer): make apt installs noninteractive 2026-04-26 06:23:41 +01:00
Peter Steinberger
58a31b12f7 fix(agents): keep runtime wakeups out of chat transcript 2026-04-26 06:23:27 +01:00
Vincent Koc
f0ea901a0d docs(image-generation): rewrite around Steps, Tabs, and AZ providers
The image-generation page was 395 lines with a 3-step quick-start
written as plain numbered prose, a sprawling 'OpenAI gpt-image-2'
section that mixed routing/legacy/OpenAI options with five inline
slash-command examples, and provider tables that mixed alphabetic
and recency order.

Restructure for scan-first reading without losing technical content:

- Wrap Quick start in a Steps component (auth -> default model ->
  ask the agent), pulling the Codex OAuth note inline with the model
  step where it belongs and surfacing the LAN/SSRF caveat as a
  Warning callout.
- Alphabetize the Supported providers table (ComfyUI, fal, Google,
  LiteLLM, MiniMax, OpenAI, OpenRouter, Vydra, xAI) and the Provider
  capabilities table (same order across both). Convert the Yes/No
  capability table to checkmarks plus exact counts for readability.
- Replace the long inline OpenAI / OpenRouter / MiniMax / xAI prose
  with a 'Provider deep dives' AccordionGroup so each backend's
  routing, legacy URL handling, and provider-specific knobs collapse
  by default.
- Move the four provider-selection-order notes into a small
  AccordionGroup ('Per-call overrides are exact', 'Auto-detection is
  auth-aware', 'Timeouts', 'Inspect at runtime').
- Collapse the five flat slash-command examples into a single Tabs
  component (4K landscape / transparent PNG / two-square /
  edit-one-ref / edit-multi-ref) with the matching CLI variant inline
  on the transparent-PNG tab.
- Sentence-case the Related list (Tools overview, Configuration
  reference) and drop the redundant generic introductory wording.
- Add sidebarTitle so the nav reads 'Image generation' explicitly.

Wording, schema fields, defaults, model refs, env vars, and the
detailed OpenAI/OpenRouter/Codex routing rules are unchanged.
2026-04-25 22:23:09 -07:00
Vincent Koc
5d3168c343 fix(logging): read config path in bundled runtime 2026-04-25 22:21:15 -07:00
Vincent Koc
d1502c2ba1 docs(media-overview): rewrite around CardGroup, sync/async split, and AZ providers
The media overview was a 91-line page that opened with a redundant
Title-Case body H1 ('# Media Generation and Understanding'), then
mixed a capability table, a Yes/Yes/Yes provider matrix, dense prose
about async behaviour and STT/Voice Call surfaces, plus duplicate
'Quick links' and 'Related' sections at the end.

Restructure for scan-first reading without losing any content:

- Drop the redundant body H1; lead with a one-paragraph summary.
- Replace the 'Capabilities at a glance' table with a CardGroup of six
  entry cards (Image / Video / Music / TTS / Media understanding / STT)
  each linking directly to its dedicated page. Mode (sync/async) is
  noted on the card so readers see latency expectations up front.
- Convert the provider matrix to checkmarks for readability and align
  the column header names. Provider rows already alphabetized.
- Pull async vs synchronous behaviour into a 5-row table that names
  why each capability is sync or async, then keep the operator-facing
  paragraph that explains task-id handoff.
- Move the long 'Google maps to ... OpenAI maps to ... xAI maps to ...'
  paragraph into a per-vendor AccordionGroup so each mapping is a
  collapsible panel instead of one large prose block.
- Drop duplicate 'Quick links' section in favour of a single Related
  list, sentence-cased to match the rest of the docs.
2026-04-25 22:20:35 -07:00
Peter Steinberger
eb5bb67e04 style(macos): satisfy swiftformat 2026-04-26 06:19:35 +01:00
Peter Steinberger
113794f277 fix(voicewake): harden trigger routing rebase 2026-04-26 06:19:35 +01:00
Longbiao CHEN
96988914ff test(gateway): align sessionId voicewake assertion 2026-04-26 06:19:35 +01:00
Longbiao CHEN
dfaa9ee87e fix(gateway): keep explicit agent main sessions stable 2026-04-26 06:19:35 +01:00
Longbiao CHEN
4cc2ffce09 fix(gateway): respect explicit voicewake session targets 2026-04-26 06:19:35 +01:00
Longbiao CHEN
ef7ad8229a fix(voicewake): drop stale sdk collateral 2026-04-26 06:19:35 +01:00
Longbiao CHEN
cbcc1227d3 fix(voicewake): require token boundaries for filler-prefix matches 2026-04-26 06:19:35 +01:00
Longbiao CHEN
e74c079b22 fix(gateway): remove duplicate ws client import 2026-04-26 06:19:35 +01:00
Longbiao CHEN
afe1abc297 feat(voicewake): refresh trigger routing on main 2026-04-26 06:19:35 +01:00
Peter Steinberger
a7382ec563 test: cover older-binary service guard 2026-04-26 06:18:37 +01:00
Vincent Koc
724e92505a docs(tts): add sidebarTitle 'Text to speech (TTS)' for the nav
Default sidebar label fell back to title 'Text-to-speech', which is fine
on the page header but readers scanning the Tools sidebar look for the
acronym 'TTS'. Add a sidebarTitle so Mintlify renders 'Text to speech
(TTS)' in the sidebar while keeping the canonical page title intact.

Sentence case matches the rest of the Tools sidebar group (e.g.
'Image generation', 'Music generation', 'Video generation').
2026-04-25 22:11:31 -07:00
Peter Steinberger
15ea0e1f83 fix(gateway): reserve health probes before route stages 2026-04-26 06:10:49 +01:00
Rubén Cuevas
f9146cabfc fix(telegram): preserve native quote replies
Preserve exact Telegram selected quote text for native quote replies, share Telegram reply parameter construction between bot delivery and direct outbound sends, and retry with legacy replies when Telegram rejects native quote parameters.\n\nThanks @rubencu.
2026-04-26 06:09:43 +01:00
Peter Steinberger
edc3504c77 fix(gateway): accept fnm default path on Linux 2026-04-26 06:09:02 +01:00
Peter Steinberger
8c35e45c00 fix: guard gateway mutations from older binaries 2026-04-26 06:07:55 +01:00
Vincent Koc
fbd6b3ce3c docs(tts): A-Z order providers and add tools/tts to Tools nav group
- docs/tools/tts.md: alphabetize providers in three places that listed
  them: the supported-providers table (Azure Speech ... Xiaomi MiMo),
  the configuration Tabs (12 provider presets in A-Z), and the field
  reference AccordionGroup. Top-level fields stay first; provider
  tabs/accordions follow strict alphabetical order. Wording, schema,
  and defaults unchanged.
- docs/docs.json: add tools/tts to the main Tools sidebar group
  (slotted between trajectory and video-generation, matching the
  alphabetical neighborhood with image-generation, music-generation,
  video-generation). Previously tts only appeared under
  Nodes > Media capabilities, which was a discoverability gap for
  readers looking for TTS alongside the other generation tools.
2026-04-25 22:05:46 -07:00
Vincent Koc
71b79f49ad docs(tts): rewrite tts.md around personas with Mintlify components
The TTS doc had grown to 1008 lines with 11 separate flat 'X primary'
config blocks, a 100-line dense 'Notes on fields' bullet list, and
the new provider-personas feature (#70748) buried near the bottom.
Restructure for readability and feature visibility:

- Lead with a Steps-based 'Quick start' so first-time readers can
  enable TTS in 4 explicit steps.
- Replace the 13-bullet provider list with a single 'Supported
  providers' table that names auth env vars and per-provider notes
  inline. Add a Warning callout for the Microsoft/edge legacy alias.
- Collapse the 11 'X primary' config blocks into one Tabs component
  ('OpenAI + ElevenLabs', 'Google Gemini', 'Azure Speech',
  'Microsoft (no key)', 'MiniMax', 'Inworld', 'xAI', 'Volcengine',
  'Xiaomi MiMo', 'OpenRouter', 'Gradium', 'Local CLI') so users see
  one preset at a time and the page is scannable.
- Promote 'Personas' to its own top-level section with two examples
  (minimal and the Alfred provider-neutral persona), and add a new
  'How providers use persona prompts' AccordionGroup covering Google
  (promptTemplate audio-profile-v1, personaPrompt), OpenAI
  (instructions auto-mapping), and Other providers, plus a fallback
  policy table.
- Note that agents.list[].tts.persona overrides global persona
  per-agent (covers the recent feat(tts) per-agent voice-override
  work).
- Convert the 100-line 'Notes on fields' wall into a per-provider
  AccordionGroup using ParamField, so the field reference is
  scannable and field types/defaults are visually distinct.
- Sentence-case headings, drop redundant body H1, fold the flow
  diagram inline with Auto-TTS behavior, and refresh the Output
  formats section to a table-first layout.
- Schema fields (label/description/provider/fallbackPolicy/prompt
  with profile/scene/sampleContext/style/accent/pacing/constraints
  and providers map) verified against src/config/types.tts.ts; all
  defaults and env-var fallbacks preserved verbatim.

Net diff: 585 insertions, 684 deletions across the same surface
area.
2026-04-25 22:00:19 -07:00
Peter Steinberger
73e2151107 fix: fail updates on activated plugin load errors 2026-04-26 05:57:31 +01:00
Peter Steinberger
ad5c00b8e0 docs: expand bonjour disable troubleshooting 2026-04-26 05:56:25 +01:00
Peter Steinberger
d1a5ea2024 fix(docker): disable bonjour by default for compose 2026-04-26 05:51:05 +01:00
Vincent Koc
4cba24a4c3 fix(logging): redact console and file sinks 2026-04-25 21:50:00 -07:00
Vincent Koc
1a8f765147 docs(changelog): note scoped plugin metadata reads 2026-04-25 21:49:09 -07:00
Vincent Koc
b7340ec6a9 fix(plugins): scope metadata manifest reads 2026-04-25 21:48:47 -07:00
Shakker
3ea20d1413 fix: harden cold plugin metadata paths 2026-04-26 05:48:10 +01:00
Vincent Koc
9c8245b178 docs(agents): clarify clean rebase gate reuse 2026-04-25 21:47:55 -07:00
Peter Steinberger
27aedcfd56 style: format repository 2026-04-26 05:47:12 +01:00
Peter Steinberger
6a67f65568 fix(voice): reuse preflight transcripts across channels 2026-04-26 05:42:04 +01:00
Vincent Koc
46b9044c3f docs: update model input modalities and OTEL token-metric attrs
Two recent commits added user-facing surface that left signature-style
references in docs stale:

- 4428661779 Alvin Tang (#20721, thanks @alvinttang) extends the
  configured model 'input' modality set to also accept 'audio' and
  'video', matching what providers like LM Studio already report.
  docs/plugins/manifest.md model-fields table listed only
  'text | image | document', so add 'audio' and 'video'.
- 44da034516 Vincent (thanks @oc-factus) adds a bounded openclaw.agent
  attribute on the openclaw.tokens counter so per-agent dashboards can
  group usage. docs/gateway/opentelemetry.md metric reference omitted
  it; add it to the attrs list.
2026-04-25 21:39:44 -07:00
Peter Steinberger
9b93b7df62 fix(whatsapp): remove ack reactions after replies 2026-04-26 05:36:14 +01:00
Peter Steinberger
427e485f76 fix(update): verify restarted gateway version 2026-04-26 05:35:45 +01:00
Peter Steinberger
6893e8f5f4 test(gateway): stabilize agent wait lifecycle test 2026-04-26 05:34:33 +01:00
Peter Steinberger
5f2273e81e fix(gateway): unify chat display projection 2026-04-26 05:33:58 +01:00
Neerav Makwana
dc9ce2a1bf fix: honor agent for models auth writes (#71933)
Honor the parent `models auth --agent <id>` flag across auth write commands: `add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.

The auth helpers now resolve the requested configured agent before choosing the auth-profile store and provider workspace, while preserving default-agent behavior when `--agent` is omitted.

Validation:
- `pnpm test src/cli/models-cli.test.ts src/commands/models/auth.test.ts`
- `pnpm test src/commands/models/auth.test.ts`
- `pnpm docs:check-mdx`
- `pnpm check:changed`
- `pnpm check`
- `pnpm build`
- `pnpm test src/cli/run-main.test.ts`

Full `pnpm test` was also run; it failed in unrelated `src/cli/run-main.test.ts` assertions during the full-suite order, while the exact file passes on both latest main and this branch. The PR diff only touches models auth CLI/auth files, docs, and changelog.

Fixes #71864.

Thanks @neeravmakwana.
2026-04-26 05:30:47 +01:00
Vincent Koc
1252da325f fix(tests): remove duplicate config mock key 2026-04-25 21:27:39 -07:00
Peter Steinberger
ae45eebef1 fix: route remote mac browser through node host 2026-04-26 05:25:59 +01:00
Peter Steinberger
b8aef04ccd docs(config): refresh config baseline hash 2026-04-26 05:20:45 +01:00
Alvin Tang
4428661779 fix(config): accept video and audio model inputs
Preserve configured audio/video model input modalities through provider catalog normalization.\n\nFixes #20721.\nThanks @alvinttang.
2026-04-26 05:18:54 +01:00
Peter Steinberger
f1eef47839 fix(agents): treat empty group replies as silent 2026-04-26 05:17:02 +01:00
Shakker
c953e98c59 docs: clarify provider index foundation scope 2026-04-26 05:14:51 +01:00
Shakker
89f368e2f9 test: exercise unsafe provider index keys 2026-04-26 05:14:51 +01:00
Shakker
e827778129 fix: keep provider index previews authoritative 2026-04-26 05:14:51 +01:00
Shakker
911172e1e6 fix: avoid provider index preview row spread 2026-04-26 05:14:51 +01:00
Shakker
f1e28370c4 docs: explain provider index authority 2026-04-26 05:14:51 +01:00
Shakker
96ac51d23d feat: add model catalog provider index contract 2026-04-26 05:14:51 +01:00
Peter Steinberger
ac0fa474f8 fix(gateway): tolerate void agent command results 2026-04-26 05:14:36 +01:00
Eulices
008e4ca81f fix: add placeholder transcript for silent voice notes (#49131)
* fix: add placeholder transcript for silent voice notes

* fix: handle placeholder transcripts per skipped attachment

* fix: preserve synthetic transcript attachment order

* fix: scope synthetic audio merge to audio slice only, preserve cross-capability and prefer ordering

Replace the global outputs.sort() with a targeted merge that:
1. Only sorts within the audio output slice (real + synthetic),
   preserving CAPABILITY_ORDER and per-capability attachments.prefer
   ordering for non-audio outputs.
2. Excludes synthetic placeholder indexes from audioAttachmentIndexes
   used by extractFileBlocks, so tiny audio-MIME files with text
   extensions can still be recovered via forcedTextMime.

Adds mergeAudioOutputsPreservingAttachmentOrder helper.

* fix: remove unused function and use toSorted() for oxlint compliance

* fix(media-understanding): preserve selected audio order for synthetic placeholders

- merge synthetic skipped-audio placeholders using audio decision order
  instead of raw attachmentIndex sorting, preserving attachments.prefer
- insert synthetic-only audio outputs at the audio capability slot
  (before video) when no real audio outputs were produced

* fix(media-understanding): use neutral too-small placeholder text

Clarify that this synthetic transcript path is triggered by attachment size,
not by a silence/no-speech detection result.

* test(media-understanding): update too-small audio placeholder expectations

* test(media-understanding): cover mixed too-small audio placeholder

* test(media-understanding): cover too-small audio context

* fix(tasks): preserve visible task title before internal context

* Revert "fix(tasks): preserve visible task title before internal context"

This reverts commit dc536fb4d3c8a01168de5d05e8562193dd68a88e.

---------

Co-authored-by: Eulices Lopez <eulices@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-26 05:14:01 +01:00
Ayaan Zaidi
bcc9fc4cf5 docs: move TTS persona changelog entry 2026-04-26 09:42:38 +05:30
Ayaan Zaidi
cc2044633c fix(tts): compose personas with agent config 2026-04-26 09:42:38 +05:30
Barron Roth
f801fe7d27 test: update provider HTTP mocks 2026-04-26 09:42:38 +05:30
Barron Roth
9975de89d1 test: fix PR CI shards 2026-04-26 09:42:38 +05:30
Barron Roth
f7c837b374 TTS: remove persona rewrite placeholder 2026-04-26 09:42:38 +05:30
Barron Roth
0594fa3c4d TTS: add provider personas 2026-04-26 09:42:38 +05:30
Peter Steinberger
80219ed1b3 fix(channels): tolerate sparse channel metadata during validation 2026-04-26 05:06:52 +01:00
likewen-tech
86328585fa fix(tasks): terminalize gateway agent run ledger
Terminalize Gateway-backed async task records from the run result while preserving aborted, failed, cancelled, and lost outcomes.\n\nThanks @likewen-tech.
2026-04-26 05:06:33 +01:00
Ayaan Zaidi
f9c8a5107c fix(cli): cap fresh session reseed size 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
8559a84e4e fix(cli): bound fresh session reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
12e4841d96 fix(cli): preserve prompt hooks in history reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
0ba28c0911 docs(changelog): note CLI compaction fix 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
3eff589ac0 test(cli): cover transcript compaction reseed 2026-04-26 09:34:24 +05:30
Ayaan Zaidi
dfd5940c34 fix(cli): compact persisted CLI transcripts 2026-04-26 09:34:24 +05:30
Peter Steinberger
b277eac656 fix: pin macos ssh remote url to loopback 2026-04-26 05:01:25 +01:00
Peter Steinberger
9ed11d6c49 fix: steer agents to safe gateway config flow 2026-04-26 05:00:17 +01:00
Vincent Koc
44da034516 fix(otel): add agent label to token metrics 2026-04-25 20:57:47 -07:00
Shakker
d251932fcf refactor: keep manifest contracts on plugin index 2026-04-26 04:55:12 +01:00
Vincent Koc
948c32dd33 fix(channels): lazy-load setup fallback runtime 2026-04-25 20:53:44 -07:00
Ayaan Zaidi
acd3d2b197 fix: reflect Claude CLI auth status (#71332) (thanks @neeravmakwana) 2026-04-26 09:21:06 +05:30
Ayaan Zaidi
76dc66f5fa fix(models): use synthetic auth expiry for status 2026-04-26 09:21:06 +05:30
Neerav Makwana
ad27e0069d fix(models): avoid externalizing Claude CLI auth 2026-04-26 09:21:06 +05:30
Neerav Makwana
911fcb47f1 fix(models): reflect Claude CLI auth status 2026-04-26 09:21:06 +05:30
Peter Steinberger
c9e7bfd1fc fix(mac): implement swift-log event handler 2026-04-26 04:48:43 +01:00
Peter Steinberger
29741f696a fix(feishu): transcribe inbound voice notes 2026-04-26 04:47:45 +01:00
Peter Steinberger
38e61e0046 test(plugins): drop duplicate bundle command mock 2026-04-26 04:46:05 +01:00
Peter Steinberger
540c70d166 fix(plugins): ignore bundled load path aliases 2026-04-26 04:46:05 +01:00
Shakker
42f87c07e9 docs: add model list catalog changelog 2026-04-26 04:41:51 +01:00
Shakker
26a647d4bb docs: scope manifest model list note 2026-04-26 04:41:51 +01:00
Shakker
0f27f2b351 feat: use indexed manifests for static model catalog rows 2026-04-26 04:41:51 +01:00
Shakker
469bd5f51e docs: mention manifest model list rows 2026-04-26 04:41:51 +01:00
Shakker
4a195b37d5 feat: declare deepseek manifest model catalog 2026-04-26 04:41:51 +01:00
Shakker
8749f1deb4 feat: declare moonshot manifest model catalog 2026-04-26 04:41:51 +01:00
Shakker
35171f4e47 feat: use manifest catalog rows for provider list fast path 2026-04-26 04:41:51 +01:00
Shakker
82a529aaaf feat: carry manifest catalog discovery mode 2026-04-26 04:41:51 +01:00
Peter Steinberger
9e4a0e7f3c fix(qqbot): ignore bot self-echo events 2026-04-26 04:40:53 +01:00
Peter Steinberger
e40094a9ef test(browser): add CDP snapshot Docker smoke 2026-04-26 04:40:26 +01:00
Peter Steinberger
4edf22f63f fix(acpx): avoid startup agent probes by default 2026-04-26 04:40:26 +01:00
Peter Steinberger
ed1ac2fc44 feat(browser): add CDP role snapshot fallback 2026-04-26 04:40:26 +01:00
Peter Steinberger
0ca9c4dcb0 fix(cli): preserve lazy placeholder options 2026-04-26 04:40:26 +01:00
Peter Steinberger
e74f2e1501 test: remove duplicate plugin enable mock 2026-04-26 04:39:54 +01:00
Shakker
2d68fda31f fix: defer onboarding install record commits 2026-04-26 04:39:12 +01:00
Shakker
34bd66d929 fix: keep read-only channels off setup runtime 2026-04-26 04:39:12 +01:00
Shakker
2e7635f4f9 fix: scope web provider ownership to plugin index 2026-04-26 04:39:12 +01:00
Peter Steinberger
6d4f65c9d4 docs: clarify codex runtime routing 2026-04-26 04:38:39 +01:00
Peter Steinberger
6336ed4166 fix: gate codex acp route hints 2026-04-26 04:36:26 +01:00
Peter Steinberger
b58223510c fix(providers): support zai preserved thinking 2026-04-26 04:35:50 +01:00
Peter Steinberger
844d2bd515 test: mock browser cleanup in heartbeat session tests 2026-04-26 04:33:37 +01:00
Colin Johnson
21082d2ede fix(plugins): verify bundled runtime deps installs
Verify bundled runtime dependency installs before reporting success, so a clean npm exit cannot hide packages missing from the managed runtime-deps root.

Also updates the bundle command test mock for the current plugin enable-state API.

Local proof:
- `pnpm test src/plugins/bundle-commands.test.ts`
- `pnpm test src/plugins/bundled-runtime-deps.test.ts src/commands/doctor-bundled-plugin-runtime-deps.test.ts src/plugins/loader.test.ts`
- `pnpm check:changed`

Co-authored-by: Colin <colin@solvely.net>
2026-04-26 04:32:33 +01:00
Peter Steinberger
96d90091c4 test: align task title sanitization expectation 2026-04-26 04:30:13 +01:00
Peter Steinberger
2c8c79de5c fix(tts): normalize streamed tts voice media 2026-04-26 04:28:19 +01:00
Peter Steinberger
f4e6322649 test: align runtime schema registry mock count 2026-04-26 04:25:41 +01:00
Peter Steinberger
924e132d96 test: add manifests to npm install fixtures 2026-04-26 04:22:28 +01:00
Pinghuachiu
7b943667a0 fix: expose image edit geometry flags in capability cli
Expose image edit geometry flags in the capability CLI and document the new infer options.\n\nThanks @Pinghuachiu.
2026-04-26 04:22:22 +01:00
Peter Steinberger
ee8f41f56e fix(channels): strip copied inbound metadata from replies 2026-04-26 04:21:20 +01:00
Vincent Koc
7fef13abbc docs(anthropic): note context1m param applies to Claude CLI backend
Ayaan's 28e4cd81a9 (#70863, thanks @bidadh, source from Arthur Kazemi
8abbae0101) extended params.context1m:true so the configured 1M
context window override now applies to eligible Claude CLI Opus and
Sonnet models, not only direct API calls. CHANGELOG entry covered
the change but docs/providers/anthropic.md '1M context window (beta)'
Accordion only described direct-API behavior, so Claude CLI users had
no signal the same param works for their backend. Add a sentence
inside the same Accordion.
2026-04-25 20:18:51 -07:00
Peter Steinberger
b3ac316e0b fix: preserve indexed plugin diagnostics 2026-04-26 04:17:15 +01:00
Shakker
862b39976d fix: remove managed plugin files on uninstall 2026-04-26 04:16:33 +01:00
Shakker
48ba3a4198 fix: clean migrated plugin install config 2026-04-26 04:16:33 +01:00
Shakker
f5f4477bae fix: reject manifestless plugin archives 2026-04-26 04:16:33 +01:00
Ayaan Zaidi
28e4cd81a9 fix: enable claude cli context1m window (#70863) (thanks @bidadh) 2026-04-26 08:45:59 +05:30
Ayaan Zaidi
64630e1c39 test: fix claude cli context1m fixture 2026-04-26 08:45:59 +05:30
Arthur Kazemi
8abbae0101 fix: enable claude-cli 1m context override 2026-04-26 08:45:59 +05:30
Arthur Kazemi
bb389a37d0 test: cover claude-cli context1m context-window behavior 2026-04-26 08:45:59 +05:30
Peter Steinberger
a91baa16de fix(tts): honor explicit directive providers 2026-04-26 04:14:48 +01:00
Peter Steinberger
969a3757b9 test: harden plugin registry mocks 2026-04-26 04:10:48 +01:00
Peter Steinberger
cf834e2a21 fix(tts): clean streamed directive text 2026-04-26 04:09:56 +01:00
Vincent Koc
2261918c8c fix(plugins): resolve activation plans from plugin registry 2026-04-25 20:06:06 -07:00
Peter Steinberger
6df120fb39 fix: keep internal completion wakes out of chat memory 2026-04-26 04:01:45 +01:00
Shakker
d0d93d0fde test: harden bundle index reconstruction 2026-04-26 03:58:29 +01:00
Peter Steinberger
8748ae3bb7 fix(skills): parse remote which bin maps 2026-04-26 03:58:22 +01:00
Peter Steinberger
18a638ceae test: isolate cold plugin registry metadata mocks 2026-04-26 03:57:38 +01:00
Ayaan Zaidi
a8b4be0b48 fix: unwrap nested Claude CLI results (#66819) (thanks @mraleko) 2026-04-26 08:27:33 +05:30
Ayaan Zaidi
1c77515396 fix(agents): scope Claude JSON unwrapping 2026-04-26 08:27:33 +05:30
Alec Hrdina
1b41513b3b agents/cli: scope nested result unwrapping 2026-04-26 08:27:33 +05:30
Alec Hrdina
015e39e3cf agents/cli: unwrap nested claude result json 2026-04-26 08:27:33 +05:30
Peter Steinberger
c3833f7729 fix(providers): satisfy vllm chat kwargs lint 2026-04-26 03:54:20 +01:00
Peter Steinberger
ed5276f9b9 fix(providers): keep vllm nemotron replies visible 2026-04-26 03:54:20 +01:00
Peter Steinberger
7a85c1a822 fix(tts): surface voice status and harden providers 2026-04-26 03:51:30 +01:00
Peter Steinberger
1231f21679 fix(plugins): tolerate partial manifest registry records
# Conflicts:
#	src/plugins/installed-plugin-index.ts
2026-04-26 03:50:40 +01:00
Peter Steinberger
f5812aa64d test: complete plugin install manifest fixtures 2026-04-26 03:50:40 +01:00
Peter Steinberger
0cf30b6a65 docs(tasks): document retained lost task audit 2026-04-26 03:50:40 +01:00
Likewen
de5b173546 fix(tasks): normalize task timestamps and retained lost audit
Normalize task lifecycle timestamps on create, update, and restore so startedAt/lastEventAt/endedAt cannot precede createdAt in audit-visible records.

Downgrade retained lost tasks with future cleanupAfter from audit errors to warnings while keeping expired or unstamped lost tasks as errors.

Verification: pnpm exec oxfmt --write --threads=1 src/tasks/task-registry.ts src/tasks/task-registry.test.ts src/tasks/task-registry.audit.ts src/tasks/task-registry.audit.test.ts

Verification: node scripts/test-projects.mjs src/tasks/task-registry.test.ts src/tasks/task-registry.audit.test.ts (task-registry.audit.test.ts 4 passed; task-registry.test.ts 45 passed)
2026-04-26 03:50:40 +01:00
Shakker
d955bf0ff8 fix: scope cold plugin manifests to index 2026-04-26 03:47:46 +01:00
Shakker
1a193b2d96 fix: scope cold plugin manifests to index 2026-04-26 03:47:45 +01:00
Peter Steinberger
f8a677bcfd docs: reduce changelog rebase churn 2026-04-26 03:46:31 +01:00
Peter Steinberger
0ddbae171d test: cover codex app-server subagents 2026-04-26 03:46:30 +01:00
Vincent Koc
c149de7750 fix(plugins): resolve runtime metadata fallbacks cold 2026-04-25 19:45:59 -07:00
Peter Steinberger
07877d71cd fix(control-ui): preserve optimistic messages on empty history 2026-04-26 03:45:07 +01:00
Peter Steinberger
97ae1c7c2e feat(tts): add read-latest voice command 2026-04-26 03:44:44 +01:00
Vincent Koc
2235a13dab fix(plugins): resolve capability metadata cold 2026-04-25 19:43:07 -07:00
Peter Steinberger
3989510251 docs: expand ACP agents guide 2026-04-26 03:42:44 +01:00
Peter Steinberger
e23d17da79 test: update full-suite planner expectation 2026-04-26 03:39:52 +01:00
Vincent Koc
d8ed49f651 fix(plugins): keep config alias normalization cold 2026-04-25 19:39:41 -07:00
Peter Steinberger
f0fa35082b fix: keep ACP completion prompts harness-safe 2026-04-26 03:39:24 +01:00
Vincent Koc
4fbc490fca fix(agents): resolve provider attribution metadata cold 2026-04-25 19:36:28 -07:00
Ayaan Zaidi
23fbdc1ec2 fix: allow large Claude live JSONL lines (#71897) 2026-04-26 08:06:06 +05:30
Ayaan Zaidi
09e60e496b fix(agents): allow large Claude live JSONL lines 2026-04-26 08:06:06 +05:30
Ayaan Zaidi
78e0976f93 test(agents): cover large Claude live JSONL output 2026-04-26 08:06:06 +05:30
Peter Steinberger
802a73a382 test: isolate legacy auth choice aliases 2026-04-26 03:34:11 +01:00
Vincent Koc
10763781fd fix(config): resolve plugin contracts cold 2026-04-25 19:33:56 -07:00
Vincent Koc
a0ca546997 test(qa): add local otel smoke harness 2026-04-25 19:30:46 -07:00
Vincent Koc
476bb38527 fix(setup): plan setup metadata from plugin registry 2026-04-25 19:30:20 -07:00
Peter Steinberger
72d8600eb5 test: fix plugin skills mock typing 2026-04-26 03:28:59 +01:00
Peter Steinberger
6855b33255 docs(tts): clarify WhatsApp voice-note delivery 2026-04-26 03:28:51 +01:00
Vincent Koc
bc24b547d0 fix(agents): resolve plugin skill metadata cold 2026-04-25 19:26:21 -07:00
Peter Steinberger
0796a888ae fix: tolerate partial plugin registry records 2026-04-26 03:26:11 +01:00
Peter Steinberger
9b91040053 fix(tts): route WhatsApp MP3 TTS as voice notes 2026-04-26 03:26:00 +01:00
Peter Steinberger
90cd9fce85 fix(agents): handle empty Claude stop turns 2026-04-26 03:23:16 +01:00
Vincent Koc
a44a3f9171 fix(auth): resolve plugin auth metadata cold 2026-04-25 19:22:31 -07:00
Ayaan Zaidi
bbd9702077 ci: fix Docker E2E image build 2026-04-26 07:49:04 +05:30
Vincent Koc
6afac5208a fix(secrets): resolve plugin env metadata cold 2026-04-25 19:18:30 -07:00
Peter Steinberger
c14d2b0c1f fix: harden plugin registry contribution lookup 2026-04-26 03:17:31 +01:00
Peter Steinberger
2d9a0d9cf0 fix: preserve image history while pruning replay context 2026-04-26 03:16:00 +01:00
Peter Steinberger
69e7e499b1 docs(tts): document per-agent tts config 2026-04-26 03:13:43 +01:00
Vincent Koc
690046637f fix(web): resolve provider candidates from plugin registry 2026-04-25 19:13:15 -07:00
Peter Steinberger
9b4f0779ce fix(tts): honor per-agent config in tts commands 2026-04-26 03:12:30 +01:00
Vincent Koc
6a688e33f6 docs(agents): require single-line changelog bullets
Add an explicit rule under Docs / Changelog that bullets must stay on
one line — no wrapping or continuation across multiple lines.
Justification: dedupe, PR-ref, and credit-audit tooling assumes
single-line entries, and the rest of the file is already uniform
single-line. The recent flatten pass collapsed 80 multi-line bullets
in last two releases plus Unreleased; this rule prevents that drift
from coming back through new entries.
2026-04-25 19:10:21 -07:00
Peter Steinberger
0e1f53f020 fix: clear system events on session reset 2026-04-26 03:09:15 +01:00
Vincent Koc
d65f28f962 docs(changelog): flatten 80 multi-line bullets in last two releases and unreleased
Many bullets in Unreleased, 2026.4.25 (Unreleased), 2026.4.24, and
2026.4.23 wrapped across two to five lines, mixing single-line and
multi-line entries within the same section. The repo convention is
single-line bullets — that is what every author/PR reformatter aims
at, what the dedupe and PR-ref scanners assume, and what readers
scrolling the file see in the rest of the changelog.

Reflow each multi-line bullet to a single line by joining
continuation lines on a single space and collapsing redundant
whitespace. Wording, casing, links, PR refs, and Thanks credits are
unchanged — the only edit is layout.

Skipped 2026.4.22 and earlier on purpose; this pass is scoped to
'last two releases plus unreleased' as requested.
2026-04-25 19:07:57 -07:00
Vincent Koc
e4199379ff fix(channels): resolve cold channel presence from registry 2026-04-25 19:07:05 -07:00
Peter Steinberger
94316334fe test: use agent wait for OpenAI web search smoke 2026-04-26 03:06:09 +01:00
Peter Steinberger
a6d9926d1d fix: keep acp management commands local 2026-04-26 03:02:04 +01:00
Peter Steinberger
9123c8158d test: stabilize main ci provider tests 2026-04-26 03:00:57 +01:00
Shakker
0f343ad568 fix: make plugin install config migration atomic 2026-04-26 02:56:44 +01:00
Peter Steinberger
04e08cea62 chore(tts): refresh plugin sdk api baseline 2026-04-26 02:54:13 +01:00
Peter Steinberger
0ca952cdd5 feat(tts): add per-agent voice overrides 2026-04-26 02:54:13 +01:00
Peter Steinberger
1bc9bada65 test: speed up security audit tests 2026-04-26 02:51:19 +01:00
Peter Steinberger
ec56dd3116 fix(pairing): preserve corrupt pairing stores 2026-04-26 02:50:19 +01:00
Vincent Koc
5469740170 fix(github): exempt maintainers from barnacle candidate labels 2026-04-25 18:49:44 -07:00
Shakker
105785a1be fix: preserve channel plugin install records 2026-04-26 02:48:29 +01:00
Peter Steinberger
e3be66ddda test: align plugin docker smoke with install ledger 2026-04-26 02:46:04 +01:00
Peter Steinberger
75a8f5863c test: speed up chat UI tests 2026-04-26 02:45:31 +01:00
Vincent Koc
526fd9d545 test: stabilize pty and agentic test lanes 2026-04-25 18:44:18 -07:00
Vincent Koc
d74f897c1c fix(status): keep channel status reads cold 2026-04-25 18:40:38 -07:00
Vincent Koc
839e7c98ff fix(channels): keep read-only plugin listing cold 2026-04-25 18:40:38 -07:00
Vincent Koc
e40157013f fix(diagnostics): complete early model stream closes 2026-04-25 18:40:00 -07:00
Vincent Koc
c7b336d83e docs(hooks): document new ctx.jobId field on plugin hook contexts
Scott Glover's commit 371b69b3e2 ('Expose cron jobId in plugin hook
context') added an optional jobId field on PluginHookAgentContext,
populated for cron-driven runs. The commit shipped without a docs
update or CHANGELOG entry, so plugin authors had no visible signal
that the new ctx.jobId field exists.

Surface ctx.jobId in two existing hook context references in
docs/plugins/hooks.md: the before_tool_call ctx-fields list, and the
runId/agent-lifecycle paragraph that already names ctx.runId — extend
it to note ctx.jobId on cron-driven runs and what plugins can do with
it (scope metrics, side effects, or state to a scheduled job).
2026-04-25 18:38:27 -07:00
Peter Steinberger
8ed52c1463 fix: bound configured acp binding readiness 2026-04-26 02:36:58 +01:00
Shakker
29463b9c47 fix: persist pending onboarding plugin installs 2026-04-26 02:35:04 +01:00
Vincent Koc
2495585a32 feat(diagnostics-otel): add exporter health diagnostics
Adds diagnostics-otel exporter health events and signal-specific endpoint wiring, with docs and config schema coverage.
2026-04-25 18:34:44 -07:00
Shakker
25ecb2895a fix: preserve bundle format in plugin index 2026-04-26 02:31:02 +01:00
Shakker
4e3b860e60 refactor: scope plugin capabilities to manifests 2026-04-26 02:31:02 +01:00
Shivanker Goel
a932a58e87 feat(fal): support Seedance reference video
Adds fal Seedance 2.0 reference-to-video support with model-aware reference input limits.
2026-04-26 02:30:23 +01:00
Peter Steinberger
566d2d73a3 fix: keep system events from extending session resets (#71845) 2026-04-26 02:29:44 +01:00
Peter Steinberger
1cce439c9c fix(ui): hide chat skeleton during reload 2026-04-26 02:27:04 +01:00
pashpashpash
e989f3c868 Respect retryable Codex app-server errors
Codex app-server sends retryable stream error notifications while a turn is still recovering. OpenClaw now ignores retryable app-server errors and preserves nested terminal error messages instead of replacing them with a generic fallback.
2026-04-25 18:26:27 -07:00
91wan
a35d259719 fix(acpx): isolate Codex ACP config from desktop hooks
Isolate Codex ACP launches with an OpenClaw-managed CODEX_HOME/config wrapper so global Codex desktop notify hooks do not leak into acpx sessions.\n\nValidation:\n- OPENCLAW_LOCAL_CHECK=0 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm check:changed\n- pnpm test extensions/acpx/src/codex-auth-bridge.test.ts\n\nThanks @91wan.
2026-04-26 02:23:55 +01:00
Vincent Koc
8c3b1366ce chore(tokenjuice): bump bundled runtime to 0.6.3 2026-04-25 18:22:44 -07:00
Peter Steinberger
d513dc7146 fix: bootstrap gateway env proxy dispatcher
Co-authored-by: mjamiv <74088820+mjamiv@users.noreply.github.com>
2026-04-26 02:22:21 +01:00
Peter Steinberger
c43ce254e1 fix: wait for acp backend before startup reconcile 2026-04-26 02:21:16 +01:00
Peter Steinberger
00d2fbfda4 test(cron): cover delivery context edge cases 2026-04-26 02:20:39 +01:00
Peter Steinberger
e309fd485e fix(cron): preserve current delivery target context 2026-04-26 02:11:00 +01:00
Peter Steinberger
0731fc1942 test: keep packaged provider setup discoverable 2026-04-26 02:07:08 +01:00
Scott Glover
371b69b3e2 Expose cron jobId in plugin hook context 2026-04-26 02:06:48 +01:00
Peter Steinberger
264d6f6aef docs: note GitHub search boolean fallback 2026-04-26 02:02:35 +01:00
Shakker
921ffad7c7 fix: commit pending plugin install records in config flows 2026-04-26 01:59:48 +01:00
Peter Steinberger
87142b5fb1 test: narrow live Docker package script changes 2026-04-26 01:59:12 +01:00
Peter Steinberger
57f05128cb docs: clarify Crestodian rescue audit metadata 2026-04-26 01:56:59 +01:00
pashpashpash
5404bbbb71 Avoid duplicate generated media attachments
Generated media can be produced in intermediate tool results before the assistant chooses which assets to share in its final reply. This change keeps those intermediate files from being appended a second time when the final reply already names the assets to deliver, and tightens the media directive parsing around unsafe or ambiguous URLs.
2026-04-25 17:56:29 -07:00
Peter Steinberger
099d18f432 test: narrow live Docker ACP changed gate 2026-04-26 01:48:33 +01:00
Peter Steinberger
1fe0e6fc4a fix(status): clarify tailscale exposure state 2026-04-26 01:47:03 +01:00
Vincent Koc
2f6615d2ee fix(triage): extract barnacle workflow 2026-04-25 17:43:08 -07:00
Peter Steinberger
5b80d0c15e feat(tts): add Azure Speech provider
Co-authored-by: Leon Chui <84605354+leonchui@users.noreply.github.com>
2026-04-26 01:42:51 +01:00
Peter Steinberger
753ccf615c fix: preserve LM Studio quant model refs (#71486) 2026-04-26 01:41:08 +01:00
Bartok9
5bb78ea7ed fix(model): preserve LM Studio '@' quant suffixes in model name resolution
stripModelProfileSuffix() in providers.ts naively truncated model names at
the first '@', discarding quant variants like @iq3_xxs, @iq4_xs, @q4_k_xl
that LM Studio uses to distinguish quantization levels.

This caused two user-facing bugs (fixes #71474):
1. /model lmstudio/qwen3.6-27b@iq3_xxs → 'model not allowed: lmstudio/qwen3.6-27b'
2. API requests sent truncated model name → LM Studio picked a random quant

Changes:
- Replace the naive indexOf('@') strip in providers.ts with
  splitTrailingAuthProfile() which already handles quant suffixes
- Extend the quant-suffix regex (q\d+...) to also match importance-
  quantization tags (iq3_xxs, iq4_xs, ...) via i?q\d+ pattern
- Add tests for @iq* quant suffixes and auth-profile-after-iq combos
2026-04-26 01:41:08 +01:00
Shakker
94ceb2bbe9 test: fix doctor symlink cleanup 2026-04-26 01:38:21 +01:00
Shakker
140ac29172 fix: defer onboarding plugin install records 2026-04-26 01:38:21 +01:00
Peter Steinberger
5edfbca6e5 fix(ci): hide configured workspace setup candidates 2026-04-26 01:34:01 +01:00
Peter Steinberger
78cfd2a512 fix: seed gateway control UI origins from runtime bind 2026-04-26 01:33:43 +01:00
Peter Steinberger
81c2a1de26 test: add Droid ACP bind Docker lane 2026-04-26 01:31:27 +01:00
Peter Steinberger
650dc59b6f fix: skip checkpoint transcripts in memory dreaming 2026-04-26 01:30:50 +01:00
Peter Steinberger
b565e6e963 fix(ci): repair plugin registry test lanes 2026-04-26 01:29:47 +01:00
Shakker
e7c131d6de fix: roll back plugin index for update channel writes 2026-04-26 01:25:26 +01:00
Val Alexander
41282fcb13 refactor(control-ui): keep quick settings personal card balanced (#71585) 2026-04-25 19:23:43 -05:00
Peter Steinberger
e6ee4d6e68 fix(browser): preserve tabs across target swaps 2026-04-26 01:21:59 +01:00
Vincent Koc
f3accc753c feat(plugins): add before agent finalize hook (#71765) 2026-04-25 17:21:17 -07:00
Vincent Koc
727e0e013e fix(triage): classify low-signal prs 2026-04-25 17:19:50 -07:00
Shakker
be1d656514 fix: roll back plugin index on config write conflicts 2026-04-26 01:13:29 +01:00
Peter Steinberger
ca0232ff0e fix(bonjour): bound stuck advertiser restarts 2026-04-26 01:11:53 +01:00
Peter Steinberger
3a4325b285 fix: prevent duplicate channel plugin tools 2026-04-26 01:06:11 +01:00
Shakker
6ed642a86d fix: rank plugin duplicates with pending install records 2026-04-26 01:03:13 +01:00
Shakker
569d489383 fix: preserve install records during registry refresh 2026-04-26 01:03:13 +01:00
Shakker
babbad81a9 fix: preserve plugin install records without manifests 2026-04-26 01:03:13 +01:00
Shakker
1848d0dd38 fix: block config writes when plugin install migration fails 2026-04-26 01:03:13 +01:00
Shakker
194c26bcd2 fix: migrate shipped plugin install config records 2026-04-26 01:03:13 +01:00
Shakker
14e2760835 docs: fix plugin reference indentation 2026-04-26 01:03:13 +01:00
Shakker
0a41fc3ef8 test: expect plugin install index records on refresh 2026-04-26 01:03:13 +01:00
Shakker
dcf7f8f44c fix: model plugin index records in cli tests 2026-04-26 01:03:13 +01:00
Shakker
1d141c39a9 docs: update plugin index changelog entries 2026-04-26 01:03:13 +01:00
Shakker
df7348e586 fix: guide config users to plugin commands 2026-04-26 01:03:12 +01:00
Shakker
ebbefd6903 fix: type plugin install config strip test 2026-04-26 01:03:12 +01:00
Shakker
b018272fa1 fix: strip plugin install records before config validation 2026-04-26 01:03:12 +01:00
Shakker
56f4264f1b fix: keep plugin audit check ids stable 2026-04-26 01:03:12 +01:00
Shakker
c79399dc68 fix: preserve plugin index records in update flows 2026-04-26 01:03:12 +01:00
Shakker
9e086d6ed8 refactor: split plugin index record reader 2026-04-26 01:03:12 +01:00
Shakker
57c4279c4a fix: remove unused plugin index import 2026-04-26 01:03:12 +01:00
Shakker
37ce39b5c5 docs: describe plugin install index store 2026-04-26 01:03:12 +01:00
Shakker
d0dafd9dca refactor: remove plugin install config fallback 2026-04-26 01:03:12 +01:00
Shakker
c19f8a5223 refactor: consolidate plugin install index store 2026-04-26 01:03:12 +01:00
Peter Steinberger
f8123e4b68 fix(ci): stabilize media and gateway tests 2026-04-26 01:01:08 +01:00
Peter Steinberger
8e12c24d17 fix: prefer native codex app-server controls 2026-04-26 00:59:02 +01:00
Peter Steinberger
77d04a39d8 fix(feishu): separate synthetic ids from reply targets 2026-04-26 00:57:38 +01:00
Peter Steinberger
e918e5f75c fix: hide runtime context from submitted prompts 2026-04-26 00:57:04 +01:00
pash-openai
edb618c6c4 Manage the Codex app-server binary in OpenClaw (#71808)
* Manage Codex app-server binary

* Use plugin deps for Codex app-server binary

* Stabilize media model registry test

* Exclude checkpoint transcripts from memory ingestion
2026-04-25 16:51:14 -07:00
Vincent Koc
fc334cda13 chore(pr-triage): route low-signal cleanup to ClawHub 2026-04-25 16:49:15 -07:00
Vincent Koc
7741dbb759 docs: split OpenTelemetry export into its own page under gateway
Logging.md had grown to 487 lines with ~300 lines dedicated to
OpenTelemetry export — wire protocol, full metric/span catalog, env
vars, captureContent shape, sampling, the diagnostic event catalog,
and protocol notes — leaving the genuine logging overview buried
behind exporter reference material.

Move the OTEL surface to a dedicated page and slim logging.md to a
focused logs overview:

- Add docs/gateway/opentelemetry.md (OpenTelemetry export). Same
  content reorganized: how it fits together, quick start, signals,
  configuration reference + env vars table, privacy/captureContent,
  sampling/flushing, full metric and span catalog, diagnostic event
  catalog, no-exporter mode, diagnostics flags pointer, disable.
- docs/logging.md: drop the OTEL section in favor of a short
  'Diagnostics and OpenTelemetry' summary that cross-links the new
  page and the diagnostics-flags page. Drops 273 lines net. Also
  drops the redundant body H1, retitles to 'Logging' (was 'Logging
  overview' which mismatched sidebar usage), and refreshes the
  Related list.
- docs/docs.json: insert gateway/opentelemetry into the
  'Health and diagnostics' sidebar group, reorder pages so the user-
  facing health/run pages come before exporter/internals pages, and
  put logging next to opentelemetry where readers naturally
  associate them.
- docs/gateway/diagnostics.md, docs/gateway/logging.md,
  docs/gateway/configuration-reference.md: cross-link the new page
  and sentence-case stale Title-Cased Related entries on
  diagnostics.md.
2026-04-25 16:46:53 -07:00
Peter Steinberger
a1090b6043 fix(feishu): accept v2 card action callbacks 2026-04-26 00:41:16 +01:00
Peter Steinberger
12c16576cd fix: gate acp spawn affordances 2026-04-26 00:30:27 +01:00
Vincent Koc
d228463120 fix(onboarding): refresh plugin registry after plugin installs 2026-04-25 16:27:56 -07:00
Peter Steinberger
435be06cde test(exec): cover control ui approval turn-source resolution 2026-04-26 00:27:01 +01:00
Peter Steinberger
41b27024bb docs(gateway): clarify backend RPC pairing 2026-04-26 00:26:35 +01:00
Vincent Koc
d74b6359fd fix(channels): refresh plugin registry after on-demand installs 2026-04-25 16:22:14 -07:00
MrBrain
28497515fe fix(qqbot): schedule reminders through cron gateway (#70937)
* fix(qqbot): schedule reminders through cron gateway

* fix(qqbot): update reminder cron instruction

* fix(qqbot): schedule reminders directly (#70937) (thanks @GaosCode)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-26 00:15:28 +01:00
Vincent Koc
73cacebac3 fix(plugins): normalize registry migration env flags 2026-04-25 16:12:01 -07:00
Peter Steinberger
c2ea0ce5a9 docs: update changelog for memory fixes 2026-04-26 00:11:35 +01:00
Peter Steinberger
1c6911c01f fix: ignore compaction checkpoints in session usage 2026-04-26 00:11:35 +01:00
Peter Steinberger
956cb1c7db fix: keep local embedding batches from flooding providers 2026-04-26 00:11:35 +01:00
Peter Steinberger
3f90005e56 build: bump bundled acpx to 0.6.1 2026-04-26 00:11:05 +01:00
Peter Steinberger
6b0c72bec8 fix(image): resolve provider-prefixed configured models
Closes #33185
2026-04-26 00:08:27 +01:00
EVA
2c35a6e599 [codex] Consolidate RuntimePlan and Harness V2 package (#71722)
* refactor: centralize runtime plan policy surface

* refactor: route embedded attempts through runtime plan

* feat: add agent harness v2 lifecycle adapter

* docs: document agent harness runtime plan

---------

Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 16:07:04 -07:00
Peter Steinberger
114c9a2f3e test: stabilize cron mcp cleanup docker smoke 2026-04-25 23:51:40 +01:00
Peter Steinberger
76a0abc768 fix(agents): keep queued announces session-only without route 2026-04-25 23:49:06 +01:00
Peter Steinberger
496d90c3b5 ci: split auto-reply shard timing 2026-04-25 23:47:00 +01:00
Rui Xu
1531123d35 feat(tts): add BytePlus Seed Speech provider
Add Volcengine/BytePlus Seed Speech as a bundled TTS provider with current API-key auth, legacy AppID/token fallback, native Ogg/Opus voice-note output, and MP3 audio-file output.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 23:46:04 +01:00
Peter Steinberger
b1b29a8fc2 fix: stabilize remote skill node probes 2026-04-25 23:42:02 +01:00
Peter Steinberger
e4bfc8066e fix(openai): extend Azure image timeout
Closes #71705
2026-04-25 23:34:18 +01:00
Peter Steinberger
e640c0a95f fix(gateway): decouple backend RPC from CLI pairing 2026-04-25 23:23:52 +01:00
zhang-guiping
91adb69c57 fix(image): resolve configured image models 2026-04-25 23:19:28 +01:00
Peter Steinberger
8f78932059 test: harden QA cleanup and update preflight 2026-04-25 23:16:30 +01:00
Peter Steinberger
81a41fe5be test: retry cli backend live cleanup 2026-04-25 23:10:32 +01:00
Peter Steinberger
309f7f1873 test(ui): split chat avatar render coverage 2026-04-25 23:07:44 +01:00
Peter Steinberger
cf303b3101 fix(cli): trim plugin preloads for setup-safe commands 2026-04-25 23:06:05 +01:00
Peter Steinberger
8d08e86f42 fix(gateway): keep diagnostic probes non-mutating 2026-04-25 23:02:39 +01:00
Peter Steinberger
bd796d1c85 docs(plugins): clarify local dependency installs 2026-04-25 22:59:09 +01:00
RoomWithOutRoof
be51c98c5d fix(onboarding): scope video-only provider auth choices 2026-04-25 22:53:00 +01:00
Peter Steinberger
ce364121aa test(ui): split tool card render coverage 2026-04-25 22:49:25 +01:00
Peter Steinberger
f1b1c3dc99 chore: update workspace dependencies 2026-04-25 22:48:44 +01:00
Gustavo Madeira Santana
d5166718bc test(matrix): cover destructive E2EE backup recovery flows (#71311)
Merged via squash.

Prepared head SHA: fd5fc06007
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-25 17:48:18 -04:00
Peter Steinberger
cbe5515b70 fix(plugins): force dependency installs local 2026-04-25 22:46:50 +01:00
Peter Steinberger
1dfa52d071 test(i18n): load registry locale fixtures concurrently 2026-04-25 22:43:17 +01:00
Peter Steinberger
f62a054ef1 perf(ui): split chat role normalization imports 2026-04-25 22:43:16 +01:00
Peter Steinberger
265b97bbba fix(cli): avoid plugin preload for agent bindings 2026-04-25 22:38:30 +01:00
Peter Steinberger
9a2dfe0c7e docs: update changelog for Discord triage fixes 2026-04-25 22:34:43 +01:00
Peter Steinberger
f731e3754c fix: lazy-load Microsoft Edge TTS runtime 2026-04-25 22:34:43 +01:00
Peter Steinberger
ce884a8dae fix: keep agents list JSON config-only 2026-04-25 22:34:43 +01:00
Peter Steinberger
b721f1dbad fix: update Ollama web search endpoint 2026-04-25 22:34:43 +01:00
Cale Shapera
0bcb4c95c1 feat(tts): add Inworld speech provider (#55972)
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification.

Co-authored-by: cshape <cshape@users.noreply.github.com>
2026-04-25 22:33:21 +01:00
Peter Steinberger
167588cb4f test(infra): import diagnostic duplicate from source url 2026-04-25 22:19:09 +01:00
Peter Steinberger
9d22061e3e test(discord): mock message process dependencies narrowly 2026-04-25 22:19:09 +01:00
Peter Steinberger
8a731c1ef7 perf(plugin-sdk): add narrow outbound send deps entry 2026-04-25 22:19:09 +01:00
Aamir Jawaid
969f8bfd9f docs(msteams): add Teams CLI setup instructions (#71747)
* docs(msteams): add Teams CLI setup instructions

Replace manual Azure Bot setup as primary path with
@microsoft/teams.cli workflow. Manual steps collapsed
into <details> blocks for users who can't use the CLI.

* docs(msteams): fix devtunnel instructions to use persistent tunnels

Use devtunnel create + host for stable URLs across sessions
instead of throwaway tunnels that change each time.

* docs(msteams): address PR feedback

- Remove "Abandon all hope" quote (showed as net-addition in diff)
- Add preview disclaimer for @microsoft/teams.cli
- Add security note for --allow-anonymous devtunnel flag
- Clarify where to find teamsAppId from create output
- Link to official devtunnel getting started guide

* docs(msteams): fix oxfmt formatting

* docs(msteams): clarify install step references create prompt

* docs(msteams): drop --env flag, use terminal output instead

Avoids writing secrets to a file that could be accidentally committed.

* docs(msteams): remove redundant H1, match other channel docs
2026-04-25 14:18:09 -07:00
kevinlin-openai
289ed9830a Add TUI context mode selector (#71760)
Co-authored-by: kevinlin-openai <kevin@dendron.so>
Co-authored-by: Codex <noreply@openai.com>
2026-04-25 17:16:03 -04:00
Sebastien Tardif
ea4da7dfcc Add startup progress indicators (#71720)
* Add startup progress indicators

* Narrow startup progress scope

* Revert startup spinner delay to immediate feedback

* Improve install.sh progress feedback for quiet steps

* Show progress for installer download phases
2026-04-25 17:16:00 -04:00
Peter Steinberger
8f1a214a23 fix: resolve oneshot ACP identities before close 2026-04-25 22:15:52 +01:00
Shakker
cbfc0ddfd1 fix: preserve disabled plugin registry migration 2026-04-25 22:14:53 +01:00
Peter Steinberger
7d343b0b10 fix(plugins): resolve bundled channel doctor metadata from package root 2026-04-25 22:13:04 +01:00
Peter Steinberger
20223e02d9 fix(plugins): anchor runtime dependency installs 2026-04-25 22:12:26 +01:00
Peter Steinberger
0f58a6597d test: extend Windows Parallels agent turn timeout 2026-04-25 22:11:55 +01:00
Ted Li
8e83e52213 fix(memory-core): skip stale dreaming recall sources (#71695)
* fix(memory-core): skip stale dreaming recall sources

* fix(memory-core): parallelize live recall filtering
2026-04-25 17:10:38 -04:00
Yao
fbefbf05bd fix(active-memory): enforce timeoutMs as hard deadline via Promise.race (#71687)
Wrap runRecallSubagent() with Promise.race so maybeResolveActiveRecall
returns a timeout result at the configured timeoutMs even when the
embedded run has not cooperatively checked the abort signal. Late
subagent rejections are caught silently to prevent unhandled promise
errors.

Fixes #71629
2026-04-25 17:10:34 -04:00
Mara 🌿
7f5789575e fix(memory-wiki): skip bridge pruning when memory-core is not loaded (#71764)
When memory-core plugin is not registered (e.g. CLI context),
listActiveMemoryPublicArtifacts returns an empty array. The previous code
would then call pruneImportedSourceEntries with an empty activeKeys Set,
which removes ALL bridge-imported entries.

Now checks getMemoryCapabilityRegistration() instead of relying on artifact
count as a proxy, correctly distinguishing between 'plugin not loaded' and
'plugin loaded with no artifacts'.

Fixes #68373
2026-04-25 17:10:31 -04:00
Poo-Squirry
a1cb8d50ba fix: allow route bindings to override DM session scope (#71750)
Co-authored-by: 따온이네 맥북프로 <tulisy@ttaon-ine-ui-MacBookPro.local>
2026-04-25 17:08:59 -04:00
pashpashpash
bf7d156bb0 Bound native hook permission fingerprints (#71758)
* fix: bound native hook permission fingerprints

* fix: address native hook fingerprint review

* test: isolate native jiti runtime assertions
2026-04-25 17:08:56 -04:00
hcl
4a72e1b990 fix(process): skip kill-tree group kill when child wasn't detached (#71662) (#71681)
* fix(process): skip kill-tree group kill when child wasn't detached (#71662)

When the supervisor spawns a child with detached:false (service-managed
runtime under launchd/systemd), the child shares the gateway's process
group. On session abort or SIGKILL, killProcessTree was unconditionally
issuing process.kill(-pid, 'SIGTERM') — which targets the entire process
GROUP (negative pid is POSIX group-kill semantics) and therefore
SIGTERMs the gateway parent along with the child.

Reporter saw this on macOS (LaunchAgent + KeepAlive=true): aborting a
claude-cli/claude-opus-4-7 session caused the gateway to receive
SIGTERM, then auto-restart, dropping all in-flight sessions. Switching
the primary model to a non-cli provider eliminated it because the
non-cli paths don't go through this kill-tree call. Did not occur on
Linux VPS where the gateway runs detached, because there
useDetached === true and the child got its own process group.

Fix:
- killProcessTree now accepts opts.detached?: boolean. When detached:false,
  killProcessTreeUnix skips the `-pid` group-kill and goes straight to
  direct-pid SIGTERM/SIGKILL. Group-kill default (detached:true) is
  preserved so all existing callers behave exactly as before.
- supervisor/adapters/child.ts:286 now threads the spawn-time `useDetached`
  flag into killProcessTree, so the kill-tree path matches the spawn-time
  detachment decision (line 45 of the same file already computes
  useDetached = process.platform !== 'win32' && !isServiceManagedRuntime()).

Tests:
- new: detached:false skips group kill and uses direct pid SIGTERM only.
- new: default behaviour (detached:true) still uses group kill (regression
  guard so the existing test case isn't accidentally weakened).

Existing tests still pass (6/6 in kill-tree.test.ts). Lint clean.

Out of scope: other killProcessTree callers (mcp-stdio-transport,
bash-tools.process, etc.) keep the default group-kill behaviour because
those processes are typically detached from the gateway. Only the
supervisor/adapters/child.ts path threads `detached` through, since it's
the path that knows whether the child was actually spawned detached.

* fixup(process): also gate kill-tree group-kill on the no-detach spawn fallback (#71662)

Greptile review on the original PR caught a P1 gap: when
spawnWithFallback's initial detached spawn fails and it retries with the
no-detach fallback (label: "no-detach", options.detached: false), the
child runs detached:false but my variable useDetached was still true.
The kill closure then passed `detached: useDetached` = true to
killProcessTree, which still group-killed the gateway — same bug, just
on the fallback path.

Compute the actual detachment as
`useDetached && !spawned.usedFallback` after spawn returns, and pass
that through. This closes the gap: the kill path now correctly skips
group-kill in BOTH:
1. Service-managed runtime (useDetached=false from the start, original case)
2. Detached-spawn fallback to no-detach (useDetached=true at intent
   time but spawned.usedFallback=true)

Tests:
- existing 'uses process-tree kill for default SIGKILL' updated to
  assert the new {detached} option is forwarded.
- new: passes detached:false to killProcessTree when spawn fell back.
- new: passes detached:false in service-managed mode (regression guard
  for the original fix).

11/11 tests pass in child.test.ts. 6/6 in kill-tree.test.ts.
2026-04-25 17:08:53 -04:00
Evgeniy
1841dd9977 fix(subagent-announce): defer drain while parent session is busy (#71706)
When a subagent finishes while its parent main session is still running
(executing tools or awaiting model output), the announce queue would
follow the configured debounce and immediately attempt to deliver the
completion event back into the parent session via callGateway. The
gateway treats the parent as busy and the announce can either get
buffered until the next external user message or surface only as a
delayed echo, breaking the natural sessions_spawn -> sessions_yield
workflow where the parent expects the result to arrive as the next
turn.

This change adds an optional shouldDefer hook on the announce queue
state. The delivery layer wires it to the existing requester session
activity probe (resolveRequesterSessionActivity), so while the parent
session is still active the drain loop sleeps for max(250ms,
debounceMs) and re-checks instead of pushing the announce. As soon as
the parent goes idle, the queue drains normally.

- Plumbs shouldDefer through getAnnounceQueue / enqueueAnnounce.
- Skips drain step in scheduleAnnounceDrain when shouldDefer says the
  target is still busy, with a bounded re-check sleep.
- Updates maybeQueueSubagentAnnounce to pass the activity probe.
- Adds a unit test that holds drain while parent is busy and resumes
  when it goes idle.

No behavior change for callers that do not pass shouldDefer.
2026-04-25 17:08:50 -04:00
sudhindrat
ca1a6e29cb test(config): cover allowConversationAccess in plugin hooks schema validation (#71621) (#71679)
* fix(model-ref): re-add nvidia/ prefix in normalizeStaticProviderModelId (#71552)

* fix(test): use nvidia-prefixed model fixture for double-prefix guard

* test(config): cover allowConversationAccess in plugin hooks schema validation (#71621)

---------

Co-authored-by: Sudhindra Tatti <sudhi@sudhindras-mini.lan>
2026-04-25 17:08:07 -04:00
Mara 🌿
4038f734f7 fix(memory-core): add runtime cron service fallback for dreaming reconciliation (#71694)
* fix(memory-core): add runtime cron service fallback for dreaming reconciliation

When the cron service is unavailable during gateway_start (e.g., due to
a startup timing race or deferred initialization), the startupCronSource
is captured as null and never refreshed. All subsequent runtime
reconciliation attempts fail with 'cron service unavailable', even when
the cron service is fully operational.

This adds a fallback path in the runtime reconciliation that attempts to
obtain the cron service from the plugin API runtime when the startup
capture was null. This handles the case where the cron service becomes
available after the initial startup event.

Fixes #67362

* fix(memory-core): hold gateway context for runtime cron resolution

The previous attempt tried to access api.runtime.cron which doesn't exist
on the PluginRuntime type. The cron service is only accessible through
PluginHookGatewayContext.getCron().

This fix stores the gateway context from the gateway_start event and uses
it to retry cron resolution at runtime when the initial capture was null.
This handles the race condition where the cron service isn't available
during gateway_start (250ms deferred init) but is ready later.

Also refreshes the startupCron capture when the runtime retry succeeds,
so subsequent reconciliation calls resolve immediately.

Addresses review feedback on #71694
2026-04-25 17:07:45 -04:00
hcl
a97fe41a9e perf(cli): skip plugin load on agents list --json (#71739) (#71746)
Reporter measured `agents list --json` at ~7-9s on a fast host (~11s in
container) on 2026.4.23, while peer `--json` commands like
`channels list`, `cron list --all`, and `sessions ... --all-agents`
stay sub-second. Their cold-call dashboard endpoint dropped from 27s to
~2s after a local dist patch — they could even retire the 5-min cache
TTL workaround they had shipped to dodge it.

Root cause: `agents list` inherits `loadPlugins: 'always'` from the
parent `agents` policy in command-catalog, then `agentsListCommand`
calls `buildProviderStatusIndex(cfg)` unconditionally — both paths
trigger the bundled-extension import waterfall (~60+ extension index.js
modules).

`channels list` already uses `loadPlugins: 'never'` and proves the
shape is right; this PR matches that shape with the safer `text-only`
variant so human invocations are unchanged.

Two-line fix per reporter:

1. `src/cli/command-catalog.ts` — opt agents list into `text-only`,
   the same plugin-preload policy bucket that already exists. Plugin
   preload runs for human text output, skips for `--json`.

2. `src/commands/agents.commands.list.ts` — skip
   `buildProviderStatusIndex` (and the per-summary provider
   enrichment loop) when `opts.json`. Provider info is only rendered
   in human text output via `formatSummary`, so dropping it from JSON
   has no observable effect on existing callers that consume `id`,
   `name`, `model`, `bindings`, `isDefault`, `identity*`, `workspace`,
   or `agentDir`. `routes` is config-derived and continues to be set
   in both modes.

Tests:
- new assertion in command-startup-policy.test.ts: `agents list` with
  jsonOutputMode:true now resolves to `loadPlugins: false` (was
  effectively `true` via the parent `agents` 'always' policy).
- existing assertion that human (jsonOutputMode:false) still triggers
  plugin load is preserved verbatim.

6/6 tests pass. Lint clean.

Out of scope:
- `--bindings` flag opt-in for restoring providers in JSON output:
  worth adding later if any consumer needs it; reporter said dashboard
  consumers don't.
- Broader plugin-discovery cache work (#67040, #71690) which addresses
  the same family of cold-start cost.
2026-04-25 17:07:42 -04:00
sudhindrat
f92a8ae9f3 fix(model-ref): re-add nvidia/ prefix in normalizeStaticProviderModelId (#71552) (#71660)
* fix(model-ref): re-add nvidia/ prefix in normalizeStaticProviderModelId (#71552)

* fix(test): use nvidia-prefixed model fixture for double-prefix guard

---------

Co-authored-by: Sudhindra Tatti <sudhi@sudhindras-mini.lan>
2026-04-25 17:07:39 -04:00
Peter Steinberger
2febe72108 fix: isolate ACP spawned runs 2026-04-25 22:06:53 +01:00
Seungwoo hong
63fac653ed fix(talk): Talk Mode TTS improvements for CJK languages (#53553)
* feat(talk): add distinct system sounds for each Talk Mode phase

Play a short system sound on phase transitions to give the user
audible feedback:
- thinking: Tink
- speaking: Pop
- listening (after speech interrupted): Bottle
- listening (after thinking): Submarine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(talk): add right Shift key to interrupt Talk Mode speech

Add TalkSpeechInterruptMonitor — a dedicated global key monitor that
listens for right Shift (keyCode 60) to interrupt Talk Mode speech.
Independent of Push-to-Talk, so it works even when PTT is disabled.

Stops only the current response; the next conversation cycle
continues normally via sendAndSpeak's resumeListeningIfNeeded flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(talk): increase silence detection timeout for CJK locales

Korean, Japanese, and Chinese speakers need longer pauses between
phrases. When the app locale is CJK, enforce a minimum 2000ms
silence window (vs the default 1500ms) to avoid premature
transcript submission.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(talk): remove force-unwraps and log CJK silence clamp in reloadConfig

Replace non-idiomatic force-unwraps (cfg.voiceId!, cfg.modelId!) with
safe flatMap unwrapping, and add an info log when CJK locale clamps the
silence timeout so the override is observable in diagnostics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(talk): add settings toggle to mute phase-transition sounds

Add a "Play phase-transition sounds" checkbox to Voice Wake settings.
When disabled, Talk Mode phase transitions (Tink/Pop/Bottle/Submarine)
are silent. Defaults to enabled to preserve existing behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(talk): add toggle for Right Option speech interrupt

Add a "Press Right Option to stop speech" checkbox to Voice Wake
settings. Also change the interrupt key from right Shift to right
Option (keyCode 61) to avoid conflicts with typing.
Defaults to enabled to preserve existing behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(talk): disable Push-to-Talk while Talk Mode is active

Talk Mode and Push-to-Talk both use the right Option key (keyCode 61).
Disable PTT when Talk Mode is enabled to prevent conflicting handlers,
and restore PTT when Talk Mode is disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(talk): show info when PTT is paused during Talk Mode

Display a footnote under the Push-to-Talk toggle when both PTT and
Talk Mode are enabled, explaining that PTT is paused while Talk Mode
is active and resumes when Talk Mode is turned off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fixup: SwiftFormat lint on TalkModeController phase sound switch

Resolves macos-swift CI lint failures introduced by Korean
comment formatting in 'feat(talk): add distinct system sounds for each Talk Mode phase'.
- Collapse consecutive spaces between sound name and comment
- Move floating comments above the listening case expression so
  they're at the correct indent level

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: hongsw <hongsw@hongswui-Macmini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Fabian Williams <fabian@adotob.com>
2026-04-25 17:05:51 -04:00
Fabian Williams
d6a179bcd9 fix(macos): SwiftFormat wrapMultilineStatementBraces on 2 main files (#71763)
Pre-existing lint errors blocking the macos-swift CI check on every
PR that touches Swift code. Apply the wrapMultilineStatementBraces
rule by moving the opening brace of the multi-line if/else if to its
own line.

- apps/macos/Sources/OpenClaw/ExecAllowlistMatcher.swift:17
- apps/macos/Sources/OpenClaw/ExecApprovals.swift:621

Pure formatting change; no behavioral effect.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:02:13 -04:00
Vincent Koc
cdcc457d2e test(i18n): make registry test sparse-safe 2026-04-25 13:57:07 -07:00
Vincent Koc
74059aaa29 fix(secrets): honor plugin install ledger for web fetch discovery 2026-04-25 13:55:00 -07:00
Peter Steinberger
9e9e024188 docs: clarify ACP model override support 2026-04-25 21:52:36 +01:00
周辉
23a818fa2d docs: enforce background mode & direct completion in coding-agent (#53585) 2026-04-25 16:50:50 -04:00
Vincent Koc
70d1871db7 fix(secrets): honor plugin install ledger in web search risk 2026-04-25 13:50:44 -07:00
Peter Steinberger
90218364b4 test: update bundled setup-entry docker prompt 2026-04-25 21:47:24 +01:00
Peter Steinberger
9d2254be06 test(agents): make Gemini MCP smoke local 2026-04-25 21:45:57 +01:00
Peter Steinberger
17a213f080 refactor(agents): split bundle MCP CLI adapters 2026-04-25 21:45:57 +01:00
Peter Steinberger
bf672d1f2c test: harden Parallels smoke timing 2026-04-25 21:41:55 +01:00
Peter Steinberger
b49d499b45 fix: stabilize native Windows onboarding 2026-04-25 21:41:47 +01:00
Peter Steinberger
dcfd5913fd refactor(agents): share bundle MCP config merging 2026-04-25 21:36:22 +01:00
Peter Steinberger
c3a3ceefbe fix(plugins): keep mirrored runtime deps on staged root 2026-04-25 21:36:06 +01:00
pashpashpash
34fb96622e Support MCP hooks in the Codex harness (#71707)
* codex harness mcp hook parity

* tighten codex hook parity floor

* prove security-style mcp hook blocking

* bound native hook relay key handling

* clarify permission relay defers to provider

* harden native hook relay approvals

* fix(agents): bound native hook relay JSON work budget

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 21:35:47 +01:00
Peter Steinberger
e2fd3dcee9 fix(google): emit opus voice-note tts 2026-04-25 21:33:33 +01:00
Tars
d5b6667823 fix(minimax): enable portal music and video generation 2026-04-25 21:30:10 +01:00
Peter Steinberger
a8e25d9307 docs: guard maintainer-owned triage 2026-04-25 21:26:05 +01:00
Peter Steinberger
607bc53ff3 fix: import missing shell env keys 2026-04-25 21:23:45 +01:00
Peter Steinberger
6a7b76e119 fix(acp): guard sessions_spawn runtime targets 2026-04-25 21:23:24 +01:00
Vincent Koc
20c3177281 fix(plugins): satisfy setup cli fallback lint 2026-04-25 13:22:29 -07:00
Vincent Koc
07796c9fb5 fix(plugins): use registry for setup cli fallback 2026-04-25 13:22:29 -07:00
Peter Steinberger
4069c81b15 test: guard bundled runtime deps against home npm projects 2026-04-25 21:16:53 +01:00
Peter Steinberger
afabbc01b2 fix: keep bundled runtime deps in managed stage 2026-04-25 21:08:16 +01:00
Blockchain Oracle
b40df76c18 fix(cli-runtime): translate MCP transports for CLI backends
Translate OpenClaw `mcp.servers.*.transport` entries into the downstream Claude/Gemini CLI `type` field before writing bundle MCP config.

Also keeps the plugin-sdk bundled-entry fast-path fixture unambiguously CommonJS on Node 24 after runtime-deps mirroring adds a `type: "module"` boundary.

Co-authored-by: Blockchain-Oracle <ajweb3dev@gmail.com>
2026-04-25 21:08:04 +01:00
Peter Steinberger
02f3e9cfa2 fix(talk): honor configured speech locale 2026-04-25 21:05:24 +01:00
Peter Steinberger
8fb24ac3ce test: cover Google telephony TTS private network opt-in 2026-04-25 21:02:06 +01:00
Rohan Shiralkar
cab66c5556 fix(google): honor models.providers.google.request.allowPrivateNetwork in TTS
Image generation and media understanding both thread the
sanitized models.providers.google.request config (including
allowPrivateNetwork) into resolveGoogleGenerativeAiHttpRequestConfig.
Speech synthesis omitted that arg, so TTS always saw
allowPrivateNetwork: false regardless of config — silently falling
back to a different speech provider when the configured Google TTS
endpoint resolved to a private/internal IP (proxies, custom backends,
test mocks).

Mirror the image-generation-provider pattern: thread request through
synthesizeGoogleTtsPcm at both call sites (synthesize and
synthesizeTelephony).

Follow-up to #67216.
2026-04-25 21:02:06 +01:00
Peter Steinberger
6e1017d88a fix: allow native app metadata reconnects 2026-04-25 21:00:31 +01:00
Vincent Koc
89c52988c5 fix(diagnostics): gate traceparent propagation on trusted metadata 2026-04-25 12:55:39 -07:00
InvalidPanda ツ
b64bfc5d9a fix(github-copilot): preserve reasoning IDs for Copilot Codex models (#71684)
* fix(github-copilot): preserve all reasoning IDs and add gpt-5.3-codex support

The existing guard (8fd15ed0e5) only skipped rewriting reasoning item IDs
when encrypted_content was a non-null string. When gpt-5.3-codex is used
via GitHub Copilot, the model falls through to the forward-compat catch-all
with reasoning:false, so encrypted_content is never requested and arrives
as null — bypassing the guard and causing a rewrite. Copilot validates
reasoning item IDs server-side regardless of whether the client includes
encrypted_content, so the rewritten id triggers the 400 error.

Two changes:

1. connection-bound-ids.ts: skip ALL reasoning items unconditionally.
   Reasoning items always reference server-side state bound to their
   original ID; rewriting any of them breaks Copilot's lookup.

2. models.ts + index.ts: extend the forward-compat cloning logic to
   cover gpt-5.3-codex (adds it to the template-target set and to
   CODEX_TEMPLATE_MODEL_IDS so it can also serve as a template source
   for gpt-5.4). Adds gpt-5.3-codex to COPILOT_XHIGH_MODEL_IDS for
   the thinking profile.

Thanks @InvalidPandaa.

* docs(github-copilot): clarify gpt-5.3-codex is a no-op template for itself

https://claude.ai/code/session_01EAFmq4WyKkiUkVAqRXp4Bm

* fix(github-copilot): remove dead reasoning prefix branch in deriveReplacementId

https://claude.ai/code/session_01EAFmq4WyKkiUkVAqRXp4Bm

* fix(github-copilot): align reasoning id replay tests

* test(plugin-sdk): use cjs sidecar for require fast path

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 20:52:07 +01:00
Vincent Koc
1d49b8cdaa fix(plugins): honor registry migration disable in doctor 2026-04-25 12:45:44 -07:00
Vincent Koc
d2046beb40 fix(plugins): stabilize registry package paths 2026-04-25 12:45:44 -07:00
Vincent Koc
958146bbac fix(plugins): satisfy registry repair lint 2026-04-25 12:45:44 -07:00
Vincent Koc
793b58b3f1 fix(plugins): add doctor registry repair 2026-04-25 12:45:43 -07:00
Vincent Koc
5c3eecfea7 fix(codex): require approvals for image-understanding turns (#71703) 2026-04-25 12:45:33 -07:00
Peter Steinberger
fb7b798f96 fix(android): prevent duplicate talk playback 2026-04-25 20:43:25 +01:00
Vincent Koc
346a72ddb9 fix(codex): require authorized inbound claims for bound turns (#71702)
* fix(codex): require authorized inbound claims for bound turns

* fix(codex): consume unauthorized bound turns
2026-04-25 12:42:23 -07:00
Vincent Koc
84f183b7ad test(diagnostics-otel): cover trace self-parent guard 2026-04-25 12:42:07 -07:00
Peter Steinberger
8f49c59d6d chore(release): publish 2026.4.24 appcast 2026-04-25 20:39:57 +01:00
Vincent Koc
b6af40f1f1 docs(minimax): note MINIMAX_API_HOST applies to image generation, baseUrl is ignored
mushuiyu_xydt's commit 0e1ef93e84 (#61155) routes MiniMax image
generation requests to the dedicated image endpoint
(api.minimax.io/v1/image_generation), ignoring models.providers.minimax.baseUrl
(which targets the chat/Anthropic-compatible API), and adds
MINIMAX_API_HOST support for the CN api.minimaxi.com endpoint. The
CHANGELOG entry covered it but docs/providers/minimax.md image-generation
section did not. Add a paragraph naming both endpoints and the
MINIMAX_API_HOST override.
2026-04-25 12:39:07 -07:00
Peter Steinberger
a5f5608d06 docs(agents): forbid self-thanks in changelog 2026-04-25 20:37:36 +01:00
Peter Steinberger
3593beee81 test(gateway): tolerate codex image probe diagnostics 2026-04-25 20:37:31 +01:00
Peter Steinberger
a5a438a17c fix: relax pricing fetch timeout 2026-04-25 20:35:29 +01:00
Vincent Koc
1915b29a3c fix(slack): stop block-based sender rehydration on assistant message edits (#71700)
* fix(slack): stop block-based sender rehydration on message edits

* docs(changelog): note Slack sender attribution fix
2026-04-25 12:34:55 -07:00
Peter Steinberger
bb6cf75463 refactor: centralize context prompt token resolution 2026-04-25 20:34:27 +01:00
Vincent Koc
5fe06f3cdc test(diagnostics-otel): cover untrusted trace parent rejection 2026-04-25 12:34:16 -07:00
Peter Steinberger
9d764ea075 test(plugins): preserve child_process exports in runtime deps mock 2026-04-25 20:33:11 +01:00
1691 changed files with 68568 additions and 19743 deletions

View File

@@ -35,6 +35,21 @@ Use this skill for maintainer-facing GitHub workflow, not for ordinary code chan
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
## Close low-signal manual PRs carefully
- Do not close for red CI alone. Require a clear low-signal category plus stale or failed validation.
- Good manual-close categories:
- blank or mostly untouched PR template with no concrete OpenClaw problem/fix
- random docs-only churn such as root README translations, generic wording tweaks, or community-plugin discoverability docs that should go through ClawHub
- test-only coverage without a linked bug, owner request, or behavior change
- refactor-only cleanup, variable renames, formatting, or generated/baseline churn without maintainer request
- third-party channel/provider/tool/skill/plugin work that belongs on ClawHub instead of core
- risky ops/infra drive-bys such as new external CI services, release workflows, host upgrade scripts, Docker base migrations, or apt retry/fix-missing tweaks without owner request and green validation
- dirty branches where a narrow stated change includes unrelated docs/generated/runtime/extension files
- repeated bot-review spam or copied bot output without author-owned fixes
- Keep or escalate plausible focused bug fixes, green PRs, active maintainer discussions, assigned work, recent author follow-up, and unique reproduction details.
- For third-party capabilities, prefer the `r: third-party-extension` auto-response label when it applies; it points contributors to publish on ClawHub.
## Handle GitHub text safely
- For issue comments and PR comments, use literal multiline strings or `-F - <<'EOF'` for real newlines. Never embed `\n`.

View File

@@ -49,6 +49,19 @@ pnpm openclaw qa suite \
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## OTEL smoke
For local QA-lab OpenTelemetry validation, use:
```bash
pnpm qa:otel:smoke
```
This starts a local OTLP/HTTP trace receiver, runs the `otel-trace-smoke`
scenario through qa-channel, decodes the emitted protobuf spans, and verifies
the exported trace names and privacy contract. It does not require Opik,
Langfuse, or external collector credentials.
## QA credentials and 1Password
- Use `op` only inside `tmux` for QA secret lookup in this repo.

View File

@@ -202,10 +202,16 @@ Before tagging or publishing, run:
pnpm check:architecture
pnpm build
pnpm ui:build
pnpm qa:otel:smoke
pnpm release:check
pnpm test:install:smoke
```
- Use `pnpm qa:otel:smoke` when release validation needs telemetry coverage.
It starts a local OTLP/HTTP trace receiver, runs QA-lab's
`otel-trace-smoke`, and checks span names plus content/identifier redaction
without external Opik or Langfuse credentials.
For a non-root smoke path:
```bash

View File

@@ -82,4 +82,5 @@ OPENCLAW_GATEWAY_TOKEN=
# ELEVENLABS_API_KEY=...
# XI_API_KEY=... # alias for ElevenLabs
# INWORLD_API_KEY=...
# DEEPGRAM_API_KEY=...

11
.github/labeler.yml vendored
View File

@@ -3,6 +3,12 @@
- any-glob-to-any-file:
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"plugin: azure-speech":
- changed-files:
- any-glob-to-any-file:
- "extensions/azure-speech/**"
- "docs/providers/azure-speech.md"
- "docs/tools/tts.md"
"channel: discord":
- changed-files:
- any-glob-to-any-file:
@@ -307,6 +313,11 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/huggingface/**"
"extensions: inworld":
- changed-files:
- any-glob-to-any-file:
- "extensions/inworld/**"
- "docs/providers/inworld.md"
"extensions: kilocode":
- changed-files:
- any-glob-to-any-file:

View File

@@ -5,8 +5,8 @@ on:
types: [opened, edited, labeled]
issue_comment:
types: [created]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned label automation; no untrusted checkout or code execution
types: [labeled]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned label automation; trusted base checkout only, no untrusted PR code execution
types: [opened, edited, synchronize, reopened, labeled]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -20,10 +20,15 @@ permissions: {}
jobs:
auto-response:
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/create-github-app-token@v3
id: app-token
continue-on-error: true
@@ -36,499 +41,15 @@ jobs:
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Handle labeled items
- name: Run Barnacle auto-response
uses: actions/github-script@v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
// Labels prefixed with "r:" are auto-response triggers.
const activePrLimit = 10;
const rules = [
{
label: "r: skill",
close: true,
message:
"Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. Were keeping the core lean on skills, so Im closing this out.",
},
{
label: "r: support",
close: true,
message:
"Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
},
{
label: "r: no-ci-pr",
close: true,
message:
"Please don't make PRs for test failures on main.\n\n" +
"The team is aware of those and will handle them directly on the codebase, not only fixing the tests but also investigating what the root cause is. Having to sift through test-fix-PRs (including some that have been out of date for weeks...) on top of that doesn't help. There are already way too many PRs for humans to manage; please don't make the flood worse.\n\n" +
"Thank you.",
},
{
label: "r: too-many-prs",
close: true,
message:
`Closing this PR because the author has more than ${activePrLimit} active PRs in this repo. ` +
"Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.",
},
{
label: "r: testflight",
close: true,
commentTriggers: ["testflight"],
message: "Not available, build from source.",
},
{
label: "r: third-party-extension",
close: true,
message:
"Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community",
},
{
label: "r: moltbook",
close: true,
lock: true,
lockReason: "off-topic",
commentTriggers: ["moltbook"],
message:
"OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
},
];
const maintainerTeam = "maintainer";
const pingWarningMessage =
"Please dont spam-ping multiple maintainers at once. Be patient, or join our community Discord for help: https://discord.gg/clawd";
const mentionRegex = /@([A-Za-z0-9-]+)/g;
const maintainerCache = new Map();
const normalizeLogin = (login) => login.toLowerCase();
const bugSubtypeLabelSpecs = {
regression: {
color: "D93F0B",
description: "Behavior that previously worked and now fails",
},
"bug:crash": {
color: "B60205",
description: "Process/app exits unexpectedly or hangs",
},
"bug:behavior": {
color: "D73A4A",
description: "Incorrect behavior without a crash",
},
};
const bugTypeToLabel = {
"Regression (worked before, now fails)": "regression",
"Crash (process/app exits or hangs)": "bug:crash",
"Behavior bug (incorrect output/state without crash)": "bug:behavior",
};
const bugSubtypeLabels = Object.keys(bugSubtypeLabelSpecs);
const extractIssueFormValue = (body, field) => {
if (!body) {
return "";
}
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(
`(?:^|\\n)###\\s+${escapedField}\\s*\\n([\\s\\S]*?)(?=\\n###\\s+|$)`,
"i",
);
const match = body.match(regex);
if (!match) {
return "";
}
for (const line of match[1].split("\n")) {
const trimmed = line.trim();
if (trimmed) {
return trimmed;
}
}
return "";
};
const ensureLabelExists = async (name, color, description) => {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color,
description,
});
}
};
const syncBugSubtypeLabel = async (issue, labelSet) => {
if (!labelSet.has("bug")) {
return;
}
const selectedBugType = extractIssueFormValue(issue.body ?? "", "Bug type");
const targetLabel = bugTypeToLabel[selectedBugType];
if (!targetLabel) {
return;
}
const targetSpec = bugSubtypeLabelSpecs[targetLabel];
await ensureLabelExists(targetLabel, targetSpec.color, targetSpec.description);
for (const subtypeLabel of bugSubtypeLabels) {
if (subtypeLabel === targetLabel) {
continue;
}
if (!labelSet.has(subtypeLabel)) {
continue;
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: subtypeLabel,
});
labelSet.delete(subtypeLabel);
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
if (!labelSet.has(targetLabel)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [targetLabel],
});
labelSet.add(targetLabel);
}
};
const isMaintainer = async (login) => {
if (!login) {
return false;
}
const normalized = normalizeLogin(login);
if (maintainerCache.has(normalized)) {
return maintainerCache.get(normalized);
}
let isMember = false;
try {
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: maintainerTeam,
username: normalized,
});
isMember = membership?.data?.state === "active";
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
maintainerCache.set(normalized, isMember);
return isMember;
};
const countMaintainerMentions = async (body, authorLogin) => {
if (!body) {
return 0;
}
const normalizedAuthor = authorLogin ? normalizeLogin(authorLogin) : "";
if (normalizedAuthor && (await isMaintainer(normalizedAuthor))) {
return 0;
}
const haystack = body.toLowerCase();
const teamMention = `@${context.repo.owner.toLowerCase()}/${maintainerTeam}`;
if (haystack.includes(teamMention)) {
return 3;
}
const mentions = new Set();
for (const match of body.matchAll(mentionRegex)) {
mentions.add(normalizeLogin(match[1]));
}
if (normalizedAuthor) {
mentions.delete(normalizedAuthor);
}
let count = 0;
for (const login of mentions) {
if (await isMaintainer(login)) {
count += 1;
}
}
return count;
};
const triggerLabel = "trigger-response";
const activePrLimitLabel = "r: too-many-prs";
const activePrLimitOverrideLabel = "r: too-many-prs-override";
const target = context.payload.issue ?? context.payload.pull_request;
if (!target) {
return;
}
const labelSet = new Set(
(target.labels ?? [])
.map((label) => (typeof label === "string" ? label : label?.name))
.filter((name) => typeof name === "string"),
const { pathToFileURL } = require("node:url");
const moduleUrl = pathToFileURL(
`${process.env.GITHUB_WORKSPACE}/scripts/github/barnacle-auto-response.mjs`,
);
const { runBarnacleAutoResponse } = await import(moduleUrl.href);
const issue = context.payload.issue;
const pullRequest = context.payload.pull_request;
const comment = context.payload.comment;
if (comment) {
const authorLogin = comment.user?.login ?? "";
if (comment.user?.type === "Bot" || authorLogin.endsWith("[bot]")) {
return;
}
const commentBody = comment.body ?? "";
const responses = [];
const mentionCount = await countMaintainerMentions(commentBody, authorLogin);
if (mentionCount >= 3) {
responses.push(pingWarningMessage);
}
const commentHaystack = commentBody.toLowerCase();
const commentRule = rules.find((item) =>
(item.commentTriggers ?? []).some((trigger) =>
commentHaystack.includes(trigger),
),
);
if (commentRule) {
responses.push(commentRule.message);
}
if (responses.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: target.number,
body: responses.join("\n\n"),
});
}
return;
}
if (issue) {
const action = context.payload.action;
if (action === "opened" || action === "edited") {
const issueText = `${issue.title ?? ""}\n${issue.body ?? ""}`.trim();
const authorLogin = issue.user?.login ?? "";
const mentionCount = await countMaintainerMentions(
issueText,
authorLogin,
);
if (mentionCount >= 3) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: pingWarningMessage,
});
}
await syncBugSubtypeLabel(issue, labelSet);
}
}
const hasTriggerLabel = labelSet.has(triggerLabel);
if (hasTriggerLabel) {
labelSet.delete(triggerLabel);
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: target.number,
name: triggerLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
const isLabelEvent = context.payload.action === "labeled";
if (!hasTriggerLabel && !isLabelEvent) {
return;
}
if (issue) {
const title = issue.title ?? "";
const body = issue.body ?? "";
const haystack = `${title}\n${body}`.toLowerCase();
const hasMoltbookLabel = labelSet.has("r: moltbook");
const hasTestflightLabel = labelSet.has("r: testflight");
const hasSecurityLabel = labelSet.has("security");
if (title.toLowerCase().includes("security") && !hasSecurityLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["security"],
});
labelSet.add("security");
}
if (title.toLowerCase().includes("testflight") && !hasTestflightLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: testflight"],
});
labelSet.add("r: testflight");
}
if (haystack.includes("moltbook") && !hasMoltbookLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["r: moltbook"],
});
labelSet.add("r: moltbook");
}
}
const invalidLabel = "invalid";
const spamLabel = "r: spam";
const dirtyLabel = "dirty";
const badBarnacleLabel = "bad-barnacle";
const noisyPrMessage =
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
if (pullRequest) {
if (labelSet.has(badBarnacleLabel)) {
core.info(`Skipping PR auto-response checks for #${pullRequest.number} because ${badBarnacleLabel} is present.`);
return;
}
if (labelSet.has(dirtyLabel)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body: noisyPrMessage,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
const labelCount = labelSet.size;
if (labelCount > 20) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body: noisyPrMessage,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
if (labelSet.has(spamLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
lock_reason: "spam",
});
return;
}
if (labelSet.has(invalidLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
state: "closed",
});
return;
}
}
if (issue && labelSet.has(spamLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
lock_reason: "spam",
});
return;
}
if (issue && labelSet.has(invalidLabel)) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
});
return;
}
if (pullRequest && labelSet.has(activePrLimitOverrideLabel)) {
labelSet.delete(activePrLimitLabel);
}
const rule = rules.find((item) => labelSet.has(item.label));
if (!rule) {
return;
}
const issueNumber = target.number;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: rule.message,
});
if (rule.close) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: "closed",
});
}
if (rule.lock) {
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
lock_reason: rule.lockReason ?? "resolved",
});
}
await runBarnacleAutoResponse({ github, context, core });

View File

@@ -1231,6 +1231,7 @@ jobs:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
shell: bash
run: |

View File

@@ -197,7 +197,8 @@ jobs:
- name: Restore Node 24 path
if: steps.gate.outputs.run_agent == 'true'
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
run:
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
set -euo pipefail
export PATH="${NODE_BIN}:${PATH}"
echo "${NODE_BIN}" >> "$GITHUB_PATH"

View File

@@ -114,7 +114,21 @@ jobs:
- name: Run root Dockerfile CLI smoke
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
const fs = require(\"node:fs\");
const path = require(\"node:path\");
const pkg = require(\"/app/package.json\");
for (const [dep, rel] of Object.entries(pkg.pnpm?.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(`missing patch for ${dep}: ${rel}`);
}
}
"
'
- name: Run agents delete shared workspace Docker CLI smoke
env:

View File

@@ -430,6 +430,11 @@ jobs:
command: pnpm test:docker:doctor-switch
timeout_minutes: 60
release_path: true
- suite_id: docker-session-runtime-context
label: Session Runtime Context Docker E2E
command: pnpm test:docker:session-runtime-context
timeout_minutes: 60
release_path: true
- suite_id: docker-qr
label: QR Import Docker E2E
command: pnpm test:docker:qr

View File

@@ -181,7 +181,8 @@ jobs:
- name: Restore Node 24 path
if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
run:
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
set -euo pipefail
export PATH="${NODE_BIN}:${PATH}"
echo "${NODE_BIN}" >> "$GITHUB_PATH"

View File

@@ -57,7 +57,9 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## GitHub / CI
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
- After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
@@ -84,6 +86,13 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase
has no conflicts and the branch diff is materially unchanged. Do a quick
`git status`, `git diff --check`, and diff/stat sanity check; rerun targeted or
full checks only if conflict resolution, upstream overlap, generated drift,
dependency/config changes, or touched-file content changes make the prior
result stale.
- Landing on `main`: verify touched surface near landing. Default feasible bar: `pnpm check` + `pnpm test`.
- Hard build gate: `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
@@ -122,14 +131,17 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
- Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s).
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete`.
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
## Git
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only. It formats staged files; still run gates.
- Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push.
- `main`: no merge commits; rebase on latest `origin/main` before push. Do not
keep chasing `main` with repeated full gates after one green run plus a clean
rebase sanity pass.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.

View File

@@ -8,27 +8,35 @@ Docs: https://docs.openclaw.ai
### Changes
- CLI/image generation: expose generic `--background` on
`openclaw infer image generate` and `openclaw infer image edit`, keep
`--openai-background` as an OpenAI alias, and let fal image generation honor
`--output-format png|jpeg`. Thanks @steipete.
- Browser/config: allow local managed Chrome launch discovery and post-launch
CDP readiness timeouts to be raised for slower hosts such as Raspberry Pi.
Fixes #66803. Thanks @beat843796.
- Codex/agent: translate `--thinking minimal` to `low` for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive `minimal` directly. Fixes #71946. Thanks @hclsys.
- TTS/WhatsApp: add `/tts latest` read-aloud support with duplicate suppression and `/tts chat on|off|default` session-scoped auto-TTS overrides, completing the on-demand voice-note UX for current-chat replies. Fixes #66032.
- Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.6.3. Thanks @vincentkoc.
- TTS/agents: allow `agents.list[].tts` to override global `messages.tts` for per-agent voices while keeping shared provider credentials and preferences in the existing TTS config surface.
- TTS/agents: make `/tts audio`, `/tts status`, and the `tts` agent tool honor the active `agents.list[].tts` voice/provider override.
- Providers/Azure Speech: add Azure Speech as a bundled TTS provider with Speech-resource auth, voice listing, SSML escaping, native Ogg/Opus voice-note output, and telephony output. (#51776) Thanks @leonchui.
- Browser automation: add a CDP-native role snapshot fallback with iframe-aware refs, cursor-clickable detection, target attach preparation, and `openclaw browser doctor --deep` live snapshot probing.
- CLI/image generation: expose generic `--background` on `openclaw infer image generate` and `openclaw infer image edit`, keep `--openai-background` as an OpenAI alias, and let fal image generation honor `--output-format png|jpeg`. Thanks @steipete.
- Browser/config: allow local managed Chrome launch discovery and post-launch CDP readiness timeouts to be raised for slower hosts such as Raspberry Pi. Fixes #66803. Thanks @beat843796.
- Discord: allow `channels.discord.voice.model` to override the LLM used for voice channel responses while keeping STT and TTS on their existing media settings. (#64368) Thanks @mrdavey.
- Browser/CLI: add `openclaw browser start --headless` as a one-shot local managed browser launch override without rewriting persisted browser config. Thanks @BenediktSchackenberg.
- CLI/Crestodian: open interactive Crestodian in the full OpenClaw TUI shell instead of a basic readline prompt.
- CLI/Crestodian: shorten the startup greeting to the active planner/model, config state, Gateway probe result, and next debug action instead of dumping every discovered backend.
- Plugins: migrate the local plugin registry automatically during package install/update, preserving legacy config and install-ledger state while indexing existing plugin manifests for the new cold registry path. Thanks @vincentkoc.
- Plugins/doctor: make `openclaw doctor --fix` move legacy `plugins.installs`
config records into the managed plugin install ledger and refresh the cold
registry index when needed. Thanks @vincentkoc.
- Plugins: migrate the local plugin registry automatically during package install/update, keeping install metadata in the plugin index while indexing existing plugin manifests for the new cold registry path. Thanks @vincentkoc and @shakkernerd.
- Plugins/doctor: make `openclaw doctor --fix` refresh the plugin index and cold registry index when needed without treating plugin install records as authored config. Thanks @vincentkoc and @shakkernerd.
- Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy `gen_ai.system` by default while emitting `gen_ai.provider.name` under `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`. Thanks @vincentkoc.
- Diagnostics/OTEL: support signal-specific OTLP endpoint overrides for traces, metrics, and logs via config or standard OTEL environment variables. Thanks @vincentkoc.
- Diagnostics/OTEL: emit bounded telemetry exporter health diagnostics for startup and log-export failures without exporting raw error text. Thanks @vincentkoc.
- Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc.
- Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc.
- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.
- Plugins/startup: normalize startup and provider plugin enablement through registry aliases so boot paths do not need the legacy manifest alias scan. Thanks @vincentkoc.
- Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc.
- Plugins/registry: keep installed plugin index records focused on install/state/load paths and resolve plugin capabilities from manifests scoped to indexed plugins. Thanks @shakkernerd.
- Plugins/registry: route cold manifest and capability lookups through the installed plugin index so setup, channels, config, secrets, doctor, and provider metadata paths avoid broad plugin-root scans before runtime execution. Thanks @shakkernerd.
- CLI/models: speed up `models list --all --provider <id>` for static manifest-backed providers by loading catalog rows through the installed plugin index instead of broad manifest scans or runtime suppression hooks. Thanks @shakkernerd.
- CLI/models: use OpenClaw Provider Index preview rows as the final cold fallback for installable providers, while keeping user config, installed manifests, and refreshed cache rows above provider-index metadata. Thanks @vincentkoc.
- Providers/plugins: keep onboarding and auth-choice setup lists on cold manifest/install metadata and add Provider Index install metadata for not-yet-installed provider plugins. Thanks @vincentkoc.
- Providers/plugins: keep provider setup guidance and configure auth imports on cold manifest metadata, with a regression guard against static provider-runtime imports on setup/configure list paths. Thanks @vincentkoc.
- Plugins/chat commands: refresh the persisted plugin registry after `/plugins enable` and `/plugins disable`, matching the CLI mutation path. Thanks @vincentkoc.
- Plugins/compat: mark `OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY` as a deprecated break-glass switch and point operators at registry repair instead. Thanks @vincentkoc.
- Plugins/registry: ignore stale persisted registry reads when plugin policy no longer matches current config, and stamp generated registry files with a do-not-edit warning. Thanks @vincentkoc.
@@ -38,86 +46,251 @@ Docs: https://docs.openclaw.ai
- Diagnostics/OTEL: export existing tool-loop diagnostics as `openclaw.tool.loop` counters and spans without loop messages, session identifiers, params, or tool output. Thanks @vincentkoc.
- Diagnostics/OTEL: export diagnostic memory samples and pressure as bounded memory histograms, counters, and pressure spans to help spot leak regressions without session or payload data. Thanks @vincentkoc.
- Diagnostics/OTEL: add the GenAI `gen_ai.client.token.usage` histogram for input/output model usage while keeping session identifiers and aggregate cache counters out of the semantic metric. Thanks @vincentkoc.
- Plugins/install: move managed plugin install metadata from `plugins.installs`
to the state-managed `plugins/installs.json` ledger, with legacy config reads
kept as a deprecated compatibility fallback. Thanks @vincentkoc.
- Diagnostics/OTEL: add a bounded `openclaw.agent` label to OpenClaw token metrics so per-agent Grafana dashboards can group usage without exporting session identifiers. Thanks @oc-factus.
- Plugins/install: consolidate managed plugin install metadata into the state-managed plugin index at `plugins/installs.json`, replacing the temporary `plugins/installed-index.json` path and removing `plugins.installs` as an authored config surface. Thanks @vincentkoc and @shakkernerd.
- Diagnostics/OTEL: add the GenAI `gen_ai.client.operation.duration` histogram for model-call latency in seconds with bounded provider/model/API and error attributes. Thanks @vincentkoc.
- Diagnostics/OTEL: add GenAI usage token attributes to model-usage spans, including cache read/write input token counts without session identifiers or prompt/response content. Thanks @vincentkoc.
- Diagnostics/OTEL: include bounded GenAI operation, provider, and request-model attributes on model-usage spans so token usage remains self-describing without diagnostic identifiers. Thanks @vincentkoc.
- Diagnostics/OTEL: keep model-usage span GenAI provider attributes aligned with the existing semantic-convention opt-in policy, using legacy `gen_ai.system` unless latest experimental GenAI conventions are enabled. Thanks @vincentkoc.
- Diagnostics/OTEL: keep `gen_ai.request.model` present on GenAI token usage metrics with a bounded `unknown` fallback when model usage events do not include a model. Thanks @vincentkoc.
- Docs/OTEL: document the GenAI token and model-call duration metrics, model-usage span attributes, and `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` provider-attribute behavior. Thanks @vincentkoc.
- Diagnostics/trace: add an internal traceparent propagation helper that only formats trusted dispatcher metadata, keeping plugin-emitted diagnostic traces out of outbound propagation by default. Thanks @vincentkoc.
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.
- Providers/Xiaomi: add MiMo TTS as a bundled speech provider with MP3/WAV output and voice-note Opus transcoding. Fixes #52376. (#55614) Thanks @zoujiejun.
- Providers/ElevenLabs: include `eleven_v3` in the bundled TTS model catalog so model selection surfaces can offer ElevenLabs v3. (#68321) Thanks @itsuzef.
- Providers/Local CLI TTS: add a bundled local command speech provider with file/stdout input, voice-note Opus conversion, and telephony PCM output. (#56239) Thanks @solar2ain.
- Providers/Inworld: add Inworld as a bundled speech provider with streaming TTS synthesis, voice listing, voice-note output, and PCM telephony output. (#55972) Thanks @cshape.
- Providers/Volcengine: add Volcengine/BytePlus Seed Speech as a bundled TTS provider with API-key auth, native Ogg/Opus voice-note output, and MP3 audio-file output. (#55641) Thanks @xuruiray.
- Android/Talk Mode: expose Talk Mode in the Voice tab with runtime-owned voice capture modes and microphone foreground-service escalation. Thanks @alex-latitude.
- Providers/LiteLLM: register `litellm` as an image-generation provider so `image_generate model=litellm/...` calls and `agents.defaults.imageGenerationModel.fallbacks` entries resolve through the LiteLLM proxy. Thanks @zqchris.
- Providers/fal: add Seedance 2.0 reference-to-video models with multi-image, video, and audio reference input mapping plus model-specific capability limits for `video_generate`. Thanks @shivanker.
- Codex harness: require Codex app-server `0.125.0` or newer and cover native MCP `PreToolUse`, `PostToolUse`, and `PermissionRequest` payloads through the OpenClaw hook relay.
- Agents/Codex: teach prompts and `agents_list` to surface native Codex app-server availability so agents prefer `/codex ...` over Codex ACP unless ACP/acpx is explicit. Thanks @vincentkoc.
- ACPX/Droid: add Factory Droid to the live ACP bind Docker matrix, including `.factory` settings staging, `FACTORY_API_KEY` forwarding, and the single-agent `test:docker:live-acp-bind:droid` recipe.
- TTS/personas: add provider-aware TTS personas with deterministic provider binding merges, `/tts persona` controls, gateway/CLI persona state, Google Gemini `audio-profile-v1` prompt wrapping, and OpenAI instruction mapping. (#70748) Thanks @barronlroth.
- Voice Wake: add trigger-based routing so macOS voice wake phrases can select a configured agent or session target, with Gateway routing APIs and node update events. (#30354) Thanks @longbiaochen.
### Fixes
- Plugins/Voice Call: treat missing provider credentials as setup-incomplete
during Gateway startup and log the missing keys as a warning instead of a
runtime startup error, while keeping explicit command/tool errors when used. Thanks
@steipete.
- Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so
`pnpm check:changed` no longer waits on its own `lint:extensions` child.
Thanks @steipete.
- CLI/completion: dedupe provider auth flags before registering `openclaw onboard`
options, so completion-cache refresh during update no longer fails when stale
core fallback flags overlap plugin manifest flags. Fixes #71667.
- Diagnostics/trace: report live context usage from the current prompt snapshot
instead of provider turn totals, avoiding false near-full context spikes on
cached or tool-heavy runs.
- Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when `channels.whatsapp` is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008.
- CLI/model runs: keep `openclaw infer model run` on explicit OpenRouter models from loading the full provider catalog or inheriting chat-agent silent-reply policy, restoring non-empty one-shot probe output. Fixes #68791. Thanks @limpredator.
- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.
- Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.
- CLI/Volta: respawn raw `openclaw` CLI runs through the named `node` shim when the current Node executable resolves to `volta-shim`, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86.
- Installer: warn when multiple npm global roots contain OpenClaw installs, showing active Node/npm/openclaw plus each install path and version so stale version-manager installs are visible. Fixes #40839. Thanks @zhixianio.
- Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg.
- Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker.
- Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai.
- Node/Linux: make `openclaw node install` enable and restart the `openclaw-node` systemd unit instead of the gateway unit on node-only VMs. Fixes #68287. Thanks @dlebee-agent.
- Browser/CDP: retry transient raw-CDP WebSocket handshake failures before any
browser command is sent, and reconnect stale persistent Playwright CDP
sessions for safe tab-list reads without replaying mutating browser actions.
Fixes #67728.
- Gateway/Linux: retry `systemctl --user enable` after a second daemon reload when the freshly written gateway unit is not visible yet on migrated systemd installs. Fixes #65184. Thanks @liushuaiiu.
- Telegram: preserve exact selected quote text when sending native quote replies, and retry with legacy replies if Telegram rejects quote parameters. (#71952) Thanks @rubencu.
- Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd.
- Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.
- Plugins/chat: keep `/plugins list`, `/plugins enable`, and `/plugins disable` on the persisted plugin index path so chat plugin management does not load diagnostic/runtime plugin registries before execution. Thanks @shakkernerd.
- Plugins/doctor: read workspace plugin status and legacy web-search ownership through installed-index manifest metadata instead of broad manifest registry scans. Thanks @shakkernerd.
- CLI/agents: read channel provider status from read-only plugin index metadata for text `agents list` output instead of the loaded channel registry. Thanks @shakkernerd.
- Logging: redact configured secret patterns at console and file-log sink exits
so credentials that reach the logger are masked before terminal display or
JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.
- Gateway/services: refuse process and service mutations from an older OpenClaw
binary when the config was last written by a newer version, preventing
split-brain installs from stopping or rewriting newer gateway services. Fixes
#57079.
- Gateway: reserve `/healthz` and `/readyz` ahead of plugin, canvas, and Control UI HTTP stages so liveness/readiness probes still answer when a later route handler stalls. Fixes #69674. Thanks @Xike-Creek.
- Logging: load `logging.file` and redaction settings directly from the active
OpenClaw config path in bundled runtimes, so packaged gateways stop falling
back to `/tmp/openclaw`. Fixes #59370, #67168, and #61295. Thanks @KeaneYan,
@Pan9hu, and @zsjlovelike.
- Logging: rotate file logs at `logging.maxFileBytes`, keep bounded numbered
archives, and make long-lived rolling loggers follow the current-day file
instead of suppressing diagnostics or writing stale dated files. Fixes #58583
and #62381. Thanks @jpeghead and @zhaoleink.
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
start browser-capable CLI node services through the restored
`openclaw node start` command, and show an actionable browser-control error
when the local control service is missing. Fixes #66637.
- Gateway/update: fail package updates when the restarted managed gateway reports the wrong version, including fallback restarts and JSON mode, avoiding false-success mixed-version restarts after macOS LaunchAgent updates. Fixes #71835. Thanks @abhinas90 and @jsompis.
- Gateway/update: warn before package updates and bundled plugin runtime-dependency repairs when the target volume appears low on disk space, without blocking installs on best-effort filesystem checks. Fixes #71835. Thanks @abhinas90 and @jsompis.
- Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin.
- Gateway/Linux: include fnm `aliases/default/bin` in generated service PATHs and let doctor accept either modern fnm aliases or the legacy `current/bin` symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott.
- Installer/Linux: run apt installs with noninteractive dpkg and needrestart settings so fresh Ubuntu 24.04 `curl | bash` installs do not hang while installing Node.js, Git, or build tools. Fixes #41146. Thanks @iht76, @alexcarv318, @cs3gallery, @firofame, and @cgdusek.
- Providers/Bedrock: defer the AWS SDK import until Bedrock discovery actually runs so plugin registration and setup stay lightweight on cold start. Fixes #71690. Thanks @jarvis-ai-gregmoser.
- Installer/macOS: stop immediately when Homebrew `node@24` installation fails and avoid printing PATH advice for missing Homebrew Node installs. Fixes #70411. Thanks @1fanwang.
- WhatsApp: remove ack reactions after a visible reply when `messages.removeAckAfterReply` is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken.
- Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo.
- Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.
- Plugins: scope setup and web-provider metadata manifest reads to explicit plugin ids when callers already know the owning plugin set. Thanks @vincentkoc.
- Plugins/onboarding: defer onboarding install-record index writes until the guarded config commit so setup failures cannot leave the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
- Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd.
- Config/providers: accept `video` and `audio` in configured model `input` values and
preserve them in provider catalog entries. Fixes #20721. Thanks @alvinttang.
- Models/auth: honor the parent `--agent` flag for auth write commands (`add`, `login`, `setup-token`, `paste-token`, and the GitHub Copilot shortcut) so OAuth/API-key/token results are written to the requested agent store instead of the default agent. Fixes #71864. (#71933) Thanks @balric-seo.
- TTS: strip model-emitted TTS directives from streamed block text before channel
delivery, including directives split across adjacent blocks, while preserving
the accumulated raw reply for final-mode synthesis. Fixes #38937.
- TTS: keep explicit `provider=...` directive keys scoped to that provider and
warn on unsupported keys instead of letting another speech provider consume
overlapping keys. Fixes #60131.
- TTS/Feishu: normalize final-mode streamed TTS-only audio before delivery so
generated voice-note files use the same safe media path and native voice
routing as normal final replies. Fixes #71920.
- Feishu: transcribe inbound voice-note audio with the shared media audio path
before agent dispatch and keep raw Feishu `file_key` payloads out of message
text. Fixes #67120 and #61876.
- Tasks: terminalize async Gateway agent task records from the Gateway run result while preserving aborted, failed, and cancelled outcomes instead of leaving completed runs stuck as active or lost. (#71905) Thanks @likewen-tech.
- WhatsApp: let authorized group voice-note transcripts satisfy mention gating
before reply dispatch, while keeping unmentioned transcripts in pending group
history. Fixes #44908.
- Media understanding: carry channel voice-note preflight state into attachment
selection so WhatsApp, Feishu, Telegram, and Discord do not transcribe the
same inbound audio twice. Fixes #70580.
- TTS/BlueBubbles: deliver compatible auto-TTS audio as iMessage voice memo
bubbles instead of plain MP3/CAF file attachments. Fixes #16848.
- TTS: resolve voice-note and voice-memo routing from channel plugin
capabilities instead of speech-core-owned channel id lists.
- ACP: send subagent and async-task completion wakes to external ACP harnesses as
plain prompts instead of OpenClaw internal runtime-context envelopes, while
keeping those envelopes out of ACP transcripts.
- TTS/status: show configured TTS model, voice, and sanitized custom endpoint in `/status`, preserve OpenAI-compatible TTS instructions on custom endpoints, and retry empty Microsoft/Edge TTS output once. Addresses #46602, #47232, and #43936. Thanks @leekuangtao, @Huntterxx, and @rex993.
- Agents/Gateway: steer agent-driven config edits and restarts through the owner-only `gateway` tool, document `config.schema.lookup` as the field-doc source, and warn against using `gateway stop && gateway start` as a restart substitute on macOS. Fixes #71929. Thanks @ygc3817922006-sketch.
- Media understanding/audio: inject a deterministic transcript placeholder for too-small voice notes so agents do not hallucinate transcription or provider failures. Fixes #48944. Thanks @eulicesl.
- Providers/vLLM: send Nemotron 3 chat-template kwargs when thinking is off
and honor configured `params.chat_template_kwargs` for OpenAI-compatible
completions, so vLLM/Nemotron replies stay visible instead of becoming
thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch.
- Channels/replies: strip copied inbound metadata blocks from user-facing
assistant replies and model replay history, so Discord/vLLM sessions do not
leak `Conversation info` / `UNTRUSTED ... message body` envelopes after a
model echoes them. Fixes #71847. Thanks @jmystaki-create.
- Subagents/memory: keep inter-session completion wakes out of memory and
dreaming session exports, and strip internal runtime-context blocks from
realtime Control UI chat events.
- Agents/Claude: treat zero-token empty `stop` turns as failed provider output,
retry once, repair replay, and allow configured model fallback instead of
preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI.
- Tasks: normalize task lifecycle timestamps at create, update, and restore time, and report retained lost tasks as audit warnings until their cleanup window expires. (#71871) Thanks @likewen-tech.
- Diagnostics/OTEL: treat normal early model stream cleanup as a completed model call instead of exporting a misleading `StreamAbandoned` error span. Thanks @vincentkoc.
- Gateway/pairing: stop corrupt or unreadable device/node pairing stores from being treated as empty state, preserving `paired.json` for repair instead of overwriting approved pairings. Fixes #71873. Thanks @iret77.
- ACP: keep `/acp` management commands, plus local `/status` and `/unfocus`, on the Gateway path inside ACP-bound threads so they are not consumed as ACP prompt text. Fixes #66298. Thanks @kindomLee.
- ACPX: stop probing ACP agents during normal Gateway startup; the embedded backend now registers without spawning Codex/ACP child processes unless `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=1` is explicitly set.
- CLI/image edit: accept `--size`, `--aspect-ratio`, and `--resolution` on `openclaw infer image edit` and report all supported edit flags from `capability inspect image.edit`. Thanks @Pinghuachiu.
- ACP: wait for the configured runtime backend to become healthy before startup identity reconciliation, avoiding transient acpx warnings during Gateway boot. Fixes #40566.
- Channels/ACP bindings: time out configured binding readiness checks instead of letting Discord preflight hang forever when an ACP target never settles. Fixes #68776.
- Control UI: hide the chat loading skeleton during background history reloads when existing messages or active stream content are already visible, avoiding reload flashes on high-latency local gateways. Fixes #71844. Thanks @WolvenRA.
- Control UI: keep locally optimistic chat messages visible when a history reload temporarily returns empty, avoiding lost first-turn messages on high-latency gateways. Fixes #71878. Thanks @WolvenRA.
- Control UI: keep chat history limits based on visible messages after filtering heartbeat and control-only transcript rows, so recent hidden entries no longer make older visible replies disappear. Thanks @WolvenRA.
- Agents/images: scrub old `[media attached: ...]`, `[Image: source: ...]`,
and `media://inbound/...` markers from pruned model replay context so stale
media refs are not rehydrated as fresh prompt images. Fixes #71868. Thanks
@jmeadlock.
- Docker/Bonjour: disable Bonjour/mDNS advertising by default for bundled
Compose gateways on bridge networking, while keeping host/macvlan opt-in with
`OPENCLAW_DISABLE_BONJOUR=0`. Fixes #71879. Thanks @gbballpack.
- CLI/status: label the OpenClaw Serve/Funnel setting as `Tailscale exposure` and show daemon state separately when available, so `gateway.tailscale.mode: "off"` no longer reads like the Tailscale daemon is stopped. Fixes #71790. Thanks @pesvobodak.
- Plugins/Bonjour: stop ciao mDNS watchdog failures from looping forever when the advertiser stays stuck in `probing` or `announcing`; Bonjour now disables itself for the current Gateway process after repeated failed restarts while the Gateway keeps running. Fixes #69011. Thanks @siddharthaagarwalofficial-ux, @FiredMosquito831, and @spikefcz.
- Gateway/Fly.io: seed Control UI allowed origins from the actual runtime bind and port so CLI-driven non-loopback starts do not crash before config exists. Fixes #71823.
- macOS/remote SSH: keep discovered gateway hosts in `gateway.remote.sshTarget` while pinning SSH transport URLs to the local loopback tunnel, so browser automation does not regress into blocked non-loopback `ws://` endpoints. Fixes #67336.
- Gateway/proxy: bootstrap env proxy dispatching from direct Gateway startup so provider and plugin network requests honor `HTTPS_PROXY`/`HTTP_PROXY` before the first embedded agent attempt runs. (#71833) Thanks @mjamiv.
- Plugins/runtime deps: verify clean npm installs actually place requested bundled runtime packages in the managed install root, reporting exact missing specs instead of a false successful repair. (#71883) Thanks @Solvely-Colin.
- Plugins/discovery: ignore stale `plugins.load.paths` aliases that point back at packaged bundled plugin directories and have doctor remove them, keeping bundled plugins on the runtime-deps staging path. Thanks @codex.
- Models/LM Studio: preserve `@iq*` quant suffixes in model refs and provider matching so `/model lmstudio/...@iq3_xxs` keeps the exact LM Studio variant. Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.
- Matrix/cron: preserve the live Matrix delivery target when creating implicit announce reminder jobs so mixed-case room IDs are not reconstructed from lowercased session keys. Fixes #71798.
- Feishu: accept Schema 2.0 card action callbacks that report `context.open_chat_id` instead of legacy `context.chat_id`, so button callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.
- Feishu: keep synthetic card-action and bot-menu ids out of platform reply targets, using the real card callback message id when Feishu provides one and plain-sending otherwise. Fixes #71673. Thanks @eddy1068.
- Plugins/QQ Bot: prefer an installed QQ Bot plugin that declares it replaces the bundled `qqbot` channel, preventing duplicate `qqbot_channel_api` and `qqbot_remind` tool registration noise. Fixes #63102.
- Browser automation: keep stable tab ids and labels attached when Chromium replaces the raw target after form submissions or other action-triggered navigations, and return the replacement `targetId` from `/act` when the match is provable. Fixes #46137.
- QQ Bot: make `qqbot_remind` schedule, list, and remove Gateway cron jobs directly for owner-authorized senders instead of returning `cronParams` and relying on a follow-up generic `cron` tool call. Fixes #70865. (#70937) Thanks @GaosCode.
- Agents/ACP: hide `sessions_spawn` ACP runtime options unless an ACP backend is loaded, and make `/acp doctor` call out `plugins.allow` blocking bundled `acpx`. Thanks @vincentkoc.
- Agents/Codex: keep ACP prompt/skill routing hidden unless an ACP runtime backend is available, and warn in doctor when enabled Codex plugin configs still route `openai-codex/*` models through PI. Thanks @vincentkoc.
- Media delivery: avoid sending generated image attachments twice when the assistant reply already includes explicit `MEDIA:` lines for the same turn, and reject unsafe remote `MEDIA:` URLs before delivery. Thanks @pashpashpash.
- Codex harness: ignore retryable app-server error notifications after Codex recovers, and preserve the real nested error message for terminal app-server failures instead of replacing it with a generic failure. Thanks @pashpashpash.
- Agents/Codex: prepare native Codex sub-agent session metadata without a
nested Gateway session patch and add a focused Docker smoke for the app-server
sub-agent path. Thanks @vincentkoc.
- Agents/subagents: keep queued subagent announces session-only when the requester has no external channel target, avoiding ambiguous multi-channel delivery failures. Fixes #59201. Thanks @larrylhollan.
- Image understanding: preserve configured provider-prefixed vision model metadata when callers request the model without the provider prefix, so custom image models keep their `input: ["text", "image"]` capability. Fixes #33185. Thanks @Kobe9312 and @vincentkoc.
- Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.
- Plugins/install: reject native plugin archives that do not include a valid `openclaw.plugin.json`, preventing manifestless archives from writing install records that later show missing-manifest diagnostics. Thanks @shakkernerd.
- Plugins/uninstall: remove tracked managed plugin install directories even when the persisted install path differs from the default id-derived target, while still refusing deletes outside the managed extensions root. Thanks @shakkernerd.
- Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.
- Plugins/onboarding: defer channel/provider plugin install records until the owning config write commits, keeping setup failures from advancing the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
- Plugins/config: route configure and agent setup writes with pending plugin install records through the plugin index commit helper so provider onboarding metadata is not stripped by plain config writes. Thanks @shakkernerd.
- Plugins/channels: merge pending channel plugin install records with the existing plugin index before config writes, preserving unrelated tracked installs during channel setup, resolve, remove, and capability repair flows. Thanks @shakkernerd.
- Plugins/config: defer shipped `plugins.installs` index migration during config writes until the guarded config commit window and roll it back if the config write fails before commit. Thanks @shakkernerd.
- Sessions: keep embedded runtime context out of the visible user prompt by sending it as a hidden next-turn custom message, and teach doctor to repair affected 2026.4.24 transcripts with duplicated prompt-rewrite branches. Fixes #71761.
- Gateway/subagents: keep direct-loopback backend RPCs authenticated with the shared gateway token/password off stale CLI paired-device scope baselines, so internal calls no longer hit `scope-upgrade` pairing prompts while remote, browser, node, device-token, and explicit-device paths still require normal pairing approval. Fixes #63548.
- Providers/Azure OpenAI: give deployment-scoped image generation requests a longer 600s default timeout so slow `gpt-image-2` generations can complete without a per-call `timeoutMs`. Fixes #71705. Thanks @voytas75.
- Gateway/plugins: link source-checkout bundled runtime dependency caches instead of recursively copying `node_modules` on the gateway main thread, preventing local status, node, and skill probes from timing out during startup cache restores. Thanks @steipete.
- Skills/remote nodes: only expose remote macOS skill bins for connected nodes, clear stale bin matches when node probes fail, and include probe command, timeout, bin count, and connection state in timeout logs. Thanks @steipete.
- Skills/remote nodes: recognize `system.which` object-map responses when probing connected macOS nodes, so Linux gateways can expose macOS-only skills such as Apple Notes when the required binaries are installed remotely. Fixes #71877. Thanks @miguelarios.
- CLI/gateway: keep diagnostic probes from creating first-time read-only device pairings, while still reusing cached device tokens for detailed read probes. Fixes #71766. Thanks @SunboZ.
- CLI/plugins: keep `message` startup, `channels logs`, `agents delete`, and `agents set-identity` off broad plugin preloading; message delivery still loads plugins when the action actually runs.
- Image understanding: resolve configured image models such as local LM Studio vision entries before reporting `Unknown model` when the discovery registry has not registered that provider. Fixes #66486. Thanks @zhanggpcsu.
- QQ Bot: ignore self-echoed bot messages using the outbound ref-index marker, preventing mirrored replies from re-entering the agent loop while still allowing users to quote bot replies. Fixes #71912. Thanks @wangyc6003.
- Sessions: separate reset freshness from session-store `updatedAt`, so heartbeat, cron, exec, and gateway bookkeeping no longer prevent configured daily/idle resets from rolling long-running channel sessions. Fixes #68315, #63732, #63820, and #69083. Thanks @maxatv, @longhairedsi, @bradfreels, and @akessel56.
- Sessions: clear queued system-event notices during `/new`, `/reset`, gateway `sessions.reset`, and daily/idle rollover so stale background updates cannot leak into the first prompt of the fresh session. Fixes #66864. Thanks @opeyio, @Magicray1217, and @cedillarack.
- CLI/agents: keep `agents bind`, `agents unbind`, and `agents bindings` on setup-safe channel metadata paths so they do not preload bundled plugin runtimes or stage runtime dependencies. Fixes #71743.
- Plugins/registry: preserve explicit disabled plugin records during registry migration without persisting every unused bundled plugin discovered on disk. Thanks @shakkernerd.
- Windows/native: keep CLI startup and bundled provider plugin loading off Windows ESM raw-path failure paths, fixing native onboarding/install smoke on Node 24. Thanks @steipete.
- Plugins/doctor: read bundled channel doctor capabilities through the same packaged plugin directory resolver used by plugin loading, so published installs keep Matrix DM allowlist repairs on `channels.matrix.dm.*` instead of writing invalid top-level `dmPolicy` keys. Fixes #71757.
- Plugins/Windows: keep bundled plugin Jiti loaders off the native import path on Windows so channel plugins such as Telegram no longer crash with `ERR_UNSUPPORTED_ESM_URL_SCHEME` on `C:\...` paths. Fixes #71749. Thanks @smeyer9.
- Providers/Ollama: use Ollama's current `/api/web_search` endpoint and honor `https://ollama.com` model-provider base URLs for Ollama Web Search. Fixes #71741. Thanks @madhvidua.
- Memory/Ollama: serialize Ollama memory embedding batches and add an inline batch timeout override, with longer defaults for local/self-hosted embedding providers. Thanks @steipete.
- Sessions/usage: exclude compaction checkpoint transcript snapshots from usage totals and session discovery, while keeping old checkpoint files removable. Thanks @steipete.
- CLI/agents: keep `openclaw agents list --json` on the config-only path by default, avoiding bundled plugin loading unless callers request `--bindings`. Fixes #71739. Thanks @kaloster.
- Plugins/install: force plugin dependency installs to stay project-local even when inherited npm config requests global installs, so successful installs still materialize the plugin's staged `node_modules`.
- Providers/Google: transcode Gemini TTS PCM to Opus for voice-note targets so WhatsApp and other native voice-note replies can play as voice messages.
- TTS/WhatsApp: mark non-Opus provider output as voice-note intent so channel delivery transcodes MP3/WebM replies to Ogg/Opus PTT audio.
- Plugins/runtime deps: reuse existing external bundled-plugin stage roots when mirrored plugin roots are inspected again, avoiding second-generation `openclaw-unknown-*` stages and repeated first-turn restaging. Fixes #71599.
- iOS/macOS Talk Mode: allow `talk.speechLocale` to set the speech recognition locale for non-English voice conversations. Fixes #44688.
- Plugins/providers: honor explicit plugin candidate lists instead of reading a persisted registry snapshot from local state, keeping candidate-scoped provider discovery hermetic.
- Plugins/doctor: keep bundled plugin runtime-dependency repairs inside the managed OpenClaw stage even when user npm prefix/global config points npm at `$HOME/node_modules`. Fixes #71730.
- ACP/sessions_spawn: reject normal OpenClaw config agent ids when callers explicitly request `runtime="acp"`, while allowing agents configured with `runtime.type="acp"` to resolve to their ACP harness id. Fixes #63914.
- ACP/sessions_spawn: apply `runTimeoutSeconds` to ACP child turns and dispatch those turns on the background subagent lane, so quota-stalled ACP harnesses do not occupy the main agent lane indefinitely. Fixes #68823.
- ACP/oneshot: reconcile runtime session identity before closing completed oneshot ACP runs, so finished `sessions.json` entries do not stay stuck with `acp.identity.state="pending"`.
- ACPX: bundle `acpx@0.6.1` so unsupported generic model overrides fail clearly instead of silently falling back to the target adapter default.
- ACP/models: document that non-Codex ACP model overrides require adapter support for ACP `models` plus `session/set_model`, so unsupported harnesses fail clearly instead of silently falling back to their defaults.
- Plugins/Voice Call: treat missing provider credentials as setup-incomplete during Gateway startup and log the missing keys as a warning instead of a runtime startup error, while keeping explicit command/tool errors when used. Thanks @steipete.
- Android/Talk Mode: prevent duplicate TTS playback when fast or repeated final chat events arrive while Talk Mode is waiting for its own response. Fixes #46546.
- Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so `pnpm check:changed` no longer waits on its own `lint:extensions` child. Thanks @steipete.
- CLI/completion: dedupe provider auth flags before registering `openclaw onboard` options, so completion-cache refresh during update no longer fails when stale core fallback flags overlap plugin manifest flags. Fixes #71667.
- Diagnostics/trace: report live context usage from the current prompt snapshot instead of provider turn totals, avoiding false near-full context spikes on cached or tool-heavy runs.
- Providers/Google: honor `models.providers.google.request.allowPrivateNetwork` for Gemini TTS and telephony TTS, matching Google image generation and media understanding. (#71723) Thanks @ro-hansolo.
- Providers/MiniMax: register `minimax-portal` for music and video generation, preserving OAuth auth and regional MiniMax base URLs across the shared `music_generate` and `video_generate` tools. (#63241) Thanks @tars90percent.
- Providers/onboarding: keep Runway and Alibaba Model Studio out of the text-inference setup picker by scoping their video-generation auth choices to the media setup flow. (#65856) Thanks @Jah-yee.
- Plugins/Bonjour: stop the gateway from crash-looping on `CIAO PROBING CANCELLED` when the mDNS watchdog cancels a stuck probe. Restores the rejection-handler wiring dropped during the bonjour plugin migration and shares unhandled-rejection state across module instances so plugin-staged copies of `openclaw/plugin-sdk/runtime` register into the same handler set the host consults. Especially affects Docker on macOS, where mDNS probing reliably hits the watchdog. Thanks @troyhitch.
- Google Meet: report pinned Chrome nodes as offline or missing capabilities in
setup/join diagnostics, keep inaccessible nodes out of auto-selection, and
preflight local BlackHole/SoX requirements before agents try local Chrome.
Thanks @steipete.
- Providers/MiniMax: route `image-01` requests to the dedicated image
generation endpoint while preserving CN endpoint selection. Fixes #61149.
Thanks @mushuiyu886.
- Plugins/startup: remove ownerless bundled runtime-dependency install locks
after a short grace window and include lock owner details when startup times
out waiting for a plugin runtime-deps lock.
- Live tests/voice: accept common STT variants for OpenClaw and ElevenLabs
brand names so provider smoke tests fail on real regressions rather than
equivalent transcripts.
- Agents/replies: forward sanitized underlying agent failure details on external
channels instead of replacing unknown failures with a generic retry message.
- Browser/CDP: honor configured remote and `attachOnly` CDP HTTP/WebSocket
timeouts when opening tabs through raw CDP or `/json/new` fallback. (#54238)
Thanks @FuncWei.
- WhatsApp/TTS: send visible text separately from PTT voice-note audio instead
of relying on hidden voice-note captions. Fixes #51081.
- Browser/client: avoid telling agents to restart OpenClaw for dispatcher
timeouts on external browser profiles such as `attachOnly`, remote CDP, and
existing-session. (#40815) Thanks @0xsline.
- Agents/TTS: preserve `[[audio_as_voice]]` directives on trusted text
tool-result `MEDIA:` payloads so generated audio still delivers as a voice
note. (#46535) Thanks @azade-c.
- Agents/TTS: keep queued tool media when an assistant ends with `NO_REPLY` on
non-block delivery paths, so media-only generated audio replies still send.
(#60025) Thanks @bradlind1.
- Telegram/STT: frame inbound voice-note transcripts as machine-generated,
untrusted text in agent context while preserving raw transcript mention
detection. Closes #33360. Thanks @smartchainark.
- Google Meet: report pinned Chrome nodes as offline or missing capabilities in setup/join diagnostics, keep inaccessible nodes out of auto-selection, and preflight local BlackHole/SoX requirements before agents try local Chrome. Thanks @steipete.
- Providers/MiniMax: route `image-01` requests to the dedicated image generation endpoint while preserving CN endpoint selection. Fixes #61149. Thanks @mushuiyu886.
- Plugins/startup: remove ownerless bundled runtime-dependency install locks after a short grace window and include lock owner details when startup times out waiting for a plugin runtime-deps lock.
- Plugins/install: anchor bundled runtime-dependency npm installs with an OpenClaw-owned package manifest so Linux updates cannot accidentally write to a parent `$HOME/node_modules` tree. Fixes #71730.
- Plugins/install: pass onboarding plugin config into plugin index writes so local plugin installs outside default discovery roots keep their install records. Thanks @shakkernerd.
- Plugins/install: migrate shipped `plugins.installs` config records into the plugin index while stripping them from runtime config and future writes. Thanks @shakkernerd.
- Plugins/install: durably remove shipped `plugins.installs` from `openclaw.json` after its records are copied into the plugin index, while rolling back the index write if config cleanup fails. Thanks @shakkernerd.
- Plugins/install: keep migrated plugin install records in the plugin index even when the plugin manifest is missing or invalid, so update, uninstall, inspect, and audit can still recover broken installs. Thanks @shakkernerd.
- Plugins/security: keep plugin audit JSON check ids stable while reporting plugin index install-record findings with updated wording. Thanks @shakkernerd.
- CLI/config: reject direct `plugins.installs` edits with guidance to use `openclaw plugins install`, `openclaw plugins update`, or `openclaw plugins uninstall` instead. Thanks @shakkernerd.
- Live tests/voice: accept common STT variants for OpenClaw and ElevenLabs brand names so provider smoke tests fail on real regressions rather than equivalent transcripts.
- Agents/replies: forward sanitized underlying agent failure details on external channels instead of replacing unknown failures with a generic retry message.
- CLI/MCP: translate OpenClaw `mcp.servers.*.transport` entries into Claude/Gemini CLI `type` fields so streamable HTTP MCP servers load in CLI backend sessions. (#71724) Thanks @Blockchain-Oracle.
- Browser/CDP: honor configured remote and `attachOnly` CDP HTTP/WebSocket timeouts when opening tabs through raw CDP or `/json/new` fallback. (#54238) Thanks @FuncWei.
- WhatsApp/TTS: send visible text separately from PTT voice-note audio instead of relying on hidden voice-note captions. Fixes #51081.
- Browser/client: avoid telling agents to restart OpenClaw for dispatcher timeouts on external browser profiles such as `attachOnly`, remote CDP, and existing-session. (#40815) Thanks @0xsline.
- Agents/TTS: preserve `[[audio_as_voice]]` directives on trusted text tool-result `MEDIA:` payloads so generated audio still delivers as a voice note. (#46535) Thanks @azade-c.
- Agents/TTS: keep queued tool media when an assistant ends with `NO_REPLY` on non-block delivery paths, so media-only generated audio replies still send. (#60025) Thanks @bradlind1.
- Telegram/STT: frame inbound voice-note transcripts as machine-generated, untrusted text in agent context while preserving raw transcript mention detection. Closes #33360. Thanks @smartchainark.
- Subagents/browser: show an actionable `/tools` notice when browser automation is configured but filtered out by the active tool profile, and document that coding-profile agents should use `tools.alsoAllow: ["browser"]` rather than subagent allowlists alone.
- Control UI/Quick Settings: persist the assistant avatar override to browser local storage (mirroring the user avatar) so uploaded image data URLs no longer fail config validation with "Too big: expected string to have <=200 characters". Also lift the gateway-side `ui.assistant.avatar` length cap to match the user avatar size budget for non-UI clients writing the field directly. Thanks @BunsDev.
- Plugin SDK: share diagnostic event subscriptions across duplicate source/dist
module graphs so legacy root SDK imports still receive runtime diagnostic events.
- Agents/Bedrock: prevent empty assistant stream-error turns from poisoning
Converse replay by persisting, repairing, and replaying a non-empty fallback
block. Fixes #71572. (#71627) Thanks @openperf.
- Agents/Anthropic/Bedrock: strip thinking blocks with missing, empty, or blank
replay signatures before provider conversion, falling back to non-empty
omitted-reasoning text when needed so corrupted signed-thinking history no
longer poisons subsequent turns. Fixes #45010. (#70054) Thanks @castaples.
- Agents/Anthropic/Bedrock: preserve stripped thinking-only assistant replay
turns with non-empty omitted-reasoning text so provider adapters keep strict
user/assistant turn shape. Thanks @wujiaming88.
- Plugin SDK: share diagnostic event subscriptions across duplicate source/dist module graphs so legacy root SDK imports still receive runtime diagnostic events.
- Agents/Bedrock: prevent empty assistant stream-error turns from poisoning Converse replay by persisting, repairing, and replaying a non-empty fallback block. Fixes #71572. (#71627) Thanks @openperf.
- Agents/Anthropic/Bedrock: strip thinking blocks with missing, empty, or blank replay signatures before provider conversion, falling back to non-empty omitted-reasoning text when needed so corrupted signed-thinking history no longer poisons subsequent turns. Fixes #45010. (#70054) Thanks @castaples.
- Agents/Anthropic/Bedrock: preserve stripped thinking-only assistant replay turns with non-empty omitted-reasoning text so provider adapters keep strict user/assistant turn shape. Thanks @wujiaming88.
- ACP/Codex: pass `sessions_spawn(runtime="acp")` model and thinking overrides into Codex ACP startup, normalize `openai-codex/*` refs and slash reasoning suffixes, and recognize managed Codex ACP wrapper commands without blocking current `gpt-5.5` sessions. Fixes #40393. (#71643) Thanks @91wan.
- Browser/CDP: make readiness diagnostics use the same discovery-first fallback as reachability for bare `ws://` Browserless and Browserbase CDP URLs. Fixes #69532.
- Browser/CDP: explain that loopback Browserless or other externally managed CDP services need `attachOnly: true` and matching Browserless `EXTERNAL` endpoint when reporting local port ownership conflicts, and fall back to the configured bare WebSocket root when a discovered Browserless endpoint rejects CDP. Fixes #49815.
@@ -129,9 +302,7 @@ Docs: https://docs.openclaw.ai
- Plugins/model defaults: run Skill Workshop review, Active Memory recall, and session-memory slug generation on the configured agent default model instead of the hardcoded OpenAI SDK fallback when hook context lacks model metadata. Fixes #71659.
- Providers/Venice: fill the required DeepSeek V4 `reasoning_content` placeholder for `venice/deepseek-v4-pro` and `venice/deepseek-v4-flash` replay turns without sending native DeepSeek `thinking` controls that Venice rejects. Fixes #71628.
- Browser/existing-session: support per-profile Chrome MCP command/args, map `cdpUrl` to `--browserUrl` or `--wsEndpoint`, and avoid combining endpoint flags with `--userDataDir`. Fixes #47879, #48037, and #62706. Thanks @puneet1409, @zhehao, and @madkow1001.
- Media/plugins: bound MIME sniffing and ZIP archive preflight before handing
untrusted files to `file-type` or `jszip`, reducing parser CPU and memory
exposure for attachments and ClawHub plugin archives. Thanks @vincentkoc.
- Media/plugins: bound MIME sniffing and ZIP archive preflight before handing untrusted files to `file-type` or `jszip`, reducing parser CPU and memory exposure for attachments and ClawHub plugin archives. Thanks @vincentkoc.
- Memory-host SDK: use trusted env-proxy mode for remote embedding and batch HTTP calls only when Undici will proxy that target, preserving SSRF DNS pinning for `ALL_PROXY`-only and `NO_PROXY` bypass cases. Fixes #52162. (#71506) Thanks @DhtIsCoding.
- Gateway/dashboard: render Control UI and WebSocket links with `https://`/`wss://` when `gateway.tls.enabled=true`, including `openclaw gateway status`. Fixes #71494. (#71499) Thanks @deepkilo.
- Agents/OpenAI-compatible: default proxy/local completions tool requests to `tool_choice: "auto"` when tools are present, so providers enter native tool-calling mode instead of replying with plain-text tool directives. (#71472) Thanks @Speed-maker.
@@ -171,12 +342,12 @@ Docs: https://docs.openclaw.ai
- Browser/Linux: detect Chromium-based installs under `/opt/google`, `/opt/brave.com`, `/usr/lib/chromium`, and `/usr/lib/chromium-browser` before asking users to set `browser.executablePath`. (#48563) Thanks @lupuletic.
- Sessions/browser: close tracked browser tabs when idle, daily, `/new`, or `/reset` session rollover archives the previous transcript, preventing tabs from leaking past the old session. Thanks @jakozloski.
- Sessions/forking: fall back to transcript-estimated parent token counts when cached totals are stale or missing, so oversized thread forks start fresh instead of cloning the full parent transcript. Thanks @jalehman.
- OpenAI/Codex: send Codex Responses system prompts through top-level
`instructions` while preserving the existing native Codex payload controls.
- OpenAI/Codex: send Codex Responses system prompts through top-level `instructions` while preserving the existing native Codex payload controls.
- MCP/CLI: retire bundled MCP runtimes at the end of one-shot `openclaw agent` and `openclaw infer model run` gateway/local executions, so repeated scripted runs do not accumulate stdio MCP child processes. Fixes #71457. Thanks @spartoviMD.
- OpenAI/Codex image generation: canonicalize legacy `openai-codex.baseUrl` values such as `https://chatgpt.com/backend-api` to the Codex Responses backend before calling `gpt-image-2`, matching the chat transport. Fixes #71460. Thanks @GodsBoy.
- Control UI: make `/usage` use the fresh context snapshot for context percentage, and include cache-write tokens in the Usage overview cache-hit denominator. Fixes #47885. Thanks @imwyvern and @Ante042.
- GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay so Copilot can validate encrypted reasoning payloads across requests. (#71448) Thanks @a410979729-sys.
- GitHub Copilot: never rewrite connection-bound reasoning item IDs regardless of whether `encrypted_content` is present, fixing a 400 "Encrypted content item_id did not match" error with `gpt-5.3-codex` and future Codex models that fall through to the forward-compat catch-all with `reasoning: false`. Also recognize Codex-named models as reasoning-capable so they inherit the correct capability flags. Refs #68735. Thanks @InvalidPandaa.
- Agents/replies: recover final-answer text when streamed assistant chunks contain only whitespace, preventing completed turns from surfacing as empty-payload errors. Fixes #71454. (#71467) Thanks @Sanjays2402.
- Feishu/TTS: transcode voice-intent MP3 and other audio replies to Ogg/Opus before sending native Feishu audio bubbles, while keeping ordinary MP3 attachments as files. Fixes #61249 and #37868. Thanks @sg1416-zg and @ycjlb2023-peteryi.
- WhatsApp/TTS: transcode MP3/WebM audio, including Microsoft Edge TTS output, to Ogg/Opus before sending PTT voice notes.
@@ -190,6 +361,17 @@ Docs: https://docs.openclaw.ai
- Sessions: make `sessions_spawn(mode="session")` errors name usable alternatives when the current channel cannot bind subagent threads. Fixes #67400. (#67790) Thanks @stainlu.
- Agents/Claude CLI: pass the OpenClaw system prompt through Claude's prompt-file flag so Windows runs avoid argv length failures without changing system prompt semantics. Fixes #69158. (#69211) Thanks @skylee-01, @cassioanorte, @Syu0, and @Stache73.
- Agents/CLI sessions: bind `google-gemini-cli` session auth-epoch to the Google account identity in `~/.gemini/oauth_creds.json`, so Gemini-backed agents resume their conversation after gateway restart instead of minting a fresh session, and stale bindings are invalidated when the authenticated Google account changes. Fixes #70973. (#71076) Thanks @openperf.
- Slack: stop treating user mentions in assistant-authored message edit blocks as sender attribution, preventing edited bot messages from spoofing a mentioned DM user. (#71700) Thanks @vincentkoc.
- Codex: consume unauthorized bound conversation inbound claims before they can fall through to other claim handlers or enqueue Codex turns. (#71702) Thanks @vincentkoc.
- Codex media understanding: require approval-checked app-server image turns while explicitly declining tool, file, permission, and elicitation approval requests for the bounded image worker. (#71703) Thanks @vincentkoc.
- Agents/Claude CLI: allow large live `stream-json` JSONL lines up to the existing per-turn raw limit, preventing large Telegram, WebChat, MCP, and image turns from aborting on the old stdout buffer cap. Fixes #71793, #71080, and #70766. (#71897) Thanks @chacher86, @shivamgrover21, and @tpjordan.
- Agents/Claude CLI: unwrap nested Claude result envelopes in CLI JSON output so delegated agent responses surface as final text instead of raw result JSON. (#66819) Thanks @mraleko.
- Agents/Claude CLI: apply the configured 1M context window override to eligible Claude CLI Opus and Sonnet models when `context1m` is enabled. (#70863) Thanks @bidadh.
- Models/status: report fresh Claude CLI native auth instead of stale stored `anthropic:claude-cli` profile expiry when local credentials are current. Fixes #71256. (#71332) Thanks @matthiasjanke and @neeravmakwana.
- CLI backends: compact OpenClaw transcripts after over-budget CLI turns and reseed fresh CLI sessions from the compacted transcript instead of stale external resume state. Fixes #68329. (#71916) Thanks @obviyus.
- Telegram: keep default tool progress messages visible when answer preview streaming is disabled. (#71825) Thanks @VACInc.
- Configure/models: clear deselected model fallbacks when updating the model picker allowlist, including provider-scoped setup flows. (#71596) Thanks @rubencu.
- Agents/streaming: strip namespaced `<antml:thinking>` reasoning tags from streamed assistant replies before user-visible text is emitted. (#69288) Thanks @xialonglee.
## 2026.4.24
@@ -471,6 +653,7 @@ Docs: https://docs.openclaw.ai
- Slack: route native stream fallback replies through the normal chunked sender so long buffered Slack Connect responses are not dropped or duplicated. (#71124) Thanks @martingarramon.
- WhatsApp: transcribe accepted voice notes before agent dispatch while keeping spoken transcripts out of command authorization. (#64120) Thanks @rogerdigital.
- Plugins/CLI: expose channel plugin CLI descriptors during discovery-mode plugin loads so snapshot registries keep channel commands visible without activating full runtimes. (#71309) Thanks @gumadeiras.
- Matrix: separate recovery-key, backup, and owner-trust diagnostics during E2EE recovery, add recovery-key rotation for backup reset, and cover destructive backup restore paths in QA. (#71311) Thanks @gumadeiras.
- WhatsApp: deliver media generated by tool-result replies while still suppressing text-only tool chatter. (#60968) Thanks @adaclaw.
- Config/agents: accept `agents.list[].contextTokens` in strict config validation so per-agent overrides survive hot reload, letting `/status` reflect the configured model window instead of the 200k fallback. Fixes #70692. (#71247) Thanks @statxc.
- Heartbeat: include async exec completion details in heartbeat prompts so command-finished notifications relay the actual output. (#71213) Thanks @GodsBoy.
@@ -606,9 +789,19 @@ Docs: https://docs.openclaw.ai
### Fixes
- Dependencies: refresh workspace package pins and lockfile entries for AWS SDK,
Anthropic SDK, ACP SDK, Matrix crypto, TypeBox, Vite, tsdown, Slack Bolt,
CopilotKit AIMock, and related bundled plugin packages. Thanks @steipete.
- Gateway/env: import each missing expected login-shell env var independently,
so an existing gateway token no longer prevents `env.shellEnv` from loading
plugin credentials such as `TWILIO_*` from `.profile`. Thanks @steipete.
- macOS/Gateway pairing: silently accept same-host native app
`metadata-upgrade` reconnects, so macOS patch-version changes update paired
metadata instead of spamming security audit warnings and `pairing required`
disconnects. Thanks @steipete.
- CLI/Gateway: wait for one-shot gateway RPC clients to finish WebSocket teardown before the CLI process exits, reducing hangs where commands like `openclaw status` or `openclaw version` could finish their work but stay alive until an external timeout killed them (#70691). Thanks @Takhoffman.
- Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy `off`/`low` fallback behavior to a safe provider-supported `medium` equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make `/status` report the same resolved default as runtime (#70601). Thanks @Takhoffman.
- Gateway/model pricing: fetch OpenRouter and LiteLLM pricing asynchronously at startup and extend catalog fetch timeouts to 30 seconds, reducing noisy timeout warnings during slow upstream responses.
- Gateway/model pricing: extend OpenRouter and LiteLLM catalog fetch timeouts to 60 seconds, reducing noisy timeout warnings during slow upstream responses. Thanks @steipete.
- Agents/failover: classify bare undici transport failures (`terminated`, `UND_ERR_SOCKET`, `UND_ERR_CONNECT_TIMEOUT`, body/header timeouts, aborted streams) and pi-ai's openai-codex `Request failed` sentinel as `timeout`, so Cloudflare 502s with empty bodies and mid-response socket resets actually enter the configured fallback chain instead of surfacing as unclassified errors. Fixes #69368. (#69677) Thanks @sk7n4k3d.
- Providers/Anthropic Vertex: restore ADC-backed model discovery after the lightweight provider-discovery path by resolving emitted discovery entries, exposing synthetic auth on bootstrap discovery, and honoring copied env snapshots when probing the default GCP ADC path. Fixes #65715. (#65716) Thanks @feiskyer.
- Plugins/install: add newly installed plugin ids to an existing `plugins.allow` list before enabling them, so allowlisted configs load installed plugins after restart.

View File

@@ -173,6 +173,7 @@ RUN chown node:node /app
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
COPY --from=runtime-assets --chown=node:node /app/package.json .
COPY --from=runtime-assets --chown=node:node /app/patches ./patches
COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
COPY --from=runtime-assets --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=runtime-assets --chown=node:node /app/skills ./skills

View File

@@ -35,11 +35,18 @@ public struct WakeWordGateMatch: Sendable, Equatable {
public let triggerEndTime: TimeInterval
public let postGap: TimeInterval
public let command: String
public let trigger: String?
public init(triggerEndTime: TimeInterval, postGap: TimeInterval, command: String) {
public init(
triggerEndTime: TimeInterval,
postGap: TimeInterval,
command: String,
trigger: String? = nil)
{
self.triggerEndTime = triggerEndTime
self.postGap = postGap
self.command = command
self.trigger = trigger
}
}
@@ -53,13 +60,17 @@ public enum WakeWordGate {
}
private struct TriggerTokens {
let source: String
let tokens: [String]
}
private struct MatchCandidate {
let index: Int
let endIndex: Int
let tokenCount: Int
let triggerEnd: TimeInterval
let gap: TimeInterval
let trigger: String
}
public static func match(
@@ -87,9 +98,19 @@ public enum WakeWordGate {
let gap = nextToken.start - triggerEnd
if gap < config.minPostTriggerGap { continue }
if let best, i <= best.index { continue }
let endIndex = i + count - 1
if let best {
if endIndex < best.endIndex { continue }
if endIndex == best.endIndex, count <= best.tokenCount { continue }
}
best = MatchCandidate(index: i, triggerEnd: triggerEnd, gap: gap)
best = MatchCandidate(
index: i,
endIndex: endIndex,
tokenCount: count,
triggerEnd: triggerEnd,
gap: gap,
trigger: trigger.source)
}
}
@@ -97,7 +118,11 @@ public enum WakeWordGate {
let command = commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
.trimmingCharacters(in: Self.whitespaceAndPunctuation)
guard command.count >= config.minCommandLength else { return nil }
return WakeWordGateMatch(triggerEndTime: best.triggerEnd, postGap: best.gap, command: command)
return WakeWordGateMatch(
triggerEndTime: best.triggerEnd,
postGap: best.gap,
command: command,
trigger: best.trigger)
}
public static func commandText(
@@ -145,7 +170,7 @@ public enum WakeWordGate {
.map { normalizeToken(String($0)) }
.filter { !$0.isEmpty }
if tokens.isEmpty { continue }
output.append(TriggerTokens(tokens: tokens))
output.append(TriggerTokens(source: tokens.joined(separator: " "), tokens: tokens))
}
return output
}

View File

@@ -47,6 +47,21 @@ import Testing
#expect(match?.command == "do it")
}
@Test func matchPrefersMostSpecificTriggerWhenOverlapping() {
let transcript = "hey clawd do it"
let segments = makeSegments(
transcript: transcript,
words: [
("hey", 0.0, 0.1),
("clawd", 0.2, 0.1),
("do", 0.8, 0.1),
("it", 1.0, 0.1),
])
let config = WakeWordGateConfig(triggers: ["clawd", "hey clawd"], minPostTriggerGap: 0.3)
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
#expect(match?.trigger == "hey clawd")
}
@Test func commandTextHandlesForeignRangeIndices() {
let transcript = "hey clawd do thing"
let other = "do thing"

View File

@@ -2,6 +2,54 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.24</title>
<pubDate>Sat, 25 Apr 2026 19:34:45 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042490</sparkle:version>
<sparkle:shortVersionString>2026.4.24</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.24</h2>
<h3>Highlights</h3>
<ul>
<li>Google Meet joins OpenClaw as a bundled participant plugin, with personal Google auth, Chrome/Twilio realtime sessions, paired-node Chrome support, artifact/attendance exports, and recovery tooling for already-open Meet tabs.</li>
<li>DeepSeek V4 Flash and V4 Pro are in the bundled catalog, V4 Flash is the onboarding default, and DeepSeek thinking/replay behavior is fixed for follow-up tool-call turns.</li>
<li>Talk, Voice Call, and Google Meet can use realtime voice loops that consult the full OpenClaw agent for deeper tool-backed answers.</li>
<li>Browser automation gets coordinate clicks, longer default action budgets, per-profile headless overrides, and steadier tab reuse/recovery.</li>
<li>Plugin and model infrastructure is lighter at startup: static model catalogs, manifest-backed model rows, lazy provider dependencies, and external runtime-dependency repair for packaged installs.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Packaged installs: preserve package-root runtime dependencies and their exported subpaths when bundled plugin runtime mirrors fall back to copying shared chunks, fixing Windows npm updates that could fail to load copied <code>dist</code> modules.</li>
<li>Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing <code>every</code> values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.</li>
<li>Telegram: remove the startup persisted-offset <code>getUpdates</code> preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.</li>
<li>Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.</li>
<li>Browser/aria snapshots: bind <code>format=aria</code> <code>axN</code> refs to live DOM nodes through backend DOM ids when Playwright is available, so follow-up browser actions can use those refs without timing out. (#62434) Thanks @MrKipler.</li>
<li>Telegram: prevent duplicate in-process long pollers for the same bot token and add clearer <code>getUpdates</code> conflict diagnostics for external duplicate pollers. Fixes #56230.</li>
<li>Browser/Linux: detect Chromium-based installs under <code>/opt/google</code>, <code>/opt/brave.com</code>, <code>/usr/lib/chromium</code>, and <code>/usr/lib/chromium-browser</code> before asking users to set <code>browser.executablePath</code>. (#48563) Thanks @lupuletic.</li>
<li>Sessions/browser: close tracked browser tabs when idle, daily, <code>/new</code>, or <code>/reset</code> session rollover archives the previous transcript, preventing tabs from leaking past the old session. Thanks @jakozloski.</li>
<li>Sessions/forking: fall back to transcript-estimated parent token counts when cached totals are stale or missing, so oversized thread forks start fresh instead of cloning the full parent transcript. Thanks @jalehman.</li>
<li>OpenAI/Codex: send Codex Responses system prompts through top-level</li>
</ul>
<code>instructions</code> while preserving the existing native Codex payload controls.
<ul>
<li>MCP/CLI: retire bundled MCP runtimes at the end of one-shot <code>openclaw agent</code> and <code>openclaw infer model run</code> gateway/local executions, so repeated scripted runs do not accumulate stdio MCP child processes. Fixes #71457.</li>
<li>OpenAI/Codex image generation: canonicalize legacy <code>openai-codex.baseUrl</code> values such as <code>https://chatgpt.com/backend-api</code> to the Codex Responses backend before calling <code>gpt-image-2</code>, matching the chat transport. Fixes #71460.</li>
<li>Control UI: make <code>/usage</code> use the fresh context snapshot for context percentage, and include cache-write tokens in the Usage overview cache-hit denominator. Fixes #47885. Thanks @imwyvern and @Ante042.</li>
<li>GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay so Copilot can validate encrypted reasoning payloads across requests. (#71448) Thanks @a410979729-sys.</li>
<li>Agents/replies: recover final-answer text when streamed assistant chunks contain only whitespace, preventing completed turns from surfacing as empty-payload errors. Fixes #71454. (#71467) Thanks @Sanjays2402.</li>
<li>Feishu/TTS: transcode voice-intent MP3 and other audio replies to Ogg/Opus before sending native Feishu audio bubbles, while keeping ordinary MP3 attachments as files. Fixes #61249 and #37868.</li>
<li>Telegram/webhook: acknowledge validated webhook updates before running bot middleware, keeping slow agent turns from tripping Telegram delivery retries while preserving per-chat processing lanes. Fixes #71392. Thanks @joelforsberg46-source.</li>
<li>MCP: retire one-shot embedded bundled MCP runtimes at run end, skip bundle-MCP startup when a runtime tool allowlist cannot reach bundle-MCP tools, and add <code>mcp.sessionIdleTtlMs</code> idle eviction for leaked session runtimes. Fixes #71106, #71110, #70389, and #70808.</li>
<li>MCP/config reload: hot-apply <code>mcp.*</code> changes by disposing cached session MCP runtimes, and dispose bundled MCP runtimes during gateway shutdown so removed <code>mcp.servers</code> entries reap child processes promptly. Fixes #60656.</li>
<li>Gateway/restart continuation: durably hand restart continuations to a session-delivery queue before deleting the restart sentinel, recover queued continuation work after crashy restarts, and fall back to a session-only wake when no channel route survives reboot. (#70780) Thanks @fuller-stack-dev.</li>
<li>Agents/tool-result pruning: harden the tool-result character estimator and context-pruning loops against malformed <code>{ type: "text" }</code> blocks created by void or undefined tool handler results, serializing non-string text payloads for size accounting so they cannot bypass trimming as zero-sized. Fixes #34979. (#51267) Thanks @cgdusek, @alvinttang, and @coffeexcoin.</li>
<li>Daemon/service-env: add Nix Home Manager profile bin directories to generated gateway service PATHs on macOS and Linux, honoring <code>NIX_PROFILES</code> right-to-left precedence and falling back to <code>~/.nix-profile/bin</code> when unset. Fixes #44402. (#59935) Thanks @jerome-benoit.</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.24/OpenClaw-2026.4.24.zip" length="48033180" type="application/octet-stream" sparkle:edSignature="wxOfxadSZ/9iXMitaC6SA9J6YPZC3P2tkeK7HZPHzjUIlzQTvOl7EjR4aRyXzaYt1N1AK5ba+YhuCwEngrTdCQ=="/>
</item>
<item>
<title>2026.4.22</title>
<pubDate>Thu, 23 Apr 2026 15:18:00 +0000</pubDate>
@@ -315,121 +363,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.20/OpenClaw-2026.4.20.zip" length="47535600" type="application/octet-stream" sparkle:edSignature="D7XcNGxmc10IIayYY91RZBoascFSnXyd4dg6cSpC3+PTIwVrWYs/FwSBc/1J+1P53LlnTHKDGQYMkWVNMnRSAQ=="/>
</item>
<item>
<title>2026.4.15</title>
<pubDate>Thu, 16 Apr 2026 23:33:29 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026041590</sparkle:version>
<sparkle:shortVersionString>2026.4.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.15</h2>
<h3>Changes</h3>
<ul>
<li>Anthropic/models: default Anthropic selections, <code>opus</code> aliases, Claude CLI defaults, and bundled image understanding to Claude Opus 4.7.</li>
<li>Google/TTS: add Gemini text-to-speech support to the bundled <code>google</code> plugin, including provider registration, voice selection, WAV reply output, PCM telephony output, and setup/docs guidance. (#67515) Thanks @barronlroth.</li>
<li>Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new <code>models.authStatus</code> gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.</li>
<li>Memory/LanceDB: add cloud storage support to <code>memory-lancedb</code> so durable memory indexes can run on remote object storage instead of local disk only. (#63502) Thanks @rugvedS07.</li>
<li>GitHub Copilot/memory search: add a GitHub Copilot embedding provider for memory search, and expose a dedicated Copilot embedding host helper so plugins can reuse the transport while honoring remote overrides, token refresh, and safer payload validation. (#61718) Thanks @feiskyer and @vincentkoc.</li>
<li>Agents/local models: add experimental <code>agents.defaults.experimental.localModelLean: true</code> to drop heavyweight default tools like <code>browser</code>, <code>cron</code>, and <code>message</code>, reducing prompt size for weaker local-model setups without changing the normal path. (#66495) Thanks @ImLukeF.</li>
<li>Packaging/plugins: localize bundled plugin runtime deps to their owning extensions, trim the published docs payload, and tighten install/package-manager guardrails so published builds stay leaner and core stops carrying extension-owned runtime baggage. (#67099) Thanks @vincentkoc.</li>
<li>QA/Matrix: split Matrix live QA into a source-linked <code>qa-matrix</code> runner and keep repo-private <code>qa-*</code> surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.</li>
<li>Docs/showcase: add a scannable hero, complete section jump links, and a responsive video grid for community examples. (#48493) Thanks @jchopard69.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Gateway/tools: anchor trusted local <code>MEDIA:</code> tool-result passthrough on the exact raw name of this run's registered built-in tools, and reject client tool definitions whose names normalize-collide with a built-in or with another client tool in the same request (<code>400 invalid_request_error</code> on both JSON and SSE paths), so a client-supplied tool named like a built-in can no longer inherit its local-media trust. (#67303)</li>
<li>Agents/replay recovery: classify the provider wording <code>401 input item ID does not belong to this connection</code> as replay-invalid, so users get the existing <code>/new</code> session reset guidance instead of a raw 401-style failure. (#66475) Thanks @dallylee.</li>
<li>Gateway/webchat: enforce localRoots containment on webchat audio embedding path [AI-assisted]. (#67298) Thanks @pgondhi987.</li>
<li>Matrix/pairing: block DM pairing-store entries from authorizing room control commands [AI-assisted]. (#67294) Thanks @pgondhi987.</li>
<li>Docker/build: verify <code>@matrix-org/matrix-sdk-crypto-nodejs</code> native bindings with <code>find</code> under <code>node_modules</code> instead of a hardcoded <code>.pnpm/...</code> path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.</li>
<li>Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring <code>channels.matrix.password</code>, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.</li>
<li>Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with <code>NO_REPLY</code> so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.</li>
<li>Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so <code>OPENCLAW_BUNDLED_PLUGINS_DIR</code> flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.</li>
<li>Packaging/plugins: prune common test/spec cargo from bundled plugin runtime dependencies and fail npm release validation if packaged test cargo reappears, keeping published tarballs leaner without plugin-specific special cases. (#67275) thanks @gumadeiras.</li>
<li>Agents/context + Memory: trim default startup/skills prompt budgets, cap <code>memory_get</code> excerpts by default with explicit continuation metadata, and keep QMD reads aligned with the same bounded excerpt contract so long sessions pull less context by default without losing deterministic follow-up reads.</li>
<li>Matrix/commands: skip DM pairing-store reads on room traffic now that room control-command authorization ignores pairing-store entries, keeping the room path narrower without changing room auth behavior. (#67325) Thanks @gumadeiras.</li>
<li>Memory-core/dreaming: skip dreaming narrative transcripts from session-store metadata before bootstrap records land so dream diary prompt/prose lines do not pollute session ingestion. (#67315) thanks @jalehman.</li>
<li>Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when <code>agents.defaults.contextTokens</code> is the real limit. (#66236) Thanks @ImLukeF.</li>
<li>Dreaming/memory-core: change the default <code>dreaming.storage.mode</code> from <code>inline</code> to <code>separate</code> so Dreaming phase blocks (<code>## Light Sleep</code>, <code>## REM Sleep</code>) land in <code>memory/dreaming/{phase}/YYYY-MM-DD.md</code> instead of being injected into <code>memory/YYYY-MM-DD.md</code>. Daily memory files no longer get dominated by structured candidate output, and the daily-ingestion scanner that already strips dream marker blocks no longer has to compete with hundreds of phase-block lines on every run. Operators who want the previous behavior can opt in by setting <code>plugins.entries.memory-core.config.dreaming.storage.mode: "inline"</code>. (#66412) Thanks @mjamiv.</li>
<li>Control UI/Overview: fix false-positive "missing" alerts on the Model Auth status card for aliased providers, env-backed OAuth with auth.profiles, and unresolvable env SecretRefs. (#67253) Thanks @omarshahine.</li>
<li>Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan.</li>
<li>Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus.</li>
<li>Discord/tool-call text: strip standalone Gemma-style <code><function>...</function></code> tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth.</li>
<li>WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight <code>creds.json</code> writes and falsely restores from backup. (#67464) Thanks @neeravmakwana.</li>
<li>BlueBubbles/catchup: add a per-message retry ceiling (<code>catchup.maxFailureRetries</code>, default 10) so a persistently-failing message with a malformed payload no longer wedges the catchup cursor forever. After N consecutive <code>processMessage</code> failures against the same GUID, catchup logs a WARN, skips that message on subsequent sweeps, and lets the cursor advance past it. Transient failures still retry from the same point as before. Also fixes a lost-update race in the persistent dedupe file lock that silently dropped inbound GUIDs on concurrent writes, a dedupe file naming migration gap on version upgrade, and a balloon-event bypass that let catchup replay debouncer-coalesced events as standalone messages. (#67426, #66870) Thanks @omarshahine.</li>
<li>Ollama/chat: strip the <code>ollama/</code> provider prefix from Ollama chat request model ids so configured refs like <code>ollama/qwen3:14b-q8_0</code> stop 404ing against the Ollama API. (#67457) Thanks @suboss87.</li>
<li>Agents/tools: resolve non-workspace host tilde paths against the OS home directory and keep edit recovery aligned with that same path target, so <code>~/...</code> host edit/write operations stop failing or reading back the wrong file when <code>OPENCLAW_HOME</code> differs. (#62804) Thanks @stainlu.</li>
<li>Speech/TTS: auto-enable the bundled Microsoft and ElevenLabs speech providers, and route generic TTS directive tokens through the explicit or active provider first so overrides like <code>[[tts:speed=1.2]]</code> stop silently landing on the wrong provider. (#62846) Thanks @stainlu.</li>
<li>OpenAI Codex/models: normalize stale native transport metadata in both runtime resolution and discovery/listing so legacy <code>openai-codex</code> rows with missing <code>api</code> or <code>https://chatgpt.com/backend-api/v1</code> self-heal to the canonical Codex transport instead of routing requests through broken HTML/Cloudflare paths, combining the original fixes proposed in #66969 (saamuelng601-pixel) and #67159 (hclsys). (#67635)</li>
<li>Agents/failover: treat HTML provider error pages as upstream transport failures for CDN-style 5xx responses without misclassifying embedded body text as API rate limits, while still preserving auth remediation for HTML 401/403 pages and proxy remediation for HTML 407 pages. (#67642) Thanks @stainlu.</li>
<li>Gateway/skills: bump the cached skills-snapshot version whenever a config write touches <code>skills.*</code> (for example <code>skills.allowBundled</code>, <code>skills.entries.<id>.enabled</code>, or <code>skills.profile</code>). Existing agent sessions persist a <code>skillsSnapshot</code> in <code>sessions.json</code> that reuses the skill list frozen at session creation; without this invalidation, removing a bundled skill from the allowlist left the old snapshot live and the model kept calling the disabled tool, producing <code>Tool <name> not found</code> loops that ran until the embedded-run timeout. (#67401) Thanks @xantorres.</li>
<li>Agents/tool-loop: enable the unknown-tool stream guard by default. Previously <code>resolveUnknownToolGuardThreshold</code> returned <code>undefined</code> unless <code>tools.loopDetection.enabled</code> was explicitly set to <code>true</code>, which left the protection off in the default configuration. A hallucinated or removed tool (for example <code>himalaya</code> after it was dropped from <code>skills.allowBundled</code>) would then loop "Tool X not found" attempts until the full embedded-run timeout. The guard has no false-positive surface because it only triggers on tools that are objectively not registered in the run, so it now stays on regardless of <code>tools.loopDetection.enabled</code> and still accepts <code>tools.loopDetection.unknownToolThreshold</code> as a per-run override (default 10). (#67401) Thanks @xantorres.</li>
<li>TUI/streaming: add a client-side streaming watchdog to <code>tui-event-handlers</code> so the <code>streaming · Xm Ys</code> activity indicator resets to <code>idle</code> after 30s of delta silence on the active run. Guards against lost or late <code>state: "final"</code> chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on <code>streaming</code> indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new <code>streamingWatchdogMs</code> context option (set to <code>0</code> to disable), and the handler now exposes a <code>dispose()</code> that clears the pending timer on shutdown. (#67401) Thanks @xantorres.</li>
<li>Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per <code>(baseUrl, modelKey, contextLength)</code> tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined <code>preload failed</code> log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.</li>
<li>Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as <code>...toolresult1</code> during compaction and retry flows. (#67620) Thanks @stainlu.</li>
<li>Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf</li>
<li>Codex/harness: auto-enable the Codex plugin when <code>codex</code> is selected as an embedded agent harness runtime, including forced default, per-agent, and <code>OPENCLAW_AGENT_RUNTIME</code> paths. (#67474) Thanks @duqaXxX.</li>
<li>OpenAI Codex/CLI: keep resumed <code>codex exec resume</code> runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported <code>--skip-git-repo-check</code> resume arg plus Codex's native <code>sandbox_mode="workspace-write"</code> config override. (#67666) Thanks @plgonzalezrx8.</li>
<li>Codex/app-server: parse Desktop-originated app-server user agents such as <code>Codex Desktop/0.118.0</code>, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.</li>
<li>Cron/announce delivery: keep isolated announce <code>NO_REPLY</code> stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale <code>NO_REPLY</code> text. (#65016) Thanks @BKF-Gitty.</li>
<li>Sessions/Codex: skip redundant <code>delivery-mirror</code> transcript appends only when the latest assistant message has the same visible text, preventing duplicate visible replies on Codex-backed turns without suppressing repeated answers across turns. (#67185) Thanks @andyylin.</li>
<li>Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT.</li>
<li>BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept <code>updated-message</code> webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine.</li>
<li>Agents/skills: sort prompt-facing <code>available_skills</code> entries by skill name after merging sources so <code>skills.load.extraDirs</code> order no longer changes prompt-cache prefixes. (#64198) Thanks @Bartok9.</li>
<li>Agents/OpenAI Responses: add <code>models.providers.*.models.*.compat.supportsPromptCacheKey</code> so OpenAI-compatible proxies that forward <code>prompt_cache_key</code> can keep prompt caching enabled while incompatible endpoints can still force stripping. (#67427) Thanks @damselem.</li>
<li>Agents/context engines: keep loop-hook and final <code>afterTurn</code> prompt-cache touch metadata aligned with the current assistant turn so cache-aware context engines retain accurate cache TTL state during tool loops. (#67767) thanks @jalehman.</li>
<li>Memory/dreaming: strip AI-facing inbound metadata envelopes from session-corpus user turns before normalization so REM topic extraction sees the user's actual message text, including array-shaped split envelopes. (#66548) Thanks @zqchris.</li>
<li>Agents/errors: detect standalone Cloudflare/CDN HTML challenge pages before transport DNS classification so provider block pages no longer appear as local DNS lookup failures. (#67704) Thanks @chris-yyau.</li>
<li>Security/approvals: redact secrets in exec approval prompts so inline approval review can no longer leak credential material in rendered prompt content. (#61077, #64790)</li>
<li>CLI/configure: re-read the persisted config hash after writes so config updates stop failing with stale-hash races. (#64188, #66528)</li>
<li>CLI/update: prune stale packaged <code>dist</code> chunks after npm upgrades and keep downgrade/verify inventory checks compat-safe so global upgrades stop failing on stale chunk imports. (#66959) Thanks @obviyus.</li>
<li>Onboarding/CLI: fix channel-selection crashes on globally installed CLI setups during onboarding. (#66736)</li>
<li>Video generation/live tests: bound provider polling for live video smoke, default to the fast non-FAL text-to-video path, and use a one-second lobster prompt so release validation no longer waits indefinitely on slow provider queues.</li>
<li>Memory-core/QMD <code>memory_get</code>: reject reads of arbitrary workspace markdown paths and only allow canonical memory files (<code>MEMORY.md</code>, <code>memory.md</code>, <code>DREAMS.md</code>, <code>dreams.md</code>, <code>memory/**</code>) plus exact paths of active indexed QMD workspace documents, so the QMD memory backend can no longer be used as a generic workspace-file read shim that bypasses <code>read</code> tool-policy denials. (#66026) Thanks @eleqtrizit.</li>
<li>Cron/agents: forward embedded-run tool policy and internal event params into the attempt layer so <code>--tools</code> allowlists, cron-owned message-tool suppression, explicit message targeting, and command-path internal events all take effect at runtime again. (#62675) Thanks @hexsprite.</li>
<li>Setup/providers: guard preferred-provider lookup during setup so malformed plugin metadata with a missing provider id no longer crashes the wizard with <code>Cannot read properties of undefined (reading 'trim')</code>. (#66649) Thanks @Tianworld.</li>
<li>Matrix/security: normalize sandboxed profile avatar params, preserve <code>mxc://</code> avatar URLs, and surface gmail watcher stop failures during reload. (#64701) Thanks @slepybear.</li>
<li>Telegram/documents: drop leaked binary caption bytes from inbound Telegram text handling so document uploads like <code>.mobi</code> or <code>.epub</code> no longer explode prompt token counts. (#66663) Thanks @joelnishanth.</li>
<li>Gateway/auth: resolve the active gateway bearer per-request on the HTTP server and the HTTP upgrade handler via <code>getResolvedAuth()</code>, mirroring the WebSocket path, so a secret rotated through <code>secrets.reload</code> or config hot-reload stops authenticating on <code>/v1/*</code>, <code>/tools/invoke</code>, plugin HTTP routes, and the canvas upgrade path immediately instead of remaining valid on HTTP until gateway restart. (#66651) Thanks @mmaps.</li>
<li>Agents/compaction: cap the compaction reserve-token floor to the model context window so small-context local models (e.g. Ollama with 16K tokens) no longer trigger context-overflow errors or infinite compaction loops on every prompt. (#65671) Thanks @openperf.</li>
<li>Agents/OpenAI Responses: classify the exact <code>Unknown error (no error details in response)</code> transport failure as failover reason <code>unknown</code> so assistant/model fallback still runs for that no-details failure path. (#65254) Thanks @OpenCodeEngineer.</li>
<li>Models/probe: surface invalid-model probe failures as <code>format</code> instead of <code>unknown</code> in <code>models list --probe</code>, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi.</li>
<li>Agents/failover: classify OpenAI-compatible <code>finish_reason: network_error</code> stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699.</li>
<li>Onboarding/channels: normalize channel setup metadata before discovery and validation so malformed or mixed-shape channel plugin metadata no longer breaks setup and onboarding channel lists. (#66706) Thanks @darkamenosa.</li>
<li>Slack/native commands: fix option menus for slash commands such as <code>/verbose</code> when Slack renders native buttons by giving each button a unique action ID while still routing them through the shared <code>openclaw_cmdarg*</code> listener. Thanks @Wangmerlyn.</li>
<li>Feishu/webhook: harden the webhook transport and card-action replay guards to fail closed on missing <code>encryptKey</code> and blank callback tokens — refuse to start the webhook transport without an <code>encryptKey</code>, reject unsigned requests when no key is present instead of accepting them, and drop blank card-action tokens before the dedupe claim and dispatcher. Defense-in-depth over the already-closed monitor-account layer. (#66707) Thanks @eleqtrizit.</li>
<li>Agents/workspace files: route <code>agents.files.get</code>, <code>agents.files.set</code>, and workspace listing through the shared <code>fs-safe</code> helpers (<code>openFileWithinRoot</code>/<code>readFileWithinRoot</code>/<code>writeFileWithinRoot</code>), reject symlink aliases for allowlisted agent files, and have <code>fs-safe</code> resolve opened-file real paths from the file descriptor before falling back to path-based <code>realpath</code> so a symlink swap between <code>open</code> and <code>realpath</code> can no longer redirect the validated path off the intended inode. (#66636) Thanks @eleqtrizit.</li>
<li>Gateway/MCP loopback: switch the <code>/mcp</code> bearer comparison from plain <code>!==</code> to constant-time <code>safeEqualSecret</code> (matching the convention every other auth surface in the codebase uses), and reject non-loopback browser-origin requests via <code>checkBrowserOrigin</code> before the auth gate runs. Loopback origins (<code>127.0.0.1:*</code>, <code>localhost:*</code>, same-origin) still go through, including the <code>localhost</code>↔<code>127.0.0.1</code> host mismatch that browsers flag as <code>Sec-Fetch-Site: cross-site</code>. (#66665) Thanks @eleqtrizit.</li>
<li>Auto-reply/billing: classify pure billing cooldown fallback summaries from structured fallback reasons so users see billing guidance instead of the generic failure reply. (#66363) Thanks @Rohan5commit.</li>
<li>Agents/fallback: preserve the original prompt body on model fallback retries with session history so the retrying model keeps the active task instead of only seeing a generic continue message. (#66029) Thanks @WuKongAI-CMU.</li>
<li>Reply/secrets: resolve active reply channel/account SecretRefs before reply-run message-action discovery so channel token SecretRefs (for example Discord) do not degrade into discovery-time unresolved-secret failures. (#66796) Thanks @joshavant.</li>
<li>Agents/Anthropic: ignore non-positive Anthropic Messages token overrides and fail locally when no positive token budget remains, so invalid <code>max_tokens</code> values no longer reach the provider API. (#66664) thanks @jalehman</li>
<li>Agents/context engines: preserve prompt-only token counts, not full request totals, when deferred maintenance reuses after-turn runtime context so background compaction bookkeeping matches the active prompt window. (#66820) thanks @jalehman.</li>
<li>BlueBubbles/inbound: add a persistent file-backed GUID dedupe so MessagePoller webhook replays after BB Server restart or reconnect no longer cause the agent to re-reply to already-handled messages. (#19176, #12053, #66816) Thanks @omarshahine.</li>
<li>Secrets/plugins/status: align SecretRef inspect-vs-strict handling across plugin preload, read-only status/agents surfaces, and runtime auth paths so unresolved refs no longer crash read-only CLI flows while runtime-required non-env refs stay strict. (#66818) Thanks @joshavant.</li>
<li>Memory/dreaming: stop ordinary transcripts that merely quote the dream-diary prompt from being classified as internal dreaming runs and silently dropped from session recall ingestion. (#66852) Thanks @gumadeiras.</li>
<li>Telegram/documents: sanitize binary reply context and ZIP-like archive extraction so <code>.epub</code> and <code>.mobi</code> uploads can no longer leak raw binary into prompt context through reply metadata or archive-to-<code>text/plain</code> coercion. (#66877) Thanks @martinfrancois.</li>
<li>Telegram/native commands: restore plugin-registry-backed auto defaults for native commands and native skills so Telegram slash commands keep registering when <code>commands.native</code> and <code>commands.nativeSkills</code> stay on <code>auto</code>. (#66843) Thanks @kashevk0.</li>
<li>OpenRouter/Qwen3: parse <code>reasoning_details</code> stream deltas as thinking content without skipping same-chunk tool calls, so Qwen3 replies no longer fail empty on OpenRouter and mixed reasoning/tool-call chunks still execute normally. (#66905) Thanks @bladin.</li>
<li>BlueBubbles/catchup: replay missed webhook messages after gateway restart via a persistent per-account cursor and <code>/api/v1/message/query?after=<ts></code> pass, so messages delivered while the gateway was down no longer disappear. Uses the existing <code>processMessage</code> path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine.</li>
<li>Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq.</li>
<li>Audio/self-hosted STT: restore <code>models.providers.*.request.allowPrivateNetwork</code> for audio transcription so private or LAN speech-to-text endpoints stop tripping SSRF blocks after the v2026.4.14 regression. (#66692) Thanks @jhsmith409.</li>
<li>Auto-reply/media: allow workspace-rooted absolute media paths in auto-reply send flows so valid local media references no longer fail path validation. (#66689)</li>
<li>WhatsApp/Baileys media upload: harden encrypted upload handling so large outbound media sends avoid buffer spikes and reliability regressions. (#65966) Thanks @frankekn.</li>
<li>QQBot/cron: guard against undefined <code>event.content</code> in <code>parseFaceTags</code> and <code>filterInternalMarkers</code> so cron-triggered agent turns with no content payload no longer crash with <code>TypeError: Cannot read properties of undefined (reading 'startsWith')</code>. (#66302) Thanks @xinmotlanthua.</li>
<li>CLI/plugins: stop <code>--dangerously-force-unsafe-install</code> plugin installs from falling back to hook-pack installs after security scan failures, while still preserving non-security fallback behavior for real hook packs. (#58909) Thanks @hxy91819.</li>
<li>Claude CLI/sessions: classify <code>No conversation found with session ID</code> as <code>session_expired</code> so expired CLI-backed conversations clear the stale binding and recover on the next turn. (#65028) thanks @Ivan-Fn.</li>
<li>Context Engine: gracefully fall back to the legacy engine when a third-party context engine plugin fails at resolution time (unregistered id, factory throw, or contract violation), preventing a full gateway outage on every channel. (#66930) Thanks @openperf.</li>
<li>Control UI/chat: keep optimistic user message cards visible during active sends by deferring same-session history reloads until the active run ends, including aborted and errored runs. (#66997) Thanks @scotthuang and @vincentkoc.</li>
<li>Media/Slack: allow host-local CSV and Markdown uploads only when the fallback buffer actually decodes as text, so real plain-text files work without letting opaque non-text blobs renamed to <code>.csv</code> or <code>.md</code> slip past the host-read guard. (#67047) Thanks @Unayung.</li>
<li>Ollama/onboarding: split setup into <code>Cloud + Local</code>, <code>Cloud only</code>, and <code>Local only</code>, support direct <code>OLLAMA_API_KEY</code> cloud setup without a local daemon, and keep Ollama web search on the local-host path. (#67005) Thanks @obviyus.</li>
<li>Webchat/security: reject remote-host <code>file://</code> URLs in the media embedding path. (#67293) Thanks @pgondhi987.</li>
<li>Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment <code>dailyCount</code> across days instead of stalling at <code>1</code>. (#67091) Thanks @Bartok9.</li>
<li>Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like <code>/usr/bin/whoami</code> no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.</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.15/OpenClaw-2026.4.15.zip" length="47501638" type="application/octet-stream" sparkle:edSignature="JUG3cicpJqCQDvp7VYoN6qBuN4Kn4s0+QQFjlMR69OZlwViLdiStPIHa+1vpuoR4miYhJc9knSDVCFzSfQuYCQ=="/>
</item>
</channel>
</rss>
</rss>

View File

@@ -226,14 +226,15 @@ class TalkModeManager(
// If this is a response we initiated, handle normally below.
// Otherwise, if ttsOnAllResponses, finish streaming TTS on terminal events.
val pending = pendingRunId
if (pending == null || runId != pending) {
val knownRun = pending == runId || hasRunCompletion(runId)
if (!knownRun) {
if (ttsOnAllResponses && state == "final") {
val text = extractTextFromChatEventMessage(obj["message"])
if (!text.isNullOrBlank()) {
playTtsForText(text)
}
}
if (pending == null || runId != pending) return
return
}
Log.d(tag, "chat event arrived runId=$runId state=$state pendingRunId=$pendingRunId")
val terminal =
@@ -539,6 +540,7 @@ class TalkModeManager(
private suspend fun sendChat(message: String, session: GatewaySession): String {
val runId = UUID.randomUUID().toString()
armPendingRun(runId)
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(mainSessionKey.ifBlank { "main" }))
@@ -547,19 +549,29 @@ class TalkModeManager(
put("timeoutMs", JsonPrimitive(30_000))
put("idempotencyKey", JsonPrimitive(runId))
}
val res = session.request("chat.send", params.toString())
val parsed = parseRunId(res) ?: runId
if (parsed != runId) {
pendingRunId = parsed
try {
val res = session.request("chat.send", params.toString())
val parsed = parseRunId(res) ?: runId
if (parsed != runId) {
pendingRunId = parsed
}
return parsed
} catch (err: Throwable) {
clearPendingRun(runId)
throw err
}
return parsed
}
private suspend fun waitForChatFinal(runId: String): Boolean {
pendingFinal?.cancel()
val deferred = CompletableDeferred<Boolean>()
pendingRunId = runId
pendingFinal = deferred
consumeRunCompletion(runId)?.let { return it }
val deferred =
if (pendingRunId == runId) {
pendingFinal ?: armPendingRun(runId)
} else {
armPendingRun(runId)
}
consumeRunCompletion(runId)?.let { return it }
val result =
withContext(Dispatchers.IO) {
@@ -570,11 +582,25 @@ class TalkModeManager(
}
}
if (!result) {
if (!result && pendingRunId == runId) {
clearPendingRun(runId)
}
return result
}
private fun armPendingRun(runId: String): CompletableDeferred<Boolean> {
pendingFinal?.cancel()
val deferred = CompletableDeferred<Boolean>()
pendingRunId = runId
pendingFinal = deferred
return deferred
}
private fun clearPendingRun(runId: String) {
if (pendingRunId == runId) {
pendingFinal = null
pendingRunId = null
}
return result
}
private fun cacheRunCompletion(runId: String, isFinal: Boolean) {
@@ -593,6 +619,12 @@ class TalkModeManager(
}
}
private fun hasRunCompletion(runId: String): Boolean {
synchronized(completedRunsLock) {
return completedRunStates.containsKey(runId)
}
}
private fun consumeRunText(runId: String): String? {
synchronized(completedRunsLock) {
return completedRunTexts.remove(runId)

View File

@@ -5,6 +5,7 @@ import ai.openclaw.app.gateway.DeviceAuthTokenStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -49,6 +50,34 @@ class TalkModeManagerTest {
assertEquals(12L, playbackGeneration(manager).get())
}
@Test
fun duplicateFinalForPendingTalkRunDoesNotStartAllResponseTts() {
val manager = createManager()
val final = CompletableDeferred<Boolean>()
manager.ttsOnAllResponses = true
setPrivateField(manager, "pendingRunId", "run-talk")
setPrivateField(manager, "pendingFinal", final)
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-talk", text = "spoken once"))
assertTrue(final.isCompleted)
assertEquals(0L, playbackGeneration(manager).get())
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-talk", text = "spoken once"))
assertEquals(0L, playbackGeneration(manager).get())
}
@Test
fun nonPendingFinalStillUsesAllResponseTts() {
val manager = createManager()
manager.ttsOnAllResponses = true
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-other", text = "speak this"))
assertEquals(1L, playbackGeneration(manager).get())
}
private fun createManager(): TalkModeManager {
val app = RuntimeEnvironment.getApplication()
val sessionJob = SupervisorJob()
@@ -86,6 +115,22 @@ class TalkModeManagerTest {
field.isAccessible = true
return field.get(target)
}
private fun chatFinalPayload(runId: String, text: String): String {
return """
{
"runId": "$runId",
"sessionKey": "main",
"state": "final",
"message": {
"role": "assistant",
"content": [
{ "type": "text", "text": "$text" }
]
}
}
""".trimIndent()
}
}
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {

View File

@@ -21,6 +21,7 @@ struct SettingsTab: View {
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
@@ -278,6 +279,11 @@ struct SettingsTab: View {
help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in
self.appModel.setTalkEnabled(newValue)
}
Picker("Speech Language", selection: self.$talkSpeechLocale) {
ForEach(TalkSpeechLocale.supportedOptions()) { option in
Text(option.label).tag(option.id)
}
}
self.featureToggle(
"Background Listening",
isOn: self.$talkBackgroundEnabled,

View File

@@ -12,6 +12,7 @@ struct TalkModeGatewayConfigState {
let rawConfigApiKey: String?
let interruptOnSpeech: Bool?
let silenceTimeoutMs: Int
let speechLocaleID: String?
}
enum TalkModeGatewayConfigParser {
@@ -53,6 +54,7 @@ enum TalkModeGatewayConfigParser {
let silenceTimeoutMs = TalkConfigParsing.resolvedSilenceTimeoutMs(
talk,
fallback: defaultSilenceTimeoutMs)
let speechLocaleID = TalkConfigParsing.resolvedSpeechLocaleID(talk)
return TalkModeGatewayConfigState(
activeProvider: activeProvider,
@@ -64,6 +66,7 @@ enum TalkModeGatewayConfigParser {
defaultOutputFormat: defaultOutputFormat,
rawConfigApiKey: rawConfigApiKey,
interruptOnSpeech: interruptOnSpeech,
silenceTimeoutMs: silenceTimeoutMs)
silenceTimeoutMs: silenceTimeoutMs,
speechLocaleID: speechLocaleID)
}
}

View File

@@ -87,6 +87,7 @@ final class TalkModeManager: NSObject {
private var apiKey: String?
private var voiceAliases: [String: String] = [:]
private var interruptOnSpeech: Bool = true
private var gatewaySpeechLocaleID: String?
private var mainSessionKey: String = "main"
private var fallbackVoiceId: String?
private var lastPlaybackWasPCM: Bool = false
@@ -500,12 +501,17 @@ final class TalkModeManager: NSObject {
#endif
self.stopRecognition()
self.speechRecognizer = SFSpeechRecognizer()
let localSpeechLocale = UserDefaults.standard.string(forKey: TalkSpeechLocale.storageKey)
let resolvedSpeech = TalkSpeechLocale.makeRecognizer(
localSelection: localSpeechLocale,
gatewaySelection: self.gatewaySpeechLocaleID)
self.speechRecognizer = resolvedSpeech.recognizer
guard let recognizer = self.speechRecognizer else {
throw NSError(domain: "TalkMode", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Speech recognizer unavailable",
])
}
GatewayDiagnostics.log("talk speech: locale=\(resolvedSpeech.localeID ?? "default")")
self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
self.recognitionRequest?.shouldReportPartialResults = true
@@ -2027,6 +2033,7 @@ extension TalkModeManager {
if let interrupt = parsed.interruptOnSpeech {
self.interruptOnSpeech = interrupt
}
self.gatewaySpeechLocaleID = parsed.speechLocaleID
self.silenceWindow = TimeInterval(parsed.silenceTimeoutMs) / 1000
if parsed.normalizedPayload || parsed.defaultVoiceId != nil || parsed.rawConfigApiKey != nil {
GatewayDiagnostics.log(
@@ -2041,6 +2048,7 @@ extension TalkModeManager {
self.gatewayTalkDefaultModelId = nil
self.gatewayTalkApiKeyConfigured = false
self.gatewayTalkConfigLoaded = false
self.gatewaySpeechLocaleID = nil
self.silenceWindow = TimeInterval(Self.defaultSilenceTimeoutMs) / 1000
}
}

View File

@@ -0,0 +1,100 @@
import Foundation
import OpenClawKit
import Speech
enum TalkSpeechLocale {
static let storageKey = "talk.speechLocale"
static let automaticID = "auto"
static let fallbackLocaleID = "en-US"
struct Option: Identifiable {
let id: String
let label: String
}
static func supportedOptions(
supportedLocales: Set<Locale> = SFSpeechRecognizer.supportedLocales()
) -> [Option] {
var seen = Set<String>()
let dynamic: [Option] = supportedLocales
.compactMap { locale in
let id = self.canonicalID(locale.identifier)
guard seen.insert(id).inserted else { return nil }
return Option(id: id, label: self.friendlyName(for: locale))
}
.sorted { (lhs: Option, rhs: Option) in
lhs.label.localizedCaseInsensitiveCompare(rhs.label) == .orderedAscending
}
return [Option(id: self.automaticID, label: "Automatic")] + dynamic
}
static func resolvedLocaleID(
localSelection: String?,
gatewaySelection: String?,
deviceLocaleID: String = Locale.autoupdatingCurrent.identifier,
fallbackLocaleID: String = Self.fallbackLocaleID,
supportedLocaleIDs: Set<String>
) -> String? {
TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
preferredLocaleIDs: [
TalkConfigParsing.normalizedExplicitSpeechLocaleID(localSelection),
TalkConfigParsing.normalizedExplicitSpeechLocaleID(gatewaySelection),
deviceLocaleID,
],
fallbackLocaleID: fallbackLocaleID,
supportedLocaleIDs: supportedLocaleIDs)
}
static func makeRecognizer(
localSelection: String?,
gatewaySelection: String?,
supportedLocales: Set<Locale> = SFSpeechRecognizer.supportedLocales()
) -> (recognizer: SFSpeechRecognizer?, localeID: String?) {
let supportedIDs = Set(supportedLocales.map(\.identifier))
guard let localeID = self.resolvedLocaleID(
localSelection: localSelection,
gatewaySelection: gatewaySelection,
supportedLocaleIDs: supportedIDs)
else {
let recognizer = SFSpeechRecognizer()
return (recognizer, recognizer?.locale.identifier)
}
if let recognizer = SFSpeechRecognizer(locale: Locale(identifier: localeID)) {
return (recognizer, localeID)
}
let recognizer = SFSpeechRecognizer()
return (recognizer, recognizer?.locale.identifier)
}
static func normalizedExplicitLocaleID(_ raw: String?) -> String? {
TalkConfigParsing.normalizedExplicitSpeechLocaleID(raw, automaticID: self.automaticID)
}
private static func normalizedLocaleID(_ raw: String?) -> String? {
TalkConfigParsing.normalizedSpeechLocaleID(raw)
}
private static func canonicalID(_ raw: String) -> String {
raw.replacingOccurrences(of: "_", with: "-")
}
private static func friendlyName(for locale: Locale) -> String {
let id = self.canonicalID(locale.identifier)
let cleanLocale = Locale(identifier: id)
if let langCode = cleanLocale.language.languageCode?.identifier,
let lang = cleanLocale.localizedString(forLanguageCode: langCode),
let regionCode = cleanLocale.region?.identifier,
let region = cleanLocale.localizedString(forRegionCode: regionCode)
{
return "\(lang) (\(region))"
}
if let langCode = cleanLocale.language.languageCode?.identifier,
let lang = cleanLocale.localizedString(forLanguageCode: langCode)
{
return lang
}
return cleanLocale.localizedString(forIdentifier: id) ?? id
}
}

View File

@@ -47,6 +47,16 @@ private let iOSSilenceTimeoutMs = 900
fallback: iOSSilenceTimeoutMs) == 1500)
}
@Test func readsConfiguredSpeechLocale() {
let talk: [String: Any] = [
"speechLocale": " ru-RU ",
]
#expect(
TalkConfigParsing.resolvedSpeechLocaleID(
TalkConfigParsing.bridgeFoundationDictionary(talk)) == "ru-RU")
}
@Test func defaultsSilenceTimeoutMsWhenMissing() {
#expect(TalkConfigParsing.resolvedSilenceTimeoutMs(nil, fallback: iOSSilenceTimeoutMs) == iOSSilenceTimeoutMs)
}

View File

@@ -0,0 +1,41 @@
import Foundation
import Testing
@testable import OpenClaw
@Suite struct TalkSpeechLocaleTests {
@Test func localSelectionOverridesGatewayConfig() {
let locale = TalkSpeechLocale.resolvedLocaleID(
localSelection: "de-DE",
gatewaySelection: "ru-RU",
deviceLocaleID: "en-US",
supportedLocaleIDs: ["de-DE", "ru-RU", "en-US"])
#expect(locale == "de-DE")
}
@Test func automaticLocalSelectionAllowsGatewayConfig() {
let locale = TalkSpeechLocale.resolvedLocaleID(
localSelection: TalkSpeechLocale.automaticID,
gatewaySelection: "ru_RU",
deviceLocaleID: "en-US",
supportedLocaleIDs: ["ru-RU", "en-US"])
#expect(locale == "ru-RU")
}
@Test func unsupportedConfiguredLocaleFallsBackToDeviceThenEnglish() {
let deviceLocale = TalkSpeechLocale.resolvedLocaleID(
localSelection: "zz-ZZ",
gatewaySelection: nil,
deviceLocaleID: "fr-FR",
supportedLocaleIDs: ["fr-FR", "en-US"])
let english = TalkSpeechLocale.resolvedLocaleID(
localSelection: "zz-ZZ",
gatewaySelection: nil,
deviceLocaleID: "yy-YY",
supportedLocaleIDs: ["en-US"])
#expect(deviceLocale == "fr-FR")
#expect(english == "en-US")
}
}

View File

@@ -1,6 +1,7 @@
import AppKit
import Foundation
import Observation
import OpenClawKit
import ServiceManagement
import SwiftUI
@@ -176,6 +177,23 @@ final class AppState {
}
}
var talkPhaseSoundsEnabled: Bool {
didSet {
self.ifNotPreview {
UserDefaults.standard.set(self.talkPhaseSoundsEnabled, forKey: talkPhaseSoundsEnabledKey)
}
}
}
var talkShiftToStopEnabled: Bool {
didSet {
self.ifNotPreview {
UserDefaults.standard.set(self.talkShiftToStopEnabled, forKey: talkShiftToStopEnabledKey)
Task { TalkSpeechInterruptMonitor.shared.setEnabled(self.talkShiftToStopEnabled && self.talkEnabled) }
}
}
}
/// Gateway-provided UI accent color (hex). Optional; clients provide a default.
var seamColorHex: String?
@@ -309,6 +327,18 @@ final class AppState {
self.voiceWakeTriggersTalkMode = UserDefaults.standard
.object(forKey: voiceWakeTriggersTalkModeKey) as? Bool ?? false
self.talkEnabled = UserDefaults.standard.bool(forKey: talkEnabledKey)
if let storedPhaseSounds = UserDefaults.standard.object(forKey: talkPhaseSoundsEnabledKey) as? Bool {
self.talkPhaseSoundsEnabled = storedPhaseSounds
} else {
self.talkPhaseSoundsEnabled = true
UserDefaults.standard.set(true, forKey: talkPhaseSoundsEnabledKey)
}
if let storedShiftToStop = UserDefaults.standard.object(forKey: talkShiftToStopEnabledKey) as? Bool {
self.talkShiftToStopEnabled = storedShiftToStop
} else {
self.talkShiftToStopEnabled = true
UserDefaults.standard.set(true, forKey: talkShiftToStopEnabledKey)
}
self.seamColorHex = nil
if let storedHeartbeats = UserDefaults.standard.object(forKey: heartbeatsEnabledKey) as? Bool {
self.heartbeatsEnabled = storedHeartbeats
@@ -337,7 +367,8 @@ final class AppState {
if resolvedConnectionMode == .remote,
configRemoteTransport != .direct,
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let host = AppState.remoteHost(from: configRemoteUrl)
let host = AppState.remoteHost(from: configRemoteUrl),
!LoopbackHost.isLoopbackHost(host)
{
self.remoteTarget = "\(NSUserName())@\(host)"
} else {
@@ -406,6 +437,30 @@ final class AppState {
return trimmed
}
private static func sshTunnelGatewayUrl(existingUrl: String?, expectedRemoteHost: String?) -> String {
let fallback = "ws://127.0.0.1:18789"
let trimmed = existingUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty,
let url = URL(string: trimmed),
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
!host.isEmpty
else {
return fallback
}
let preservePort: Bool = if LoopbackHost.isLoopbackHost(host) {
true
} else if let expectedRemoteHost {
OpenClawConfigFile.canonicalHostForComparison(host) ==
OpenClawConfigFile.canonicalHostForComparison(expectedRemoteHost)
} else {
false
}
guard preservePort else { return fallback }
return "ws://127.0.0.1:\(url.port ?? 18789)"
}
private static func updateGatewayString(
_ dictionary: inout [String: Any],
key: String,
@@ -462,17 +517,14 @@ final class AppState {
case .ssh:
changed = Self.updateGatewayString(&remote, key: "transport", value: nil) || changed
if let host = draft.remoteHost {
let existingUrl = (remote["url"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
let port = parsedExisting?.port ?? 18789
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
changed = Self.updateGatewayString(&remote, key: "url", value: desiredUrl) || changed
}
let sanitizedTarget = Self.sanitizeSSHTarget(draft.remoteTarget)
let expectedRemoteHost = CommandResolver.parseSSHTarget(sanitizedTarget)?.host ?? draft.remoteHost
let existingUrl = (remote["url"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let desiredUrl = Self.sshTunnelGatewayUrl(
existingUrl: existingUrl,
expectedRemoteHost: expectedRemoteHost)
changed = Self.updateGatewayString(&remote, key: "url", value: desiredUrl) || changed
changed = Self.updateGatewayString(&remote, key: "sshTarget", value: sanitizedTarget) || changed
changed = Self.updateGatewayString(&remote, key: "sshIdentity", value: draft.remoteIdentity) || changed
}
@@ -540,7 +592,8 @@ final class AppState {
let targetMode = desiredMode ?? self.connectionMode
if targetMode == .remote,
remoteTransport != .direct,
let host = AppState.remoteHost(from: remoteUrl)
let host = AppState.remoteHost(from: remoteUrl),
!LoopbackHost.isLoopbackHost(host)
{
self.updateRemoteTarget(host: host)
}
@@ -778,6 +831,8 @@ extension AppState {
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
state.voicePushToTalkEnabled = false
state.talkEnabled = false
state.talkPhaseSoundsEnabled = true
state.talkShiftToStopEnabled = true
state.iconOverride = .system
state.heartbeatsEnabled = true
state.connectionMode = .local

View File

@@ -24,6 +24,8 @@ let voiceWakeAdditionalLocalesKey = "openclaw.voiceWakeAdditionalLocaleIDs"
let voicePushToTalkEnabledKey = "openclaw.voicePushToTalkEnabled"
let voiceWakeTriggersTalkModeKey = "openclaw.voiceWakeTriggersTalkMode"
let talkEnabledKey = "openclaw.talkEnabled"
let talkPhaseSoundsEnabledKey = "openclaw.talkPhaseSoundsEnabled"
let talkShiftToStopEnabledKey = "openclaw.talkShiftToStopEnabled"
let iconOverrideKey = "openclaw.iconOverride"
let connectionModeKey = "openclaw.connectionMode"
let remoteTargetKey = "openclaw.remoteTarget"

View File

@@ -14,7 +14,8 @@ enum ExecAllowlistMatcher {
if self.matches(pattern: pattern, target: target) { return entry }
} else if pattern != "*",
!ExecApprovalHelpers.patternHasPathSelector(rawExecutable),
self.matchesExecutableBasename(pattern: pattern, resolution: resolution) {
self.matchesExecutableBasename(pattern: pattern, resolution: resolution)
{
return entry
}
case .invalid:

View File

@@ -618,7 +618,8 @@ enum ExecApprovalsStore {
if !ExecApprovalHelpers.patternHasPathSelector(trimmedPattern),
!trimmedResolved.isEmpty,
case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved) {
case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved)
{
return ExecAllowlistEntry(
id: entry.id,
pattern: migratedPattern,

View File

@@ -42,6 +42,7 @@ struct GatewayAgentInvocation {
var channel: GatewayAgentChannel = .last
var timeoutSeconds: Int?
var idempotencyKey: String = UUID().uuidString
var voiceWakeTrigger: String?
}
/// Single, shared Gateway websocket connection for the whole app.
@@ -499,6 +500,10 @@ extension GatewayConnection {
if let timeout = invocation.timeoutSeconds {
params["timeout"] = AnyCodable(timeout)
}
if let trigger = invocation.voiceWakeTrigger {
params["voiceWakeTrigger"] = AnyCodable(
trigger.trimmingCharacters(in: .whitespacesAndNewlines))
}
do {
try await self.requestVoid(method: .agent, params: params)

View File

@@ -1,7 +1,11 @@
import Foundation
import OpenClawDiscovery
import OpenClawKit
@MainActor
enum GatewayDiscoverySelectionSupport {
private static let defaultSshTunnelGatewayUrl = "ws://127.0.0.1:18789"
static func applyRemoteSelection(
gateway: GatewayDiscoveryModel.DiscoveredGateway,
state: AppState)
@@ -13,18 +17,40 @@ enum GatewayDiscoverySelectionSupport {
state.remoteTransport = preferredTransport
}
state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
if preferredTransport == .direct {
state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
} else {
state.remoteUrl = self.sshTunnelGatewayUrl(current: state.remoteUrl)
}
state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
if preferredTransport == .direct {
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
OpenClawConfigFile.setRemoteGatewayUrlString(state.remoteUrl)
}
}
private static func sshTunnelGatewayUrl(current: String) -> String {
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
let url = URL(string: trimmed),
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
!host.isEmpty,
LoopbackHost.isLoopbackHost(host)
else {
return self.defaultSshTunnelGatewayUrl
}
return "ws://127.0.0.1:\(url.port ?? 18789)"
}
static func preferredTransport(
for gateway: GatewayDiscoveryModel.DiscoveredGateway,
current: AppState.RemoteTransport) -> AppState.RemoteTransport

View File

@@ -135,6 +135,10 @@ struct OpenClawOSLogHandler: AppLogLevelBackedHandler {
self.osLogger = os.Logger(subsystem: subsystem, category: category)
}
func log(event: LogEvent) {
self.writeLog(level: event.level, message: event.message, metadata: event.metadata)
}
func log(
level: Logger.Level,
message: Logger.Message,
@@ -143,6 +147,14 @@ struct OpenClawOSLogHandler: AppLogLevelBackedHandler {
file: String,
function: String,
line: UInt)
{
self.writeLog(level: level, message: message, metadata: metadata)
}
private func writeLog(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?)
{
let merged = Self.mergeMetadata(self.metadata, metadata)
let rendered = Self.renderMessage(message, metadata: merged)
@@ -186,6 +198,17 @@ struct OpenClawFileLogHandler: AppLogLevelBackedHandler {
let label: String
var metadata: Logger.Metadata = [:]
func log(event: LogEvent) {
self.writeLog(
level: event.level,
message: event.message,
metadata: event.metadata,
source: event.source,
file: event.file,
function: event.function,
line: event.line)
}
func log(
level: Logger.Level,
message: Logger.Message,
@@ -194,6 +217,25 @@ struct OpenClawFileLogHandler: AppLogLevelBackedHandler {
file: String,
function: String,
line: UInt)
{
self.writeLog(
level: level,
message: message,
metadata: metadata,
source: source,
file: file,
function: function,
line: line)
}
private func writeLog(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt)
{
guard AppLogSettings.fileLoggingEnabled() else { return }
let (subsystem, category) = OpenClawLogging.parseLabel(self.label)

View File

@@ -54,8 +54,15 @@ actor MacNodeBrowserProxy {
func request(paramsJSON: String?) async throws -> String {
let params = try Self.decodeRequestParams(from: paramsJSON)
let request = try Self.makeRequest(params: params, endpoint: self.endpointProvider())
let (data, response) = try await self.performRequest(request)
let endpoint = self.endpointProvider()
let request = try Self.makeRequest(params: params, endpoint: endpoint)
let data: Data
let response: URLResponse
do {
(data, response) = try await self.performRequest(request)
} catch {
throw Self.unavailableError(endpoint: endpoint, cause: error)
}
let http = try Self.requireHTTPResponse(response)
guard (200..<300).contains(http.statusCode) else {
throw NSError(domain: "MacNodeBrowserProxy", code: http.statusCode, userInfo: [
@@ -165,6 +172,19 @@ actor MacNodeBrowserProxy {
return http
}
private static func unavailableError(endpoint: Endpoint, cause: Error) -> NSError {
let url = endpoint.baseURL.absoluteString
let message = """
UNAVAILABLE: macOS app node could not reach the local browser control service at \(url). \
In remote mode, browser control is owned by the CLI node-host; start `openclaw node start` \
on this Mac and target that browser node. Underlying error: \(cause.localizedDescription)
"""
return NSError(domain: "MacNodeBrowserProxy", code: 9, userInfo: [
NSLocalizedDescriptionKey: message,
NSUnderlyingErrorKey: cause,
])
}
private static func httpErrorMessage(statusCode: Int, data: Data) -> String {
if let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as? [String: Any],
let error = object["error"] as? String,

View File

@@ -116,27 +116,40 @@ final class MacNodeModeCoordinator {
}
}
private func currentCaps() -> [String] {
nonisolated static func resolvedCaps(
browserControlEnabled: Bool,
cameraEnabled: Bool,
locationMode: OpenClawLocationMode,
connectionMode: AppState.ConnectionMode) -> [String]
{
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
if OpenClawConfigFile.browserControlEnabled() {
if browserControlEnabled, connectionMode == .local {
caps.append(OpenClawCapability.browser.rawValue)
}
if UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false {
if cameraEnabled {
caps.append(OpenClawCapability.camera.rawValue)
}
let rawLocationMode = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
if OpenClawLocationMode(rawValue: rawLocationMode) != .off {
if locationMode != .off {
caps.append(OpenClawCapability.location.rawValue)
}
return caps
}
private func currentCaps() -> [String] {
let rawLocationMode = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
return Self.resolvedCaps(
browserControlEnabled: OpenClawConfigFile.browserControlEnabled(),
cameraEnabled: UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false,
locationMode: OpenClawLocationMode(rawValue: rawLocationMode) ?? .off,
connectionMode: AppStateStore.shared.connectionMode)
}
private func currentPermissions() async -> [String: Bool] {
let statuses = await PermissionManager.status()
return Dictionary(uniqueKeysWithValues: statuses.map { ($0.key.rawValue, $0.value) })
}
private func currentCommands(caps: [String]) -> [String] {
nonisolated static func resolvedCommands(caps: [String]) -> [String] {
var commands: [String] = [
OpenClawCanvasCommand.present.rawValue,
OpenClawCanvasCommand.hide.rawValue,
@@ -171,6 +184,10 @@ final class MacNodeModeCoordinator {
return commands
}
private func currentCommands(caps: [String]) -> [String] {
Self.resolvedCommands(caps: caps)
}
private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
guard url.scheme?.lowercased() == "wss" else { return nil }
let host = url.host ?? "gateway"

View File

@@ -192,20 +192,17 @@ enum OpenClawConfigFile {
}
static func remoteGatewayPort(matchingHost sshHost: String) -> Int? {
let trimmedSshHost = sshHost.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedSshHost.isEmpty,
guard let normalizedSshHost = canonicalHostForComparison(sshHost),
let url = self.remoteGatewayUrl(),
let port = url.port,
port > 0,
let urlHost = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
!urlHost.isEmpty
let urlHost = url.host,
let normalizedUrlHost = canonicalHostForComparison(urlHost)
else {
return nil
}
let sshKey = Self.hostKey(trimmedSshHost)
let urlKey = Self.hostKey(urlHost)
guard !sshKey.isEmpty, !urlKey.isEmpty, sshKey == urlKey else { return nil }
guard normalizedSshHost == normalizedUrlHost else { return nil }
return port
}
@@ -223,6 +220,16 @@ enum OpenClawConfigFile {
}
}
static func setRemoteGatewayUrlString(_ value: String) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.updateGatewayDict { gateway in
var remote = gateway["remote"] as? [String: Any] ?? [:]
remote["url"] = trimmed
gateway["remote"] = remote
}
}
static func clearRemoteGatewayUrl() {
self.updateGatewayDict { gateway in
guard var remote = gateway["remote"] as? [String: Any] else { return }
@@ -249,15 +256,17 @@ enum OpenClawConfigFile {
return url
}
static func hostKey(_ host: String) -> String {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !trimmed.isEmpty else { return "" }
if trimmed.contains(":") { return trimmed }
let digits = CharacterSet(charactersIn: "0123456789.")
if trimmed.rangeOfCharacter(from: digits.inverted) == nil {
return trimmed
static func canonicalHostForComparison(_ raw: String?) -> String? {
guard var host = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
!host.isEmpty
else {
return nil
}
return trimmed.split(separator: ".").first.map(String.init) ?? trimmed
host = host.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
while host.hasSuffix(".") {
host.removeLast()
}
return host.isEmpty ? nil : host
}
private static func parseConfigData(_ data: Data) -> [String: Any]? {

View File

@@ -150,9 +150,11 @@ final class RemotePortTunnel {
else {
return nil
}
let sshKey = OpenClawConfigFile.hostKey(sshHost)
let urlKey = OpenClawConfigFile.hostKey(host)
guard !sshKey.isEmpty, !urlKey.isEmpty else { return nil }
guard let sshKey = OpenClawConfigFile.canonicalHostForComparison(sshHost),
let urlKey = OpenClawConfigFile.canonicalHostForComparison(host)
else {
return nil
}
guard sshKey == urlKey else {
Self.logger.debug(
"remote url host mismatch sshHost=\(sshHost, privacy: .public) urlHost=\(host, privacy: .public)")

View File

@@ -1,3 +1,4 @@
import AppKit
import Observation
@MainActor
@@ -17,6 +18,10 @@ final class TalkModeController {
} else {
TalkOverlayController.shared.dismiss()
}
TalkSpeechInterruptMonitor.shared.setEnabled(enabled && AppStateStore.shared.talkShiftToStopEnabled)
// Talk Mode and Push-to-Talk share the right Option key disable PTT while Talk Mode is active.
let pttEnabled = !enabled && AppStateStore.shared.voicePushToTalkEnabled
VoicePushToTalkHotkey.shared.setEnabled(pttEnabled)
await TalkModeRuntime.shared.setEnabled(enabled)
// Resume voice wake listener *after* TalkMode audio is fully torn down.
// Check swabbleEnabled (not voiceWakeTriggersTalkMode) so the paused wake listener
@@ -27,8 +32,15 @@ final class TalkModeController {
}
func updatePhase(_ phase: TalkModePhase) {
let previousPhase = self.phase
self.phase = phase
TalkOverlayController.shared.updatePhase(phase)
// Play distinct system sounds for each phase transition.
if phase != previousPhase {
Self.playPhaseSound(phase, previousPhase: previousPhase)
}
let effectivePhase = self.isPaused ? "paused" : phase.rawValue
Task {
await GatewayConnection.shared.talkMode(
@@ -37,6 +49,25 @@ final class TalkModeController {
}
}
private static func playPhaseSound(_ phase: TalkModePhase, previousPhase: TalkModePhase) {
guard AppStateStore.shared.talkPhaseSoundsEnabled else { return }
let soundName: String? = switch phase {
case .thinking:
"Tink" // :
case .speaking:
"Pop" // :
case .listening:
// (speakinglistening):
// (thinkinglistening ):
previousPhase == .speaking ? "Bottle" : "Submarine"
case .idle:
nil
}
if let soundName {
NSSound(named: NSSound.Name(soundName))?.play()
}
}
func updateLevel(_ level: Double) {
TalkOverlayController.shared.updateLevel(level)
}

View File

@@ -11,6 +11,7 @@ struct TalkModeGatewayConfigState {
let outputFormat: String?
let interruptOnSpeech: Bool
let silenceTimeoutMs: Int
let speechLocaleID: String?
let apiKey: String?
let seamColorHex: String?
}
@@ -53,6 +54,7 @@ enum TalkModeGatewayConfigParser {
}
let outputFormat = activeConfig?["outputFormat"]?.stringValue
let interrupt = talk?["interruptOnSpeech"]?.boolValue
let speechLocaleID = TalkConfigParsing.resolvedSpeechLocaleID(talk)
let apiKey = activeConfig?["apiKey"]?.stringValue
let resolvedVoice: String? = if activeProvider == defaultProvider {
(voice?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? voice : nil) ??
@@ -78,6 +80,7 @@ enum TalkModeGatewayConfigParser {
outputFormat: outputFormat,
interruptOnSpeech: interrupt ?? true,
silenceTimeoutMs: silenceTimeoutMs,
speechLocaleID: speechLocaleID,
apiKey: resolvedApiKey,
seamColorHex: rawSeam.isEmpty ? nil : rawSeam)
}
@@ -104,6 +107,7 @@ enum TalkModeGatewayConfigParser {
outputFormat: nil,
interruptOnSpeech: true,
silenceTimeoutMs: defaultSilenceTimeoutMs,
speechLocaleID: nil,
apiKey: resolvedApiKey,
seamColorHex: nil)
}

View File

@@ -70,6 +70,7 @@ actor TalkModeRuntime {
private var defaultOutputFormat: String?
private var interruptOnSpeech: Bool = true
private var activeTalkProvider = TalkModeRuntime.defaultTalkProvider
private var speechLocaleID: String?
private var lastInterruptedAtSeconds: Double?
private var voiceAliases: [String: String] = [:]
private var lastSpokenText: String?
@@ -186,12 +187,23 @@ actor TalkModeRuntime {
self.recognitionGeneration &+= 1
let generation = self.recognitionGeneration
let locale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
self.recognizer = SFSpeechRecognizer(locale: Locale(identifier: locale))
let voiceWakeLocale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
let supportedLocaleIDs = Set(SFSpeechRecognizer.supportedLocales().map(\.identifier))
let localeID = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
preferredLocaleIDs: [
self.speechLocaleID,
voiceWakeLocale,
Locale.autoupdatingCurrent.identifier,
],
supportedLocaleIDs: supportedLocaleIDs)
self.recognizer = localeID
.map { SFSpeechRecognizer(locale: Locale(identifier: $0)) }
?? SFSpeechRecognizer()
guard let recognizer, recognizer.isAvailable else {
self.logger.error("talk recognizer unavailable")
return
}
self.logger.debug("talk recognizer locale=\(recognizer.locale.identifier, privacy: .public)")
let request = SFSpeechAudioBufferRecognitionRequest()
Self.configureRecognitionRequest(request)
@@ -1009,11 +1021,22 @@ extension TalkModeRuntime {
self.defaultOutputFormat = cfg.outputFormat
self.interruptOnSpeech = cfg.interruptOnSpeech
self.activeTalkProvider = cfg.activeProvider
self.silenceWindow = TimeInterval(cfg.silenceTimeoutMs) / 1000
let configuredSilenceMs = cfg.silenceTimeoutMs
let locale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
let isCJKLocale = locale.hasPrefix("ko") || locale.hasPrefix("ja") || locale.hasPrefix("zh")
let effectiveSilenceMs = isCJKLocale ? max(configuredSilenceMs, 2000) : configuredSilenceMs
if isCJKLocale, configuredSilenceMs < 2000 {
self.logger
.info(
"talk CJK locale: silence timeout clamped " +
"\(configuredSilenceMs, privacy: .public)ms -> 2000ms")
}
self.silenceWindow = TimeInterval(effectiveSilenceMs) / 1000
self.speechLocaleID = cfg.speechLocaleID
self.apiKey = cfg.apiKey
let hasApiKey = (cfg.apiKey?.isEmpty == false)
let voiceLabel = (cfg.voiceId?.isEmpty == false) ? cfg.voiceId! : "none"
let modelLabel = (cfg.modelId?.isEmpty == false) ? cfg.modelId! : "none"
let voiceLabel = cfg.voiceId.flatMap { $0.isEmpty ? nil : $0 } ?? "none"
let modelLabel = cfg.modelId.flatMap { $0.isEmpty ? nil : $0 } ?? "none"
self.logger
.info(
"talk config provider=\(cfg.activeProvider, privacy: .public) " +
@@ -1021,7 +1044,8 @@ extension TalkModeRuntime {
"modelId=\(modelLabel, privacy: .public) " +
"apiKey=\(hasApiKey, privacy: .public) " +
"interrupt=\(cfg.interruptOnSpeech, privacy: .public) " +
"silenceTimeoutMs=\(cfg.silenceTimeoutMs, privacy: .public)")
"silenceTimeoutMs=\(cfg.silenceTimeoutMs, privacy: .public) " +
"speechLocale=\(cfg.speechLocaleID ?? "device", privacy: .public)")
}
static func selectTalkProviderConfig(

View File

@@ -0,0 +1,57 @@
import AppKit
import OSLog
/// Monitors right Option key (keyCode 61) to interrupt Talk Mode speech.
/// Independent of Push-to-Talk active whenever Talk Mode is enabled.
final class TalkSpeechInterruptMonitor: @unchecked Sendable {
static let shared = TalkSpeechInterruptMonitor()
private let logger = Logger(subsystem: "ai.openclaw", category: "talk.interrupt")
private var globalMonitor: Any?
private var localMonitor: Any?
func setEnabled(_ enabled: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if enabled {
self.startMonitoring()
} else {
self.stopMonitoring()
}
}
}
private func startMonitoring() {
guard self.globalMonitor == nil, self.localMonitor == nil else { return }
self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.handleFlags(keyCode: event.keyCode, modifierFlags: event.modifierFlags)
}
self.localMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.handleFlags(keyCode: event.keyCode, modifierFlags: event.modifierFlags)
return event
}
self.logger.info("talk interrupt monitor started")
}
private func stopMonitoring() {
if let globalMonitor {
NSEvent.removeMonitor(globalMonitor)
self.globalMonitor = nil
}
if let localMonitor {
NSEvent.removeMonitor(localMonitor)
self.localMonitor = nil
}
self.logger.info("talk interrupt monitor stopped")
}
private func handleFlags(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) {
// Right Option key down (keyCode 61).
guard keyCode == 61, modifierFlags.contains(.option) else { return }
Task { @MainActor in
guard TalkModeController.shared.phase == .speaking else { return }
self.logger.info("right option — interrupting talk mode speech")
TalkModeController.shared.stopSpeaking(reason: .userTap)
}
}
}

View File

@@ -80,6 +80,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
private func updateModifierState(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) {
// assert(Thread.isMainThread) - Removed for Swift 6
// Right Option (keyCode 61) acts as a hold-to-talk modifier.
if keyCode == 61 {
self.optionDown = modifierFlags.contains(.option)

View File

@@ -17,6 +17,7 @@ final class VoiceSessionCoordinator {
var isFinal: Bool
var sendChime: VoiceWakeChime
var autoSendDelay: TimeInterval?
var voiceWakeTrigger: String?
}
private let logger = Logger(subsystem: "ai.openclaw", category: "voicewake.coordinator")
@@ -28,7 +29,8 @@ final class VoiceSessionCoordinator {
source: Source,
text: String,
attributed: NSAttributedString? = nil,
forwardEnabled: Bool = false) -> UUID
forwardEnabled: Bool = false,
voiceWakeTrigger: String? = nil) -> UUID
{
let token = UUID()
self.logger.info("coordinator start token=\(token.uuidString) source=\(source.rawValue) len=\(text.count)")
@@ -40,7 +42,8 @@ final class VoiceSessionCoordinator {
attributed: attributedText,
isFinal: false,
sendChime: .none,
autoSendDelay: nil)
autoSendDelay: nil,
voiceWakeTrigger: voiceWakeTrigger)
self.session = session
VoiceWakeOverlayController.shared.startSession(
token: token,
@@ -63,7 +66,8 @@ final class VoiceSessionCoordinator {
token: UUID,
text: String,
sendChime: VoiceWakeChime,
autoSendAfter: TimeInterval?)
autoSendAfter: TimeInterval?,
voiceWakeTrigger: String? = nil)
{
guard let session, session.token == token else { return }
self.logger
@@ -73,6 +77,9 @@ final class VoiceSessionCoordinator {
self.session?.isFinal = true
self.session?.sendChime = sendChime
self.session?.autoSendDelay = autoSendAfter
if let voiceWakeTrigger {
self.session?.voiceWakeTrigger = voiceWakeTrigger
}
let attributed = VoiceWakeOverlayController.shared.makeAttributed(from: text)
VoiceWakeOverlayController.shared.presentFinal(
@@ -86,15 +93,20 @@ final class VoiceSessionCoordinator {
func sendNow(token: UUID, reason: String = "explicit") {
guard let session, session.token == token else { return }
let text = session.text.trimmingCharacters(in: .whitespacesAndNewlines)
let voiceWakeTrigger = session.voiceWakeTrigger
let sendChime = session.sendChime
guard !text.isEmpty else {
self.logger.info("coordinator sendNow \(reason) empty -> dismiss")
VoiceWakeOverlayController.shared.dismiss(token: token, reason: .empty, outcome: .empty)
self.clearSession()
return
}
VoiceWakeOverlayController.shared.beginSendUI(token: token, sendChime: session.sendChime)
VoiceWakeOverlayController.shared.beginSendUI(token: token, sendChime: sendChime)
Task.detached {
_ = await VoiceWakeForwarder.forward(transcript: text)
_ = await VoiceWakeForwarder.forward(
transcript: text,
options: .init(
voiceWakeTrigger: voiceWakeTrigger))
}
}

View File

@@ -38,6 +38,7 @@ enum VoiceWakeForwarder {
var deliver: Bool = true
var to: String?
var channel: GatewayAgentChannel = .webchat
var voiceWakeTrigger: String?
}
@discardableResult
@@ -53,7 +54,8 @@ enum VoiceWakeForwarder {
thinking: options.thinking,
deliver: deliver,
to: options.to,
channel: options.channel))
channel: options.channel,
voiceWakeTrigger: options.voiceWakeTrigger))
if result.ok {
self.logger.info("voice wake forward ok")

View File

@@ -41,7 +41,11 @@ enum VoiceWakeRecognitionDebugSupport {
minCommandLength: config.minCommandLength,
trimWake: trimWake)
else { return nil }
return WakeWordGateMatch(triggerEndTime: 0, postGap: 0, command: command)
return WakeWordGateMatch(
triggerEndTime: 0,
postGap: 0,
command: command,
trigger: VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers))
}
static func transcriptSummary(

View File

@@ -37,6 +37,7 @@ actor VoiceWakeRuntime {
private var listeningState: ListeningState = .idle
private var overlayToken: UUID?
private var activeTriggerEndTime: TimeInterval?
private var activeTriggerWord: String?
private var scheduledRestartTask: Task<Void, Never>?
private var lastLoggedText: String?
private var lastLoggedAt: Date?
@@ -256,6 +257,7 @@ actor VoiceWakeRuntime {
self.currentConfig = nil
self.listeningState = .idle
self.activeTriggerEndTime = nil
self.activeTriggerWord = nil
self.logger.debug("voicewake runtime stopped")
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "stopped")
@@ -366,7 +368,11 @@ actor VoiceWakeRuntime {
} else {
self.logger.info("voicewake runtime detected len=\(match.command.count)")
}
await self.beginCapture(command: match.command, triggerEndTime: match.triggerEndTime, config: config)
await self.beginCapture(
command: match.command,
triggerEndTime: match.triggerEndTime,
triggerWord: match.trigger,
config: config)
} else if !transcript.isEmpty, update.error == nil {
if self.isTriggerOnly(transcript: transcript, triggers: config.triggers) {
self.preDetectTask?.cancel()
@@ -494,13 +500,33 @@ actor VoiceWakeRuntime {
return
}
self.logger.info("voicewake runtime detected (trigger-only pause)")
await self.beginCapture(command: "", triggerEndTime: nil, config: config)
let matchedTrigger = self.matchedTriggerWord(transcript: lastText, triggers: triggers)
await self.beginCapture(
command: "",
triggerEndTime: nil,
triggerWord: matchedTrigger,
config: config)
}
private func isTriggerOnly(transcript: String, triggers: [String]) -> Bool {
Self.isTriggerOnlyText(transcript: transcript, triggers: triggers)
}
private func matchedTriggerWord(transcript: String, triggers: [String]) -> String? {
Self.matchedTriggerWordText(transcript: transcript, triggers: triggers)
}
private static func isTriggerOnlyText(transcript: String, triggers: [String]) -> Bool {
guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return false }
guard VoiceWakeTextUtils.startsWithTrigger(transcript: transcript, triggers: triggers) else { return false }
return Self.trimmedAfterTrigger(transcript, triggers: triggers).isEmpty
guard
VoiceWakeTextUtils.startsWithTrigger(transcript: transcript, triggers: triggers)
|| VoiceWakeTextUtils.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers)
else { return false }
return self.trimmedAfterTrigger(transcript, triggers: triggers).isEmpty
}
private static func matchedTriggerWordText(transcript: String, triggers: [String]) -> String? {
VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers)
}
private func preDetectSilenceCheck(
@@ -527,10 +553,16 @@ actor VoiceWakeRuntime {
await self.beginCapture(
command: match.command,
triggerEndTime: match.triggerEndTime,
triggerWord: match.trigger,
config: config)
}
private func beginCapture(command: String, triggerEndTime: TimeInterval?, config: RuntimeConfig) async {
private func beginCapture(
command: String,
triggerEndTime: TimeInterval?,
triggerWord: String?,
config: RuntimeConfig) async
{
// When "Trigger Talk Mode" is enabled, skip the capture/overlay flow entirely
// and activate Talk Mode immediately. Talk Mode handles its own STT pipeline.
// Pause the wake listener to avoid two audio pipelines competing on the mic
@@ -545,7 +577,6 @@ actor VoiceWakeRuntime {
await AppStateStore.shared.setTalkEnabled(true)
return
}
self.listeningState = .voiceWake
self.isCapturing = true
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "beginCapture")
@@ -557,6 +588,7 @@ actor VoiceWakeRuntime {
self.heardBeyondTrigger = !command.isEmpty
self.triggerChimePlayed = false
self.activeTriggerEndTime = triggerEndTime
self.activeTriggerWord = triggerWord
self.preDetectTask?.cancel()
self.preDetectTask = nil
self.triggerOnlyTask?.cancel()
@@ -577,7 +609,8 @@ actor VoiceWakeRuntime {
source: .wakeWord,
text: snapshot,
attributed: attributed,
forwardEnabled: true)
forwardEnabled: true,
voiceWakeTrigger: triggerWord)
}
// Keep the "ears" boosted for the capture window so the status icon animates while recording.
@@ -632,7 +665,9 @@ actor VoiceWakeRuntime {
self.lastHeard = nil
self.heardBeyondTrigger = false
self.triggerChimePlayed = false
let triggerWord = self.activeTriggerWord
self.activeTriggerEndTime = nil
self.activeTriggerWord = nil
self.lastTranscript = nil
self.lastTranscriptAt = nil
self.preDetectTask?.cancel()
@@ -653,14 +688,17 @@ actor VoiceWakeRuntime {
token: token,
text: finalTranscript,
sendChime: sendChime,
autoSendAfter: delay)
autoSendAfter: delay,
voiceWakeTrigger: triggerWord)
}
} else if !finalTranscript.isEmpty {
if sendChime != .none {
await MainActor.run { VoiceWakeChimePlayer.play(sendChime, reason: "voicewake.send") }
}
Task.detached {
await VoiceWakeForwarder.forward(transcript: finalTranscript)
await VoiceWakeForwarder.forward(
transcript: finalTranscript,
options: .init(voiceWakeTrigger: triggerWord))
}
}
self.overlayToken = nil
@@ -784,6 +822,14 @@ actor VoiceWakeRuntime {
!self.trimmedAfterTrigger(text, triggers: triggers).isEmpty
}
static func _testIsTriggerOnly(_ text: String, triggers: [String]) -> Bool {
self.isTriggerOnlyText(transcript: text, triggers: triggers)
}
static func _testMatchedTriggerWord(_ text: String, triggers: [String]) -> String? {
self.matchedTriggerWordText(transcript: text, triggers: triggers)
}
static func _testAttributedColor(isFinal: Bool) -> NSColor {
VoiceOverlayTextFormatting.makeAttributed(committed: "sample", volatile: "", isFinal: isFinal)
.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear

View File

@@ -72,6 +72,31 @@ struct VoiceWakeSettings: View {
binding: self.$state.voicePushToTalkEnabled)
.disabled(!voiceWakeSupported)
if self.state.voicePushToTalkEnabled, self.state.talkEnabled {
Text("Push-to-Talk is paused while Talk Mode is active. It resumes when Talk Mode is turned off.")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.leading, 20)
}
SettingsToggleRow(
title: "Play phase-transition sounds",
subtitle: """
Play short system sounds when Talk Mode switches between
listening, thinking, and speaking.
""",
binding: self.$state.talkPhaseSoundsEnabled)
.disabled(!voiceWakeSupported)
SettingsToggleRow(
title: "Press Right Option to stop speech",
subtitle: """
Tap the right Option key to interrupt the assistant while it is
speaking and return to listening.
""",
binding: self.$state.talkShiftToStopEnabled)
.disabled(!voiceWakeSupported)
if !voiceWakeSupported {
Label("Voice Wake requires macOS 26 or newer.", systemImage: "exclamationmark.triangle.fill")
.font(.callout)

View File

@@ -4,6 +4,11 @@ import SwabbleKit
enum VoiceWakeTextUtils {
private static let whitespaceAndPunctuation = CharacterSet.whitespacesAndNewlines
.union(.punctuationCharacters)
.union(.symbols)
private static let wakePrefixFillers: Set<String> = [
"a", "ah", "eh", "er", "erm", "hey", "hmm", "huh", "mhm", "mm", "oh", "uh", "um",
"yo", "", "", "", "", "",
]
typealias TrimWake = (String, [String]) -> String
static func normalizeToken(_ token: String) -> String {
@@ -12,6 +17,104 @@ enum VoiceWakeTextUtils {
.lowercased()
}
private static func normalizedTriggerTokens(_ trigger: String) -> [String] {
trigger
.split(whereSeparator: { $0.isWhitespace })
.map { self.normalizeToken(String($0)) }
.filter { !$0.isEmpty }
}
private static func isASCIIWordScalar(_ scalar: UnicodeScalar) -> Bool {
scalar.isASCII && CharacterSet.alphanumerics.contains(scalar)
}
private static func requiresASCIIWordBoundaries(_ value: String) -> Bool {
value.unicodeScalars.contains(where: self.isASCIIWordScalar)
}
private static func hasASCIIWordBoundaries(
transcript: String,
range: Range<String.Index>,
trigger: String) -> Bool
{
guard self.requiresASCIIWordBoundaries(trigger) else { return true }
if range.lowerBound > transcript.startIndex {
let beforeIndex = transcript.index(before: range.lowerBound)
let beforeScalars = transcript[beforeIndex].unicodeScalars
if beforeScalars.contains(where: self.isASCIIWordScalar) {
return false
}
}
if range.upperBound < transcript.endIndex {
let afterScalars = transcript[range.upperBound].unicodeScalars
if afterScalars.contains(where: self.isASCIIWordScalar) {
return false
}
}
return true
}
private static func bestRawTriggerMatch(
transcript: String,
triggers: [String]) -> (range: Range<String.Index>, normalizedTrigger: String)?
{
var bestMatch: (range: Range<String.Index>, normalizedTrigger: String, tokenCount: Int)?
for trigger in triggers {
let normalizedTokens = self.normalizedTriggerTokens(trigger)
guard !normalizedTokens.isEmpty else { continue }
let rawTrigger = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation)
let tokenCount = normalizedTokens.count
guard !rawTrigger.isEmpty else { continue }
var searchStart = transcript.startIndex
while searchStart < transcript.endIndex,
let range = transcript.range(
of: rawTrigger,
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
range: searchStart..<transcript.endIndex)
{
defer {
searchStart = transcript.index(after: range.lowerBound)
}
guard self.hasASCIIWordBoundaries(
transcript: transcript,
range: range,
trigger: rawTrigger)
else { continue }
if let bestMatch {
if range.lowerBound > bestMatch.range.lowerBound { continue }
if range.lowerBound == bestMatch.range.lowerBound,
tokenCount <= bestMatch.tokenCount
{
continue
}
}
bestMatch = (range, normalizedTokens.joined(separator: " "), tokenCount)
break
}
if let bestMatch,
bestMatch.range.lowerBound == transcript.startIndex,
bestMatch.tokenCount >= tokenCount
{
// Earlier matches take precedence, so once we match from the
// start there is no need to scan later triggers with fewer
// tokens at the same offset.
if bestMatch.tokenCount > tokenCount {
continue
}
}
}
return bestMatch.map { (range: $0.range, normalizedTrigger: $0.normalizedTrigger) }
}
static func startsWithTrigger(transcript: String, triggers: [String]) -> Bool {
let tokens = transcript
.split(whereSeparator: { $0.isWhitespace })
@@ -19,10 +122,7 @@ enum VoiceWakeTextUtils {
.filter { !$0.isEmpty }
guard !tokens.isEmpty else { return false }
for trigger in triggers {
let triggerTokens = trigger
.split(whereSeparator: { $0.isWhitespace })
.map { self.normalizeToken(String($0)) }
.filter { !$0.isEmpty }
let triggerTokens = self.normalizedTriggerTokens(trigger)
guard !triggerTokens.isEmpty, tokens.count >= triggerTokens.count else { continue }
if zip(triggerTokens, tokens.prefix(triggerTokens.count)).allSatisfy({ $0 == $1 }) {
return true
@@ -40,9 +140,55 @@ enum VoiceWakeTextUtils {
guard !transcript.isEmpty else { return nil }
guard !self.normalizeToken(transcript).isEmpty else { return nil }
guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return nil }
guard self.startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil }
guard
self.startsWithTrigger(transcript: transcript, triggers: triggers)
|| self.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers)
else { return nil }
let trimmed = trimWake(transcript, triggers)
guard trimmed.count >= minCommandLength else { return nil }
return trimmed
}
static func hasOnlyFillerBeforeTrigger(transcript: String, triggers: [String]) -> Bool {
guard let match = self.bestRawTriggerMatch(transcript: transcript, triggers: triggers) else { return false }
let prefixTokens = transcript[..<match.range.lowerBound]
.split(whereSeparator: {
$0.isWhitespace || self.whitespaceAndPunctuation.contains($0.unicodeScalars.first!)
})
.map { self.normalizeToken(String($0)) }
.filter { !$0.isEmpty }
return prefixTokens.allSatisfy { self.wakePrefixFillers.contains($0) }
}
static func matchedTriggerWord(transcript: String, triggers: [String]) -> String? {
if let rawMatch = self.bestRawTriggerMatch(transcript: transcript, triggers: triggers) {
return rawMatch.normalizedTrigger
}
let transcriptTokens = transcript
.split(whereSeparator: { $0.isWhitespace })
.map { self.normalizeToken(String($0)) }
.filter { !$0.isEmpty }
guard !transcriptTokens.isEmpty else { return nil }
var bestStartIndex = Int.max
var bestTokenCount = -1
var bestTokens: [String]?
for trigger in triggers {
let triggerTokens = self.normalizedTriggerTokens(trigger)
guard !triggerTokens.isEmpty, transcriptTokens.count >= triggerTokens.count else { continue }
for index in 0...(transcriptTokens.count - triggerTokens.count) {
let candidate = transcriptTokens[index..<(index + triggerTokens.count)]
guard zip(triggerTokens, candidate).allSatisfy({ $0 == $1 }) else { continue }
if index < bestStartIndex || (index == bestStartIndex && triggerTokens.count > bestTokenCount) {
bestStartIndex = index
bestTokenCount = triggerTokens.count
bestTokens = triggerTokens
}
}
}
return bestTokens?.joined(separator: " ")
}
}

View File

@@ -595,11 +595,14 @@ public struct AgentParams: Codable, Sendable {
public let besteffortdeliver: Bool?
public let lane: String?
public let cleanupbundlemcponrunend: Bool?
public let modelrun: Bool?
public let promptmode: AnyCodable?
public let extrasystemprompt: String?
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let voicewaketrigger: String?
public let idempotencykey: String
public let label: String?
@@ -627,11 +630,14 @@ public struct AgentParams: Codable, Sendable {
besteffortdeliver: Bool?,
lane: String?,
cleanupbundlemcponrunend: Bool?,
modelrun: Bool?,
promptmode: AnyCodable?,
extrasystemprompt: String?,
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
voicewaketrigger: String?,
idempotencykey: String,
label: String?)
{
@@ -658,11 +664,14 @@ public struct AgentParams: Codable, Sendable {
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.cleanupbundlemcponrunend = cleanupbundlemcponrunend
self.modelrun = modelrun
self.promptmode = promptmode
self.extrasystemprompt = extrasystemprompt
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.voicewaketrigger = voicewaketrigger
self.idempotencykey = idempotencykey
self.label = label
}
@@ -691,11 +700,14 @@ public struct AgentParams: Codable, Sendable {
case besteffortdeliver = "bestEffortDeliver"
case lane
case cleanupbundlemcponrunend = "cleanupBundleMcpOnRunEnd"
case modelrun = "modelRun"
case promptmode = "promptMode"
case extrasystemprompt = "extraSystemPrompt"
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case voicewaketrigger = "voiceWakeTrigger"
case idempotencykey = "idempotencyKey"
case label
}

View File

@@ -1,3 +1,4 @@
import Foundation
import Testing
@testable import OpenClaw
@@ -36,6 +37,130 @@ struct AppStateRemoteConfigTests {
#expect((remote["token"] as? String) == nil)
}
@Test
func updatedRemoteGatewayConfigPinsLoopbackUrlForSshTransport() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://gateway.example:18789"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
#expect((remote["transport"] as? String) == nil)
#expect(remote["sshTarget"] as? String == "alice@gateway.example")
}
@Test
func updatedRemoteGatewayConfigPreservesCustomLoopbackTunnelPort() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://localhost.:29876"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:29876")
}
@Test
func updatedRemoteGatewayConfigPreservesCustomPortWhenExistingHostMatchesSshTarget() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://gateway.example:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:19999")
}
@Test
func updatedRemoteGatewayConfigDropsCustomPortWhenExistingHostDoesNotMatchSshTarget() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://other-host.example:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
}
@Test
func updatedRemoteGatewayConfigDoesNotPreservePortForHostnamePrefixCollision() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://example.attacker.tld:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "alice@example.com",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
}
@Test
func appStateInitDoesNotInferLoopbackHostIntoRemoteTarget() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [remoteTargetKey: nil])
{
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"url": "ws://127.0.0.1:19999",
],
],
])
let state = AppState(preview: true)
#expect(state.remoteTarget == "")
}
}
@Test
func appStateInitPreservesExistingRemoteTargetWhenRemoteUrlIsLoopback() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [remoteTargetKey: "alice@gateway.example"])
{
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"url": "ws://127.0.0.1:19999",
],
],
])
let state = AppState(preview: true)
#expect(state.remoteTarget == "alice@gateway.example")
}
}
@Test
func syncedGatewayRootPreservesObjectTokenAcrossModeAndTransportChangesWhenUntouched() {
let initialRoot: [String: Any] = [

View File

@@ -6,6 +6,10 @@ import Testing
private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
var state: URLSessionTask.State = .running
var autoRespond = false
private(set) var sentMessages: [URLSessionWebSocketTask.Message] = []
private var sentChallenge = false
private var respondedRequestIds = Set<String>()
func resume() {}
@@ -13,41 +17,90 @@ private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
self.state = .canceling
}
func send(_: URLSessionWebSocketTask.Message) async throws {}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
self.sentMessages.append(message)
}
func receive() async throws -> URLSessionWebSocketTask.Message {
if self.autoRespond {
if !self.sentChallenge {
self.sentChallenge = true
return .string("""
{"type":"event","event":"connect.challenge","payload":{"nonce":"test-nonce"}}
""")
}
if let request = self.latestUnrespondedRequest() {
self.respondedRequestIds.insert(request.id)
if request.method == "connect" {
return .string("""
{"type":"res","id":"\(request.id)","ok":true,"payload":{"type":"hello","protocol":3,"server":{},"features":{},"snapshot":{"presence":[],"health":{},"stateVersion":{"presence":0,"health":0},"uptimeMs":0},"policy":{}}}
""")
}
return .string("""
{"type":"res","id":"\(request.id)","ok":true,"payload":{}}
""")
}
}
throw URLError(.cannotConnectToHost)
}
func receive(completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void) {
completionHandler(.failure(URLError(.cannotConnectToHost)))
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
func makeWebSocketTask(url _: URL) -> WebSocketTaskBox {
WebSocketTaskBox(task: FakeWebSocketTask())
private func latestUnrespondedRequest() -> (id: String, method: String)? {
for message in self.sentMessages.reversed() {
let data: Data?
switch message {
case .string(let text):
data = Data(text.utf8)
case .data(let raw):
data = raw
@unknown default:
data = nil
}
guard let data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let id = json["id"] as? String,
let method = json["method"] as? String,
!self.respondedRequestIds.contains(id)
else {
continue
}
return (id, method)
}
return nil
}
}
private func makeTestGatewayConnection() -> GatewayConnection {
GatewayConnection(
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
let task = FakeWebSocketTask()
func makeWebSocketTask(url _: URL) -> WebSocketTaskBox {
WebSocketTaskBox(task: self.task)
}
}
private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSession) {
let session = FakeWebSocketSession()
let connection = GatewayConnection(
configProvider: {
(url: URL(string: "ws://127.0.0.1:1")!, token: nil, password: nil)
},
sessionBox: WebSocketSessionBox(session: FakeWebSocketSession()))
sessionBox: WebSocketSessionBox(session: session))
return (connection, session)
}
@Suite(.serialized) struct GatewayConnectionControlTests {
@Test func `status fails when process missing`() async {
let connection = makeTestGatewayConnection()
let (connection, _) = makeTestGatewayConnection()
let result = await connection.status()
#expect(result.ok == false)
#expect(result.error != nil)
}
@Test func `reject empty message`() async {
let connection = makeTestGatewayConnection()
let (connection, _) = makeTestGatewayConnection()
let result = await connection.sendAgent(
message: "",
thinking: nil,
@@ -56,4 +109,38 @@ private func makeTestGatewayConnection() -> GatewayConnection {
to: nil)
#expect(result.ok == false)
}
@Test func `send agent keeps empty voice wake trigger field`() async throws {
let (connection, session) = makeTestGatewayConnection()
session.task.autoRespond = true
_ = await connection.sendAgent(GatewayAgentInvocation(
message: "test",
sessionKey: "main",
thinking: nil,
deliver: false,
to: nil,
channel: .last,
timeoutSeconds: nil,
idempotencyKey: "idem-1",
voiceWakeTrigger: " "))
guard let lastMessage = session.task.sentMessages.last else {
Issue.record("expected websocket send payload")
return
}
let payloadData: Data
switch lastMessage {
case .string(let text):
payloadData = Data(text.utf8)
case .data(let data):
payloadData = data
@unknown default:
Issue.record("unexpected websocket message type")
return
}
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
let params = json?["params"] as? [String: Any]
#expect(params?["voiceWakeTrigger"] as? String == "")
}
}

View File

@@ -84,7 +84,35 @@ struct GatewayDiscoverySelectionSupportTests {
state: state)
#expect(state.remoteTransport == .ssh)
#expect(state.remoteUrl == "ws://127.0.0.1:18789")
#expect(CommandResolver.parseSSHTarget(state.remoteTarget)?.host == "nearby-gateway.local")
let configRoot = OpenClawConfigFile.loadDict()
let remote = ((configRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
}
}
@Test func `selecting nearby lan gateway preserves existing ssh tunnel port`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": configPath]) {
let state = AppState(preview: true)
state.remoteTransport = .ssh
state.remoteUrl = "ws://localhost:29876"
GatewayDiscoverySelectionSupport.applyRemoteSelection(
gateway: self.makeGateway(
serviceHost: "nearby-gateway.local",
servicePort: 19999,
stableID: "bonjour|nearby-gateway-custom"),
state: state)
#expect(state.remoteTransport == .ssh)
#expect(state.remoteUrl == "ws://127.0.0.1:29876")
let configRoot = OpenClawConfigFile.loadDict()
let remote = ((configRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
#expect(remote["url"] as? String == "ws://127.0.0.1:29876")
}
}
}

View File

@@ -83,4 +83,28 @@ struct MacNodeBrowserProxyTests {
let arr = try #require(parsed["arr"] as? [Any])
#expect(arr.count == 2)
}
@Test func requestReportsActionableUnavailableWhenControlServiceIsMissing() async throws {
let proxy = MacNodeBrowserProxy(
endpointProvider: {
MacNodeBrowserProxy.Endpoint(
baseURL: URL(string: "http://127.0.0.1:18791")!,
token: nil,
password: nil)
},
performRequest: { _ in
throw URLError(.cannotConnectToHost)
})
do {
_ = try await proxy.request(paramsJSON: #"{"method":"GET","path":"/"}"#)
Issue.record("request should fail when browser control is unreachable")
} catch {
let message = error.localizedDescription
#expect(message.contains("UNAVAILABLE: macOS app node could not reach the local browser control service"))
#expect(message.contains("http://127.0.0.1:18791"))
#expect(message.contains("browser control is owned by the CLI node-host"))
#expect(message.contains("openclaw node start"))
}
}
}

View File

@@ -0,0 +1,32 @@
import Foundation
import OpenClawKit
import Testing
@testable import OpenClaw
struct MacNodeModeCoordinatorTests {
@Test func remoteModeDoesNotAdvertiseBrowserProxy() {
let caps = MacNodeModeCoordinator.resolvedCaps(
browserControlEnabled: true,
cameraEnabled: false,
locationMode: .off,
connectionMode: .remote)
let commands = MacNodeModeCoordinator.resolvedCommands(caps: caps)
#expect(!caps.contains(OpenClawCapability.browser.rawValue))
#expect(!commands.contains(OpenClawBrowserCommand.proxy.rawValue))
#expect(commands.contains(OpenClawCanvasCommand.present.rawValue))
#expect(commands.contains(OpenClawSystemCommand.notify.rawValue))
}
@Test func localModeAdvertisesBrowserProxyWhenEnabled() {
let caps = MacNodeModeCoordinator.resolvedCaps(
browserControlEnabled: true,
cameraEnabled: false,
locationMode: .off,
connectionMode: .local)
let commands = MacNodeModeCoordinator.resolvedCommands(caps: caps)
#expect(caps.contains(OpenClawCapability.browser.rawValue))
#expect(commands.contains(OpenClawBrowserCommand.proxy.rawValue))
}
}

View File

@@ -35,8 +35,30 @@ struct OpenClawConfigFileTests {
])
#expect(OpenClawConfigFile.remoteGatewayPort() == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.ts.net") == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway") == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "GATEWAY.ts.net.") == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway") == nil)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "other.ts.net") == nil)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.attacker.tld") == nil)
}
}
@MainActor
@Test
func `set remote gateway url string replaces scheme`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
],
],
])
OpenClawConfigFile.setRemoteGatewayUrlString("ws://127.0.0.1:18789")
let root = OpenClawConfigFile.loadDict()
let url = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any])?["url"] as? String
#expect(url == "ws://127.0.0.1:18789")
}
}

View File

@@ -25,10 +25,10 @@ struct TalkModeGatewayConfigTests {
"voiceId": "unused-voice",
],
],
"speechLocale": "ru-RU",
]),
],
issues: nil
)
issues: nil)
let parsed = TalkModeGatewayConfigParser.parse(
snapshot: snapshot,
@@ -37,12 +37,12 @@ struct TalkModeGatewayConfigTests {
defaultSilenceTimeoutMs: TalkDefaults.silenceTimeoutMs,
envVoice: "env-voice",
sagVoice: "sag-voice",
envApiKey: "env-key"
)
envApiKey: "env-key")
#expect(parsed.activeProvider == "mlx")
#expect(parsed.modelId == nil)
#expect(parsed.apiKey == nil)
#expect(parsed.voiceId == "unused-voice")
#expect(parsed.speechLocaleID == "ru-RU")
}
}

View File

@@ -35,6 +35,80 @@ struct VoiceWakeRuntimeTests {
#expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
}
@Test func `trigger only allows filler before trigger`() {
let triggers = ["openclaw"]
let text = "uh openclaw"
#expect(VoiceWakeRuntime._testIsTriggerOnly(text, triggers: triggers))
}
@Test func `trigger only rejects trailing wake word mentions in ordinary speech`() {
let triggers = ["openclaw"]
let text = "tell me about openclaw"
#expect(!VoiceWakeRuntime._testIsTriggerOnly(text, triggers: triggers))
}
@Test func `matched trigger finds trigger not at transcript start`() {
let triggers = ["openclaw"]
let text = "uh openclaw"
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "openclaw")
}
@Test func `matched trigger rejects larger word suffix matches`() {
let triggers = ["computer"]
let text = "uh computers"
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == nil)
}
@Test func `matched trigger prefers most specific overlapping phrase`() {
let triggers = ["openclaw", "hey openclaw"]
let text = "hey openclaw"
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "hey openclaw")
}
@Test func `matched trigger handles width insensitive forms without whitespace tokens`() {
let triggers = ["openclaw"]
let text = ""
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "openclaw")
}
@Test func `matched trigger handles chinese forms without whitespace tokens`() {
let triggers = ["小爪"]
let text = "嘿小爪"
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "小爪")
}
@Test func `text only fallback populates matched trigger`() {
let transcript = "hey openclaw do thing"
let config = WakeWordGateConfig(triggers: ["openclaw"], minCommandLength: 1)
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
transcript: transcript,
triggers: ["openclaw"],
config: config,
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
#expect(match?.trigger == "openclaw")
}
@Test func `text only fallback keeps the first trigger phrase when later words match another trigger`() {
let transcript = "openclaw tell me about computer vision"
let config = WakeWordGateConfig(triggers: ["openclaw", "computer"], minCommandLength: 1)
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
transcript: transcript,
triggers: ["openclaw", "computer"],
config: config,
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
#expect(match?.trigger == "openclaw")
}
@Test func `text only fallback rejects filler prefixed larger word suffix matches`() {
let transcript = "uh computers"
let config = WakeWordGateConfig(triggers: ["computer"], minCommandLength: 1)
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
transcript: transcript,
triggers: ["computer"],
config: config,
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
#expect(match == nil)
}
@Test func `trims after chinese trigger keeps post speech`() {
let triggers = ["小爪", "openclaw"]
let text = "嘿 小爪 帮我打开设置"

View File

@@ -56,6 +56,46 @@ public enum TalkConfigParsing {
self.resolvedPositiveInt(talk?["silenceTimeoutMs"], fallback: fallback)
}
public static func normalizedSpeechLocaleID(_ value: String?) -> String? {
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed.replacingOccurrences(of: "_", with: "-")
}
public static func resolvedSpeechLocaleID(
_ talk: [String: AnyCodable]?,
fallback: String? = nil
) -> String? {
self.normalizedSpeechLocaleID(talk?["speechLocale"]?.stringValue)
?? self.normalizedSpeechLocaleID(fallback)
}
public static func normalizedExplicitSpeechLocaleID(
_ value: String?,
automaticID: String = "auto"
) -> String? {
let normalized = self.normalizedSpeechLocaleID(value)
return normalized == automaticID ? nil : normalized
}
public static func resolvedSpeechRecognitionLocaleID(
preferredLocaleIDs: [String?],
fallbackLocaleID: String = "en-US",
supportedLocaleIDs: Set<String>
) -> String? {
let supported = Set(supportedLocaleIDs.compactMap(self.normalizedSpeechLocaleID))
var seen = Set<String>()
let candidates = (preferredLocaleIDs + [fallbackLocaleID])
.compactMap(self.normalizedSpeechLocaleID)
for candidate in candidates {
guard seen.insert(candidate).inserted else { continue }
if supported.isEmpty || supported.contains(candidate) {
return candidate
}
}
return nil
}
private static func normalizedTalkProviderID(_ raw: String?) -> String? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return trimmed.isEmpty ? nil : trimmed

View File

@@ -595,11 +595,14 @@ public struct AgentParams: Codable, Sendable {
public let besteffortdeliver: Bool?
public let lane: String?
public let cleanupbundlemcponrunend: Bool?
public let modelrun: Bool?
public let promptmode: AnyCodable?
public let extrasystemprompt: String?
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let voicewaketrigger: String?
public let idempotencykey: String
public let label: String?
@@ -627,11 +630,14 @@ public struct AgentParams: Codable, Sendable {
besteffortdeliver: Bool?,
lane: String?,
cleanupbundlemcponrunend: Bool?,
modelrun: Bool?,
promptmode: AnyCodable?,
extrasystemprompt: String?,
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
voicewaketrigger: String?,
idempotencykey: String,
label: String?)
{
@@ -658,11 +664,14 @@ public struct AgentParams: Codable, Sendable {
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.cleanupbundlemcponrunend = cleanupbundlemcponrunend
self.modelrun = modelrun
self.promptmode = promptmode
self.extrasystemprompt = extrasystemprompt
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.voicewaketrigger = voicewaketrigger
self.idempotencykey = idempotencykey
self.label = label
}
@@ -691,11 +700,14 @@ public struct AgentParams: Codable, Sendable {
case besteffortdeliver = "bestEffortDeliver"
case lane
case cleanupbundlemcponrunend = "cleanupBundleMcpOnRunEnd"
case modelrun = "modelRun"
case promptmode = "promptMode"
case extrasystemprompt = "extraSystemPrompt"
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case voicewaketrigger = "voiceWakeTrigger"
case idempotencykey = "idempotencyKey"
case label
}

View File

@@ -116,4 +116,21 @@ struct TalkConfigParsingTests {
#expect(TalkConfigParsing.resolvedPositiveInt(AnyCodable(true), fallback: 700) == 700)
#expect(TalkConfigParsing.resolvedPositiveInt(AnyCodable("1500"), fallback: 700) == 700)
}
@Test func resolvesSpeechLocaleID() {
#expect(TalkConfigParsing.resolvedSpeechLocaleID(["speechLocale": AnyCodable(" ru_RU ")]) == "ru-RU")
#expect(TalkConfigParsing.resolvedSpeechLocaleID(["speechLocale": AnyCodable("")], fallback: "en-US") == "en-US")
}
@Test func resolvesSpeechRecognitionLocaleFromSupportedFallbacks() {
let locale = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
preferredLocaleIDs: ["zz-ZZ", "fr-FR"],
supportedLocaleIDs: ["fr-FR", "en-US"])
let fallback = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
preferredLocaleIDs: ["zz-ZZ", "yy-YY"],
supportedLocaleIDs: ["en-US"])
#expect(locale == "fr-FR")
#expect(fallback == "en-US")
}
}

View File

@@ -6,6 +6,9 @@ services:
TERM: xterm-256color
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}
# Docker bridge networks usually do not carry mDNS multicast reliably.
# Set OPENCLAW_DISABLE_BONJOUR=0 only on host/macvlan/mDNS-capable networks.
OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-1}
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-}
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}

View File

@@ -1,4 +1,4 @@
6ed33ef102e7c92816243bfabc3626222a679c3270c12ec5ea47b28b66204b3b config-baseline.json
f86cb4d57ec1f5fd75008be0ab86151194945eb013a47ab4bdeaddafd3780da7 config-baseline.core.json
f1eefb91a486188915373b09199959f0f1a7cd01dc75ef923832741f72a12543 config-baseline.json
9f0e386d5118cbca785a2e8e9c8b170d844faf1b7ef5e82e6b15d9e1c39f3796 config-baseline.core.json
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
7825b56a5b3fcdbe2e09ef8fe5d9f12ac3598435afebe20413051e45b0d1968e config-baseline.plugin.json
a5479c182ec987bb21e814b8a4e7b3bda7190ae5c2b35fd5ca403dfa48afa115 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
f813474b1623f06e1465daacd56db970e8e92ab1be122faee0fa2a1dc2d4fc43 plugin-sdk-api-baseline.json
b3ea88c0c9b4cf6d9a46f0d34149063303853e78ef9708224608e4da79b23190 plugin-sdk-api-baseline.jsonl
947221d62a0eb0b66250fba2b011ca28a11cb1058bc542b9c155d55479f15935 plugin-sdk-api-baseline.json
0d750f785adbe4d90f209842ed9297476669dd62f7be81fa41e06b6736cc2aaf plugin-sdk-api-baseline.jsonl

View File

@@ -11,6 +11,14 @@
"source": "OpenAI provider",
"target": "OpenAI provider"
},
{
"source": "Azure Speech",
"target": "Azure Speech"
},
{
"source": "Azure Speech provider",
"target": "Azure Speech provider"
},
{
"source": "Status",
"target": "Status"
@@ -111,6 +119,10 @@
"source": "BytePlus (International)",
"target": "BytePlus国际版"
},
{
"source": "Volcengine TTS HTTP API",
"target": "Volcengine TTS HTTP API"
},
{
"source": "Amazon Bedrock Mantle",
"target": "Amazon Bedrock Mantle"

View File

@@ -5,29 +5,37 @@ read_when:
- Wiring external triggers (webhooks, Gmail) into OpenClaw
- Deciding between heartbeat and cron for scheduled tasks
title: "Scheduled tasks"
sidebarTitle: "Scheduled tasks"
---
Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.
## Quick start
```bash
# Add a one-shot reminder
openclaw cron add \
--name "Reminder" \
--at "2026-02-01T16:00:00Z" \
--session main \
--system-event "Reminder: check the cron docs draft" \
--wake now \
--delete-after-run
# Check your jobs
openclaw cron list
openclaw cron show <job-id>
# See run history
openclaw cron runs --id <job-id>
```
<Steps>
<Step title="Add a one-shot reminder">
```bash
openclaw cron add \
--name "Reminder" \
--at "2026-02-01T16:00:00Z" \
--session main \
--system-event "Reminder: check the cron docs draft" \
--wake now \
--delete-after-run
```
</Step>
<Step title="Check your jobs">
```bash
openclaw cron list
openclaw cron show <job-id>
```
</Step>
<Step title="See run history">
```bash
openclaw cron runs --id <job-id>
```
</Step>
</Steps>
## How cron works
@@ -38,18 +46,13 @@ openclaw cron runs --id <job-id>
- All cron executions create [background task](/automation/tasks) records.
- One-shot jobs (`--at`) auto-delete after success by default.
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.
- Isolated cron runs also guard against stale acknowledgement replies. If the
first result is just an interim status update (`on it`, `pulling everything
together`, and similar hints) and no descendant subagent run is still
responsible for the final answer, OpenClaw re-prompts once for the actual
result before delivery.
- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
<a id="maintenance"></a>
Task reconciliation for cron is runtime-owned: an active cron task stays live while the
cron runtime still tracks that job as running, even if an old child session row still exists.
Once the runtime stops owning the job and the 5-minute grace window expires, maintenance can
mark the task `lost`.
<Note>
Task reconciliation for cron is runtime-owned: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance can mark the task `lost`.
</Note>
## Schedule types
@@ -84,35 +87,46 @@ This fires ~56 times per month instead of 01 times per month. OpenClaw use
| Current session | `current` | Bound at creation time | Context-aware recurring work |
| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history |
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
<AccordionGroup>
<Accordion title="Main session vs isolated vs custom">
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). Those system events do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
</Accordion>
<Accordion title="What 'fresh session' means for isolated jobs">
For isolated jobs, "fresh session" means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
</Accordion>
<Accordion title="Runtime cleanup">
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
</Accordion>
<Accordion title="Subagent and Discord delivery">
When isolated cron runs orchestrate subagents, delivery also prefers the final descendant output over stale parent interim text. If descendants are still running, OpenClaw suppresses that partial parent update instead of announcing it.
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text payloads and the final answer. Media and structured Discord payloads are still delivered as separate payloads so attachments and components are not dropped.
When isolated cron runs orchestrate subagents, delivery also prefers the final
descendant output over stale parent interim text. If descendants are still
running, OpenClaw suppresses that partial parent update instead of announcing it.
For text-only Discord announce targets, OpenClaw sends the canonical final
assistant text once instead of replaying both streamed/intermediate text payloads
and the final answer. Media and structured Discord payloads are still delivered
as separate payloads so attachments and components are not dropped.
</Accordion>
</AccordionGroup>
### Payload options for isolated jobs
- `--message`: prompt text (required for isolated)
- `--model` / `--thinking`: model and thinking level overrides
- `--light-context`: skip workspace bootstrap file injection
- `--tools exec,read`: restrict which tools the job can use
<ParamField path="--message" type="string" required>
Prompt text (required for isolated).
</ParamField>
<ParamField path="--model" type="string">
Model override; uses the selected allowed model for the job.
</ParamField>
<ParamField path="--thinking" type="string">
Thinking level override.
</ParamField>
<ParamField path="--light-context" type="boolean">
Skip workspace bootstrap file injection.
</ParamField>
<ParamField path="--tools" type="string">
Restrict which tools the job can use, for example `--tools exec,read`.
</ParamField>
`--model` uses the selected allowed model for that job. If the requested model
is not allowed, cron logs a warning and falls back to the job's agent/default
model selection instead. Configured fallback chains still apply, but a plain
model override with no explicit per-job fallback list no longer appends the
agent primary as a hidden extra retry target.
`--model` uses the selected allowed model for that job. If the requested model is not allowed, cron logs a warning and falls back to the job's agent/default model selection instead. Configured fallback chains still apply, but a plain model override with no explicit per-job fallback list no longer appends the agent primary as a hidden extra retry target.
Model-selection precedence for isolated jobs is:
@@ -121,16 +135,9 @@ Model-selection precedence for isolated jobs is:
3. User-selected stored cron session model override
4. Agent/default model selection
Fast mode follows the resolved live selection too. If the selected model config
has `params.fastMode`, isolated cron uses that by default. A stored session
`fastMode` override still wins over config in either direction.
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction.
If an isolated run hits a live model-switch handoff, cron retries with the
switched provider/model and persists that live selection for the active run
before retrying. When the switch also carries a new auth profile, cron persists
that auth profile override for the active run too. Retries are bounded: after
the initial attempt plus 2 switch retries, cron aborts instead of looping
forever.
If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever.
## Delivery and output
@@ -140,13 +147,11 @@ forever.
| `webhook` | POST finished event payload to a URL |
| `none` | No runner fallback delivery |
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`).
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
For isolated jobs, chat delivery is shared. If a chat route is available, the
agent can use the `message` tool even when the job uses `--no-deliver`. If the
agent sends to the configured/current target, OpenClaw skips the fallback
announce. Otherwise `announce`, `webhook`, and `none` only control what the
runner does with the final reply after the agent turn.
For isolated jobs, chat delivery is shared. If a chat route is available, the agent can use the `message` tool even when the job uses `--no-deliver`. If the agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise `announce`, `webhook`, and `none` only control what the runner does with the final reply after the agent turn.
When an agent creates an isolated reminder from an active chat, OpenClaw stores the preserved live delivery target for the fallback announce route. Internal session keys may be lowercase; provider delivery targets are not reconstructed from those keys when current chat context is available.
Failure notifications follow a separate destination path:
@@ -157,44 +162,44 @@ Failure notifications follow a separate destination path:
## CLI examples
One-shot reminder (main session):
```bash
openclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
```
Recurring isolated job with delivery:
```bash
openclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--announce \
--channel slack \
--to "channel:C1234567890"
```
Isolated job with model and thinking override:
```bash
openclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--announce
```
<Tabs>
<Tab title="One-shot reminder">
```bash
openclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
```
</Tab>
<Tab title="Recurring isolated job">
```bash
openclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--announce \
--channel slack \
--to "channel:C1234567890"
```
</Tab>
<Tab title="Model and thinking override">
```bash
openclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--announce
```
</Tab>
</Tabs>
## Webhooks
@@ -219,52 +224,61 @@ Every request must include the hook token via header:
Query-string tokens are rejected.
### POST /hooks/wake
<AccordionGroup>
<Accordion title="POST /hooks/wake">
Enqueue a system event for the main session:
Enqueue a system event for the main session:
```bash
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
```
```bash
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
```
<ParamField path="text" type="string" required>
Event description.
</ParamField>
<ParamField path="mode" type="string" default="now">
`now` or `next-heartbeat`.
</ParamField>
- `text` (required): event description
- `mode` (optional): `now` (default) or `next-heartbeat`
</Accordion>
<Accordion title="POST /hooks/agent">
Run an isolated agent turn:
### POST /hooks/agent
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.4"}'
```
Run an isolated agent turn:
Fields: `message` (required), `name`, `agentId`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`.
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.4"}'
```
</Accordion>
<Accordion title="Mapped hooks (POST /hooks/<name>)">
Custom hook names are resolved via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms.
</Accordion>
</AccordionGroup>
Fields: `message` (required), `name`, `agentId`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`.
<Warning>
Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
### Mapped hooks (POST /hooks/\<name\>)
Custom hook names are resolved via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms.
### Security
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
- Use a dedicated hook token; do not reuse gateway auth tokens.
- Keep `hooks.path` on a dedicated subpath; `/` is rejected.
- Set `hooks.allowedAgentIds` to limit explicit `agentId` routing.
- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions.
- If you enable `hooks.allowRequestSessionKey`, also set `hooks.allowedSessionKeyPrefixes` to constrain allowed session key shapes.
- Hook payloads are wrapped with safety boundaries by default.
</Warning>
## Gmail PubSub integration
Wire Gmail inbox triggers to OpenClaw via Google PubSub.
**Prerequisites**: `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
<Note>
**Prerequisites:** `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
</Note>
### Wizard setup (recommended)
@@ -280,31 +294,34 @@ When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts `
### Manual one-time setup
1. Select the GCP project that owns the OAuth client used by `gog`:
<Steps>
<Step title="Select the GCP project">
Select the GCP project that owns the OAuth client used by `gog`:
```bash
gcloud auth login
gcloud config set project <project-id>
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
```
```bash
gcloud auth login
gcloud config set project <project-id>
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
```
2. Create topic and grant Gmail push access:
```bash
gcloud pubsub topics create gog-gmail-watch
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
```
3. Start the watch:
```bash
gog gmail watch start \
--account openclaw@gmail.com \
--label INBOX \
--topic projects/<project-id>/topics/gog-gmail-watch
```
</Step>
<Step title="Create topic and grant Gmail push access">
```bash
gcloud pubsub topics create gog-gmail-watch
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
```
</Step>
<Step title="Start the watch">
```bash
gog gmail watch start \
--account openclaw@gmail.com \
--label INBOX \
--topic projects/<project-id>/topics/gog-gmail-watch
```
</Step>
</Steps>
### Gmail model override
@@ -348,16 +365,14 @@ openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --mes
openclaw cron edit <jobId> --clear-agent
```
<Note>
Model override note:
- `openclaw cron add|edit --model ...` changes the job's selected model.
- If the model is allowed, that exact provider/model reaches the isolated agent
run.
- If it is not allowed, cron warns and falls back to the job's agent/default
model selection.
- Configured fallback chains still apply, but a plain `--model` override with
no explicit per-job fallback list no longer falls through to the agent
primary as a silent extra retry target.
- If the model is allowed, that exact provider/model reaches the isolated agent run.
- If it is not allowed, cron warns and falls back to the job's agent/default model selection.
- Configured fallback chains still apply, but a plain `--model` override with no explicit per-job fallback list no longer falls through to the agent primary as a silent extra retry target.
</Note>
## Configuration
@@ -379,17 +394,21 @@ Model override note:
}
```
The runtime state sidecar is derived from `cron.store`: a `.json` store such as
`~/clawd/cron/jobs.json` uses `~/clawd/cron/jobs-state.json`, while a store path
without a `.json` suffix appends `-state.json`.
The runtime state sidecar is derived from `cron.store`: a `.json` store such as `~/clawd/cron/jobs.json` uses `~/clawd/cron/jobs-state.json`, while a store path without a `.json` suffix appends `-state.json`.
Disable cron: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`.
**One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately.
<AccordionGroup>
<Accordion title="Retry behavior">
**One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately.
**Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.
**Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.
**Maintenance**: `cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.maxBytes` / `cron.runLog.keepLines` auto-prune run-log files.
</Accordion>
<Accordion title="Maintenance">
`cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.maxBytes` / `cron.runLog.keepLines` auto-prune run-log files.
</Accordion>
</AccordionGroup>
## Troubleshooting
@@ -406,30 +425,32 @@ openclaw logs --follow
openclaw doctor
```
### Cron not firing
- Check `cron.enabled` and `OPENCLAW_SKIP_CRON` env var.
- Confirm the Gateway is running continuously.
- For `cron` schedules, verify timezone (`--tz`) vs the host timezone.
- `reason: not-due` in run output means manual run was checked with `openclaw cron run <jobId> --due` and the job was not due yet.
### Cron fired but no delivery
- Delivery mode `none` means no runner fallback send is expected. The agent can
still send directly with the `message` tool when a chat route is available.
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`),
OpenClaw suppresses direct outbound delivery and also suppresses the fallback
queued summary path, so nothing is posted back to chat.
- If the agent should message the user itself, check that the job has a usable
route (`channel: "last"` with a previous chat, or an explicit channel/target).
### Timezone gotchas
- Cron without `--tz` uses the gateway host timezone.
- `at` schedules without timezone are treated as UTC.
- Heartbeat `activeHours` uses configured timezone resolution.
<AccordionGroup>
<Accordion title="Cron not firing">
- Check `cron.enabled` and `OPENCLAW_SKIP_CRON` env var.
- Confirm the Gateway is running continuously.
- For `cron` schedules, verify timezone (`--tz`) vs the host timezone.
- `reason: not-due` in run output means manual run was checked with `openclaw cron run <jobId> --due` and the job was not due yet.
</Accordion>
<Accordion title="Cron fired but no delivery">
- Delivery mode `none` means no runner fallback send is expected. The agent can still send directly with the `message` tool when a chat route is available.
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can fail because Matrix room IDs are case-sensitive. Edit the job to the exact `!room:server` or `room:!room:server` value from Matrix.
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`), OpenClaw suppresses direct outbound delivery and also suppresses the fallback queued summary path, so nothing is posted back to chat.
- If the agent should message the user itself, check that the job has a usable route (`channel: "last"` with a previous chat, or an explicit channel/target).
</Accordion>
<Accordion title="Cron or heartbeat appears to prevent /new-style rollover">
- Daily and idle reset freshness is not based on `updatedAt`; see [Session management](/concepts/session#session-lifecycle).
- Cron wakeups, heartbeat runs, exec notifications, and gateway bookkeeping may update the session row for routing/status, but they do not extend `sessionStartedAt` or `lastInteractionAt`.
- For legacy rows created before those fields existed, OpenClaw can recover `sessionStartedAt` from the transcript JSONL session header when the file is still available. Legacy idle rows without `lastInteractionAt` use that recovered start time as their idle baseline.
</Accordion>
<Accordion title="Timezone gotchas">
- Cron without `--tz` uses the gateway host timezone.
- `at` schedules without timezone are treated as UTC.
- Heartbeat `activeHours` uses configured timezone resolution.
</Accordion>
</AccordionGroup>
## Related

View File

@@ -126,6 +126,11 @@ Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (pu
**Compaction events**: `session:compact:before` includes `messageCount`, `tokenCount`. `session:compact:after` adds `compactedCount`, `summaryLength`, `tokensBefore`, `tokensAfter`.
`command:stop` observes the user issuing `/stop`; it is cancellation/command
lifecycle, not an agent-finalization gate. Plugins that need to inspect a
natural final answer and ask the agent for one more pass should use the typed
plugin hook `before_agent_finalize` instead. See [Plugin hooks](/plugins/hooks).
## Hook discovery
Hooks are discovered from these directories, in order of increasing override precedence:

View File

@@ -93,7 +93,7 @@ See [Hooks](/automation/hooks).
### Heartbeat
Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records. Use `HEARTBEAT.md` for a small checklist, or a `tasks:` block when you want due-only periodic checks inside heartbeat itself. Empty heartbeat files skip as `empty-heartbeat-file`; due-only task mode skips as `no-tasks-due`.
Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records and do not extend daily/idle session reset freshness. Use `HEARTBEAT.md` for a small checklist, or a `tasks:` block when you want due-only periodic checks inside heartbeat itself. Empty heartbeat files skip as `empty-heartbeat-file`; due-only task mode skips as `no-tasks-due`.
See [Heartbeat](/gateway/heartbeat).

View File

@@ -5,12 +5,14 @@ read_when:
- Debugging delivery failures for detached agent runs
- Understanding how background runs relate to sessions, cron, and heartbeat
title: "Background tasks"
sidebarTitle: "Background tasks"
---
> **Looking for scheduling?** See [Automation & Tasks](/automation) for choosing the right mechanism. This page covers **tracking** background work, not scheduling it.
<Note>
Looking for scheduling? See [Automation & Tasks](/automation) for choosing the right mechanism. This page covers **tracking** background work, not scheduling it.
</Note>
Background tasks track work that runs **outside your main conversation session**:
ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations.
Background tasks track work that runs **outside your main conversation session**: ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations.
Tasks do **not** replace sessions, cron jobs, or heartbeats — they are the **activity ledger** that records what detached work happened, when, and whether it succeeded.
@@ -24,48 +26,63 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
- ACP, subagents, all cron jobs, and CLI operations create tasks. Heartbeat turns do not.
- Each task moves through `queued → running → terminal` (succeeded, failed, timed_out, cancelled, or lost).
- Cron tasks stay live while the cron runtime still owns the job; chat-backed CLI tasks stay live only while their owning run context is still active.
- Completion is push-driven: detached work can notify directly or wake the
requester session/heartbeat when it finishes, so status polling loops are
usually the wrong shape.
- Completion is push-driven: detached work can notify directly or wake the requester session/heartbeat when it finishes, so status polling loops are usually the wrong shape.
- Isolated cron runs and subagent completions best-effort clean up tracked browser tabs/processes for their child session before final cleanup bookkeeping.
- Isolated cron delivery suppresses stale interim parent replies while
descendant subagent work is still draining, and it prefers final descendant
output when that arrives before delivery.
- Isolated cron delivery suppresses stale interim parent replies while descendant subagent work is still draining, and it prefers final descendant output when that arrives before delivery.
- Completion notifications are delivered directly to a channel or queued for the next heartbeat.
- `openclaw tasks list` shows all tasks; `openclaw tasks audit` surfaces issues.
- Terminal records are kept for 7 days, then automatically pruned.
## Quick start
```bash
# List all tasks (newest first)
openclaw tasks list
<Tabs>
<Tab title="List and filter">
```bash
# List all tasks (newest first)
openclaw tasks list
# Filter by runtime or status
openclaw tasks list --runtime acp
openclaw tasks list --status running
# Filter by runtime or status
openclaw tasks list --runtime acp
openclaw tasks list --status running
```
# Show details for a specific task (by ID, run ID, or session key)
openclaw tasks show <lookup>
</Tab>
<Tab title="Inspect">
```bash
# Show details for a specific task (by ID, run ID, or session key)
openclaw tasks show <lookup>
```
</Tab>
<Tab title="Cancel and notify">
```bash
# Cancel a running task (kills the child session)
openclaw tasks cancel <lookup>
# Cancel a running task (kills the child session)
openclaw tasks cancel <lookup>
# Change notification policy for a task
openclaw tasks notify <lookup> state_changes
```
# Change notification policy for a task
openclaw tasks notify <lookup> state_changes
</Tab>
<Tab title="Audit and maintenance">
```bash
# Run a health audit
openclaw tasks audit
# Run a health audit
openclaw tasks audit
# Preview or apply maintenance
openclaw tasks maintenance
openclaw tasks maintenance --apply
```
# Preview or apply maintenance
openclaw tasks maintenance
openclaw tasks maintenance --apply
# Inspect TaskFlow state
openclaw tasks flow list
openclaw tasks flow show <lookup>
openclaw tasks flow cancel <lookup>
```
</Tab>
<Tab title="Task flow">
```bash
# Inspect TaskFlow state
openclaw tasks flow list
openclaw tasks flow show <lookup>
openclaw tasks flow cancel <lookup>
```
</Tab>
</Tabs>
## What creates a task
@@ -77,17 +94,22 @@ openclaw tasks flow cancel <lookup>
| CLI operations | `cli` | `openclaw agent` commands that run through the gateway | `silent` |
| Agent media jobs | `cli` | Session-backed `video_generate` runs | `silent` |
Main-session cron tasks use `silent` notify policy by default — they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
<AccordionGroup>
<Accordion title="Notify defaults for cron and media">
Main-session cron tasks use `silent` notify policy by default — they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
Session-backed `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished video itself. If you opt into `tools.media.asyncCompletion.directSend`, async `music_generate` and `video_generate` completions try direct channel delivery first before falling back to the requester-session wake path.
Session-backed `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished video itself. If you opt into `tools.media.asyncCompletion.directSend`, async `music_generate` and `video_generate` completions try direct channel delivery first before falling back to the requester-session wake path.
While a session-backed `video_generate` task is still active, the tool also acts as a guardrail: repeated `video_generate` calls in that same session return the active task status instead of starting a second concurrent generation. Use `action: "status"` when you want an explicit progress/status lookup from the agent side.
**What does not create tasks:**
- Heartbeat turns — main-session; see [Heartbeat](/gateway/heartbeat)
- Normal interactive chat turns
- Direct `/command` responses
</Accordion>
<Accordion title="Concurrent video_generate guardrail">
While a session-backed `video_generate` task is still active, the tool also acts as a guardrail: repeated `video_generate` calls in that same session return the active task status instead of starting a second concurrent generation. Use `action: "status"` when you want an explicit progress/status lookup from the agent side.
</Accordion>
<Accordion title="What does not create tasks">
- Heartbeat turns — main-session; see [Heartbeat](/gateway/heartbeat)
- Normal interactive chat turns
- Direct `/command` responses
</Accordion>
</AccordionGroup>
## Task lifecycle
@@ -115,12 +137,14 @@ stateDiagram-v2
Transitions happen automatically — when the associated agent run ends, the task status updates to match.
Agent run completion is authoritative for active task records. A successful detached run finalizes as `succeeded`, ordinary run errors finalize as `failed`, and timeout or abort outcomes finalize as `timed_out`. If an operator already cancelled the task, or the runtime already recorded a stronger terminal state such as `failed`, `timed_out`, or `lost`, a later success signal does not downgrade that terminal status.
`lost` is runtime-aware:
- ACP tasks: backing ACP child session metadata disappeared.
- Subagent tasks: backing child session disappeared from the target agent store.
- Cron tasks: the cron runtime no longer tracks the job as active.
- CLI tasks: isolated child-session tasks use the child session; chat-backed CLI tasks use the live run context instead, so lingering channel/group/direct session rows do not keep them alive.
- CLI tasks: isolated child-session tasks use the child session; chat-backed CLI tasks use the live run context instead, so lingering channel/group/direct session rows do not keep them alive. Gateway-backed `openclaw agent` runs also finalize from their run result, so completed runs do not sit active until the sweeper marks them `lost`.
## Delivery and notifications
@@ -134,9 +158,7 @@ When a task reaches a terminal state, OpenClaw notifies you. There are two deliv
Task completion triggers an immediate heartbeat wake so you see the result quickly — you do not have to wait for the next scheduled heartbeat tick.
</Tip>
That means the usual workflow is push-based: start detached work once, then let
the runtime wake or notify you on completion. Poll task state only when you
need debugging, intervention, or an explicit audit.
That means the usual workflow is push-based: start detached work once, then let the runtime wake or notify you on completion. Poll task state only when you need debugging, intervention, or an explicit audit.
### Notification policies
@@ -156,96 +178,93 @@ openclaw tasks notify <lookup> state_changes
## CLI reference
### `tasks list`
<AccordionGroup>
<Accordion title="tasks list">
```bash
openclaw tasks list [--runtime <acp|subagent|cron|cli>] [--status <status>] [--json]
```
```bash
openclaw tasks list [--runtime <acp|subagent|cron|cli>] [--status <status>] [--json]
```
Output columns: Task ID, Kind, Status, Delivery, Run ID, Child Session, Summary.
Output columns: Task ID, Kind, Status, Delivery, Run ID, Child Session, Summary.
</Accordion>
<Accordion title="tasks show">
```bash
openclaw tasks show <lookup>
```
### `tasks show`
The lookup token accepts a task ID, run ID, or session key. Shows the full record including timing, delivery state, error, and terminal summary.
```bash
openclaw tasks show <lookup>
```
</Accordion>
<Accordion title="tasks cancel">
```bash
openclaw tasks cancel <lookup>
```
The lookup token accepts a task ID, run ID, or session key. Shows the full record including timing, delivery state, error, and terminal summary.
For ACP and subagent tasks, this kills the child session. For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable.
### `tasks cancel`
</Accordion>
<Accordion title="tasks notify">
```bash
openclaw tasks notify <lookup> <done_only|state_changes|silent>
```
</Accordion>
<Accordion title="tasks audit">
```bash
openclaw tasks audit [--json]
```
```bash
openclaw tasks cancel <lookup>
```
Surfaces operational issues. Findings also appear in `openclaw status` when issues are detected.
For ACP and subagent tasks, this kills the child session. For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable.
| Finding | Severity | Trigger |
| ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ |
| `stale_queued` | warn | Queued for more than 10 minutes |
| `stale_running` | error | Running for more than 30 minutes |
| `lost` | warn/error | Runtime-backed task ownership disappeared; retained lost tasks warn until `cleanupAfter`, then become errors |
| `delivery_failed` | warn | Delivery failed and notify policy is not `silent` |
| `missing_cleanup` | warn | Terminal task with no cleanup timestamp |
| `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) |
### `tasks notify`
</Accordion>
<Accordion title="tasks maintenance">
```bash
openclaw tasks maintenance [--json]
openclaw tasks maintenance --apply [--json]
```
```bash
openclaw tasks notify <lookup> <done_only|state_changes|silent>
```
Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks and Task Flow state.
### `tasks audit`
Reconciliation is runtime-aware:
```bash
openclaw tasks audit [--json]
```
- ACP/subagent tasks check their backing child session.
- Cron tasks check whether the cron runtime still owns the job.
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
Surfaces operational issues. Findings also appear in `openclaw status` when issues are detected.
Completion cleanup is also runtime-aware:
| Finding | Severity | Trigger |
| ------------------------- | -------- | ----------------------------------------------------- |
| `stale_queued` | warn | Queued for more than 10 minutes |
| `stale_running` | error | Running for more than 30 minutes |
| `lost` | error | Runtime-backed task ownership disappeared |
| `delivery_failed` | warn | Delivery failed and notify policy is not `silent` |
| `missing_cleanup` | warn | Terminal task with no cleanup timestamp |
| `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) |
- Subagent completion best-effort closes tracked browser tabs/processes for the child session before announce cleanup continues.
- Isolated cron completion best-effort closes tracked browser tabs/processes for the cron session before the run fully tears down.
- Isolated cron delivery waits out descendant subagent follow-up when needed and suppresses stale parent acknowledgement text instead of announcing it.
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary. Terminal failed runs announce failure status without replaying captured reply text.
- Cleanup failures do not mask the real task outcome.
### `tasks maintenance`
</Accordion>
<Accordion title="tasks flow list | show | cancel">
```bash
openclaw tasks flow list [--status <status>] [--json]
openclaw tasks flow show <lookup> [--json]
openclaw tasks flow cancel <lookup>
```
```bash
openclaw tasks maintenance [--json]
openclaw tasks maintenance --apply [--json]
```
Use these when the orchestrating Task Flow is the thing you care about rather than one individual background task record.
Use this to preview or apply reconciliation, cleanup stamping, and pruning for
tasks and Task Flow state.
Reconciliation is runtime-aware:
- ACP/subagent tasks check their backing child session.
- Cron tasks check whether the cron runtime still owns the job.
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
Completion cleanup is also runtime-aware:
- Subagent completion best-effort closes tracked browser tabs/processes for the child session before announce cleanup continues.
- Isolated cron completion best-effort closes tracked browser tabs/processes for the cron session before the run fully tears down.
- Isolated cron delivery waits out descendant subagent follow-up when needed and
suppresses stale parent acknowledgement text instead of announcing it.
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary. Terminal failed runs announce failure status without replaying captured reply text.
- Cleanup failures do not mask the real task outcome.
### `tasks flow list|show|cancel`
```bash
openclaw tasks flow list [--status <status>] [--json]
openclaw tasks flow show <lookup> [--json]
openclaw tasks flow cancel <lookup>
```
Use these when the orchestrating Task Flow is the thing you care about rather
than one individual background task record.
</Accordion>
</AccordionGroup>
## Chat task board (`/tasks`)
Use `/tasks` in any chat session to see background tasks linked to that session. The board shows
active and recently completed tasks with runtime, status, timing, and progress or error detail.
Use `/tasks` in any chat session to see background tasks linked to that session. The board shows active and recently completed tasks with runtime, status, timing, and progress or error detail.
When the current session has no visible linked tasks, `/tasks` falls back to agent-local task counts
so you still get an overview without leaking other-session details.
When the current session has no visible linked tasks, `/tasks` falls back to agent-local task counts so you still get an overview without leaking other-session details.
For the full operator ledger, use the CLI: `openclaw tasks list`.
@@ -263,9 +282,7 @@ The summary reports:
- **failures** — count of `failed` + `timed_out` + `lost`
- **byRuntime** — breakdown by `acp`, `subagent`, `cron`, `cli`
Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are
preferred, stale completed rows are hidden, and recent failures only surface when no active work
remains. This keeps the status card focused on what matters right now.
Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are preferred, stale completed rows are hidden, and recent failures only surface when no active work remains. This keeps the status card focused on what matters right now.
## Storage and maintenance
@@ -283,44 +300,55 @@ The registry loads into memory at gateway start and syncs writes to SQLite for d
A sweeper runs every **60 seconds** and handles three things:
1. **Reconciliation** — checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
2. **Cleanup stamping** — sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days).
3. **Pruning** — deletes records past their `cleanupAfter` date.
<Steps>
<Step title="Reconciliation">
Checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
</Step>
<Step title="Cleanup stamping">
Sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days). During retention, lost tasks still appear in audit as warnings; after `cleanupAfter` expires or when cleanup metadata is missing, they are errors.
</Step>
<Step title="Pruning">
Deletes records past their `cleanupAfter` date.
</Step>
</Steps>
**Retention**: terminal task records are kept for **7 days**, then automatically pruned. No configuration needed.
<Note>
**Retention:** terminal task records are kept for **7 days**, then automatically pruned. No configuration needed.
</Note>
## How tasks relate to other systems
### Tasks and Task Flow
<AccordionGroup>
<Accordion title="Tasks and Task Flow">
[Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
[Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
See [Task Flow](/automation/taskflow) for details.
See [Task Flow](/automation/taskflow) for details.
</Accordion>
<Accordion title="Tasks and cron">
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`; runtime execution state lives beside it in `~/.openclaw/cron/jobs-state.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
### Tasks and cron
See [Cron Jobs](/automation/cron-jobs).
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`; runtime execution state lives beside it in `~/.openclaw/cron/jobs-state.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
</Accordion>
<Accordion title="Tasks and heartbeat">
Heartbeat runs are main-session turns — they do not create task records. When a task completes, it can trigger a heartbeat wake so you see the result promptly.
See [Cron Jobs](/automation/cron-jobs).
See [Heartbeat](/gateway/heartbeat).
### Tasks and heartbeat
Heartbeat runs are main-session turns — they do not create task records. When a task completes, it can trigger a heartbeat wake so you see the result promptly.
See [Heartbeat](/gateway/heartbeat).
### Tasks and sessions
A task may reference a `childSessionKey` (where work runs) and a `requesterSessionKey` (who started it). Sessions are conversation context; tasks are activity tracking on top of that.
### Tasks and agent runs
A task's `runId` links to the agent run doing the work. Agent lifecycle events (start, end, error) automatically update the task status — you do not need to manage the lifecycle manually.
</Accordion>
<Accordion title="Tasks and sessions">
A task may reference a `childSessionKey` (where work runs) and a `requesterSessionKey` (who started it). Sessions are conversation context; tasks are activity tracking on top of that.
</Accordion>
<Accordion title="Tasks and agent runs">
A task's `runId` links to the agent run doing the work. Agent lifecycle events (start, end, error) automatically update the task status — you do not need to manage the lifecycle manually.
</Accordion>
</AccordionGroup>
## Related
- [Automation & Tasks](/automation) — all automation mechanisms at a glance
- [Task Flow](/automation/taskflow) — flow orchestration above tasks
- [Scheduled Tasks](/automation/cron-jobs) — scheduling background work
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
- [CLI: Tasks](/cli/tasks) — CLI command reference
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
- [Scheduled Tasks](/automation/cron-jobs) — scheduling background work
- [Task Flow](/automation/taskflow) — flow orchestration above tasks

View File

@@ -21,6 +21,8 @@ need a separate `openclaw plugins install` step.
- OpenClaw talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`).
- Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
- Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
- Auto-TTS replies that synthesize MP3 or CAF audio are delivered as iMessage
voice memo bubbles instead of plain file attachments.
- Pairing/allowlist works the same way as other channels (`/channels/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying.
- Advanced features: edit, unsend, reply threading, message effects, group management.

View File

@@ -263,6 +263,10 @@ Now create some channels on your Discord server and start chatting. Your agent c
- Gateway owns the Discord connection.
- Reply routing is deterministic: Discord inbound replies back to Discord.
- Discord guild/channel metadata is added to the model prompt as untrusted
context, not as a user-visible reply prefix. If a model copies that envelope
back, OpenClaw strips the copied metadata from outbound replies and from
future replay context.
- By default (`session.dmScope=main`), direct chats share the agent main session (`agent:main:main`).
- Guild channels are isolated session keys (`agent:<agentId>:discord:channel:<channelId>`).
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).

View File

@@ -414,6 +414,15 @@ Full configuration: [Gateway configuration](/gateway/configuration)
- ✅ Video/media
- ✅ Stickers
Inbound Feishu/Lark audio messages are normalized as media placeholders instead
of raw `file_key` JSON. When `tools.media.audio` is configured, OpenClaw
downloads the voice-note resource and runs shared audio transcription before the
agent turn, so the agent receives the spoken transcript. If Feishu includes
transcript text directly in the audio payload, that text is used without another
ASR call. Without an audio transcription provider, the agent still receives a
`<media:audio>` placeholder plus the saved attachment, not the raw Feishu
resource payload.
### Send
- ✅ Text

View File

@@ -272,6 +272,7 @@ Notes:
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Groups where silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats still treat empty replies as a failed agent turn.
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.

View File

@@ -398,6 +398,12 @@ Restore room keys from server backup:
openclaw matrix verify backup restore
```
If the backup key is not already loaded on disk, pass the Matrix recovery key:
```bash
openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"
```
Interactive self-verification flow:
```bash
@@ -480,6 +486,8 @@ openclaw matrix verify status
```
Add `--account <id>` to target a named account. This can also recreate secret storage if the current backup secret cannot be loaded safely.
Add `--rotate-recovery-key` only when you intentionally want the old recovery
key to stop unlocking the fresh backup baseline.
</Accordion>
@@ -501,6 +509,34 @@ openclaw matrix verify status
</Accordion>
<Accordion title="Deleted or invalid Matrix device">
If `verify status` says the current device is no longer listed on the
homeserver, create a new OpenClaw Matrix device. For password login:
```bash
openclaw matrix account add \
--account assistant \
--homeserver https://matrix.example.org \
--user-id '@assistant:example.org' \
--password '<password>' \
--device-name OpenClaw-Gateway
```
For token auth, create a fresh access token in your Matrix client or admin UI,
then update OpenClaw:
```bash
openclaw matrix account add \
--account assistant \
--homeserver https://matrix.example.org \
--access-token '<token>'
```
Replace `assistant` with the account ID from the failed command, or omit
`--account` for the default account.
</Accordion>
<Accordion title="Device hygiene">
Old OpenClaw-managed devices can accumulate. List and prune:
@@ -847,6 +883,11 @@ Matrix accepts these target forms anywhere OpenClaw asks you for a room or user
- Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server`
- Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server`
Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix
when configuring explicit delivery targets, cron jobs, bindings, or allowlists.
OpenClaw keeps internal session keys canonical for storage, so those lowercase
keys are not a reliable source for Matrix delivery IDs.
Live directory lookup uses the logged-in Matrix account:
- User lookups query the Matrix user directory on that homeserver.

View File

@@ -5,7 +5,7 @@ read_when:
title: "Microsoft Teams"
---
Text and DM attachments are supported; channel and group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. Message actions expose explicit `upload-file` for file-first sends.
Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. Message actions expose explicit `upload-file` for file-first sends.
## Bundled plugin
@@ -27,25 +27,64 @@ openclaw plugins install ./path/to/local/msteams-plugin
Details: [Plugins](/tools/plugin)
## Quick setup (beginner)
## Quick setup
1. Ensure the Microsoft Teams plugin is available.
- Current packaged OpenClaw releases already bundle it.
- Older/custom installs can add it manually with the commands above.
2. Create an **Azure Bot** (App ID + client secret + tenant ID).
3. Configure OpenClaw with those credentials.
4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
5. Install the Teams app package and start the gateway.
The [`@microsoft/teams.cli`](https://www.npmjs.com/package/@microsoft/teams.cli) handles bot registration, manifest creation, and credential generation in a single command.
Minimal config (client secret):
**1. Install and log in**
```bash
npm install -g @microsoft/teams.cli@preview
teams login
teams status # verify you're logged in and see your tenant info
```
> **Note:** The Teams CLI is currently in preview. Commands and flags may change between releases.
**2. Start a tunnel** (Teams can't reach localhost)
Install and authenticate the devtunnel CLI if you haven't already ([getting started guide](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started)).
```bash
# One-time setup (persistent URL across sessions):
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
# Each dev session:
devtunnel host my-openclaw-bot
# Your endpoint: https://<tunnel-id>.devtunnels.ms/api/messages
```
> **Note:** `--allow-anonymous` is required because Teams can't authenticate with devtunnels. Each incoming bot request is still validated by the Teams SDK automatically.
Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (but these may change URLs each session).
**3. Create the app**
```bash
teams app create \
--name "OpenClaw" \
--endpoint "https://<your-tunnel-url>/api/messages"
```
This single command:
- Creates an Entra ID (Azure AD) application
- Generates a client secret
- Builds and uploads a Teams app manifest (with icons)
- Registers the bot (Teams-managed by default — no Azure subscription needed)
The output will show `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and a **Teams App ID** — note these for the next steps. It also offers to install the app in Teams directly.
**4. Configure OpenClaw** using the credentials from the output:
```json5
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
appId: "<CLIENT_ID>",
appPassword: "<CLIENT_SECRET>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
@@ -53,10 +92,34 @@ Minimal config (client secret):
}
```
For production deployments, consider using [federated authentication](#federated-authentication) (certificate or managed identity) instead of client secrets.
Or use environment variables directly: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`.
**5. Install the app in Teams**
`teams app create` will prompt you to install the app — select "Install in Teams". If you skipped it, you can get the link later:
```bash
teams app get <teamsAppId> --install-link
```
**6. Verify everything works**
```bash
teams app doctor <teamsAppId>
```
This runs diagnostics across bot registration, AAD app config, manifest validity, and SSO setup.
For production deployments, consider using [federated authentication](#federated-authentication-certificate--managed-identity) (certificate or managed identity) instead of client secrets.
Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated).
## Goals
- Talk to OpenClaw via Teams DMs, group chats, or channels.
- Keep routing deterministic: replies always go back to the channel they arrived on.
- Default to safe channel behavior (mentions required unless configured otherwise).
## Config writes
By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
@@ -126,54 +189,93 @@ Example:
}
```
## Azure Bot setup
<details>
<summary><strong>Manual setup (without the Teams CLI)</strong></summary>
Before configuring OpenClaw, create an Azure Bot resource and capture its credentials.
If you can't use the Teams CLI, you can set up the bot manually through the Azure Portal.
<Steps>
<Step title="Create the Azure Bot">
Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot) and fill in the **Basics** tab:
### How it works
| Field | Value |
| ------------------ | -------------------------------------------------------- |
| **Bot handle** | Your bot name, e.g. `openclaw-msteams` (must be unique) |
| **Subscription** | Your Azure subscription |
| **Resource group** | Create new or use existing |
| **Pricing tier** | **Free** for dev/testing |
| **Type of App** | **Single Tenant** (recommended) |
| **Creation type** | **Create new Microsoft App ID** |
1. Ensure the Microsoft Teams plugin is available (bundled in current releases).
2. Create an **Azure Bot** (App ID + secret + tenant ID).
3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
4. Upload/install the Teams app into a team (or personal scope for DMs).
5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway.
6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
<Note>
New multi-tenant bots were deprecated after 2025-07-31. Use **Single Tenant** for new bots.
</Note>
### Step 1: Create Azure Bot
Click **Review + create****Create** (wait ~1-2 minutes).
1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot)
2. Fill in the **Basics** tab:
</Step>
| Field | Value |
| ------------------ | -------------------------------------------------------- |
| **Bot handle** | Your bot name, e.g., `openclaw-msteams` (must be unique) |
| **Subscription** | Select your Azure subscription |
| **Resource group** | Create new or use existing |
| **Pricing tier** | **Free** for dev/testing |
| **Type of App** | **Single Tenant** (recommended - see note below) |
| **Creation type** | **Create new Microsoft App ID** |
<Step title="Capture credentials">
From the Azure Bot resource → **Configuration**:
> **Deprecation notice:** Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots.
- copy **Microsoft App ID**`appId`
- **Manage Password** → **Certificates & secrets****New client secret** → copy the value → `appPassword`
- **Overview** → **Directory (tenant) ID**`tenantId`
3. Click **Review + create****Create** (wait ~1-2 minutes)
</Step>
### Step 2: Get Credentials
<Step title="Configure messaging endpoint">
Azure Bot → **Configuration** → set **Messaging endpoint**:
1. Go to your Azure Bot resource → **Configuration**
2. Copy **Microsoft App ID** → this is your `appId`
3. Click **Manage Password** → go to the App Registration
4. Under **Certificates & secrets****New client secret** → copy the **Value** → this is your `appPassword`
5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId`
- Production: `https://your-domain.com/api/messages`
- Local dev: use a tunnel (see [Local development](#local-development-tunneling))
### Step 3: Configure Messaging Endpoint
</Step>
1. In Azure Bot → **Configuration**
2. Set **Messaging endpoint** to your webhook URL:
- Production: `https://your-domain.com/api/messages`
- Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below)
<Step title="Enable the Teams channel">
Azure Bot → **Channels** → click **Microsoft Teams** → Configure → Save. Accept the Terms of Service.
</Step>
</Steps>
### Step 4: Enable Teams Channel
## Federated authentication
1. In Azure Bot → **Channels**
2. Click **Microsoft Teams** → Configure → Save
3. Accept the Terms of Service
### Step 5: Build Teams App Manifest
- Include a `bot` entry with `botId = <App ID>`.
- Scopes: `personal`, `team`, `groupChat`.
- `supportsFiles: true` (required for personal scope file handling).
- Add RSC permissions (see [RSC Permissions](#current-teams-rsc-permissions-manifest)).
- Create icons: `outline.png` (32x32) and `color.png` (192x192).
- Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
### Step 6: Configure OpenClaw
```json5
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
```
Environment variables: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`.
### Step 7: Run the Gateway
The Teams channel starts automatically when the plugin is available and `msteams` config exists with credentials.
</details>
## Federated Authentication (Certificate + Managed Identity)
> Added in 2026.3.24
@@ -268,7 +370,7 @@ Use Azure Managed Identity for passwordless authentication. This is ideal for de
- `MSTEAMS_USE_MANAGED_IDENTITY=true`
- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id>` (only for user-assigned)
### AKS workload identity setup
### AKS Workload Identity Setup
For AKS deployments using workload identity:
@@ -315,63 +417,55 @@ For AKS deployments using workload identity:
**Default behavior:** When `authType` is not set, OpenClaw defaults to client secret authentication. Existing configurations continue to work without changes.
## Local development (tunneling)
## Local Development (Tunneling)
Teams can't reach `localhost`. Use a tunnel for local development:
**Option A: ngrok**
Teams can't reach `localhost`. Use a persistent dev tunnel so your URL stays the same across sessions:
```bash
ngrok http 3978
# Copy the https URL, e.g., https://abc123.ngrok.io
# Set messaging endpoint to: https://abc123.ngrok.io/api/messages
# One-time setup:
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
# Each dev session:
devtunnel host my-openclaw-bot
```
**Option B: Tailscale Funnel**
Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (URLs may change each session).
If your tunnel URL changes, update the endpoint:
```bash
tailscale funnel 3978
# Use your Tailscale funnel URL as the messaging endpoint
teams app update <teamsAppId> --endpoint "https://<new-url>/api/messages"
```
## Teams Developer Portal (alternative)
## Testing the Bot
Instead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps):
**Run diagnostics:**
1. Click **+ New app**
2. Fill in basic info (name, description, developer info)
3. Go to **App features** → **Bot**
4. Select **Enter a bot ID manually** and paste your Azure Bot App ID
5. Check scopes: **Personal**, **Team**, **Group Chat**
6. Click **Distribute** → **Download app package**
7. In Teams: **Apps** → **Manage your apps** → **Upload a custom app** → select the ZIP
```bash
teams app doctor <teamsAppId>
```
This is often easier than hand-editing JSON manifests.
Checks bot registration, AAD app, manifest, and SSO configuration in one pass.
## Testing the bot
**Send a test message:**
**Option A: Azure Web Chat (verify webhook first)**
1. In Azure Portal → your Azure Bot resource → **Test in Web Chat**
2. Send a message - you should see a response
3. This confirms your webhook endpoint works before Teams setup
**Option B: Teams (after app installation)**
1. Install the Teams app (sideload or org catalog)
1. Install the Teams app (use the install link from `teams app get <id> --install-link`)
2. Find the bot in Teams and send a DM
3. Check gateway logs for incoming activity
<Accordion title="Environment variable overrides">
## Environment variables
Any of the bot/auth config keys can also be set via env vars:
All config keys can be set via environment variables instead:
- `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`
- `MSTEAMS_AUTH_TYPE` (`"secret"` or `"federated"`)
- `MSTEAMS_CERTIFICATE_PATH`, `MSTEAMS_CERTIFICATE_THUMBPRINT` (federated + certificate)
- `MSTEAMS_USE_MANAGED_IDENTITY`, `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` (federated + managed identity; client ID only for user-assigned)
</Accordion>
- `MSTEAMS_APP_ID`
- `MSTEAMS_APP_PASSWORD`
- `MSTEAMS_TENANT_ID`
- `MSTEAMS_AUTH_TYPE` (optional: `"secret"` or `"federated"`)
- `MSTEAMS_CERTIFICATE_PATH` (federated + certificate)
- `MSTEAMS_CERTIFICATE_THUMBPRINT` (optional, not required for auth)
- `MSTEAMS_USE_MANAGED_IDENTITY` (federated + managed identity)
- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` (user-assigned MI only)
## Member info action
@@ -393,7 +487,7 @@ The action is gated by `channels.msteams.actions.memberInfo` (default: enabled w
- In other words, allowlists gate who can trigger the agent; only specific supplemental context paths are filtered today.
- DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms["<user_id>"].historyLimit`.
## Current Teams RSC permissions
## Current Teams RSC Permissions (Manifest)
These are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed.
@@ -411,7 +505,13 @@ These are the **existing resourceSpecific permissions** in our Teams app manifes
- `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention
## Example Teams manifest
To add RSC permissions via the Teams CLI:
```bash
teams app rsc add <teamsAppId> ChannelMessage.Read.Group --type Application
```
## Example Teams Manifest (redacted)
Minimal, valid example with the required fields. Replace IDs and URLs.
@@ -473,18 +573,31 @@ Minimal, valid example with the required fields. Replace IDs and URLs.
To update an already-installed Teams app (e.g., to add RSC permissions):
```bash
# Download, edit, and re-upload the manifest
teams app manifest download <teamsAppId> manifest.json
# Edit manifest.json locally...
teams app manifest upload manifest.json <teamsAppId>
# Version is auto-bumped if content changed
```
After updating, reinstall the app in each team for new permissions to take effect, and **fully quit and relaunch Teams** (not just close the window) to clear cached app metadata.
<details>
<summary>Manual manifest update (without CLI)</summary>
1. Update your `manifest.json` with the new settings
2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`)
3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`)
4. Upload the new zip:
- **Option A (Teams Admin Center):** Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version
- **Option B (Sideload):** In Teams → Apps → Manage your apps → Upload a custom app
5. **For team channels:** Reinstall the app in each team for new permissions to take effect
6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata
- **Teams Admin Center:** Teams apps → Manage apps → find your app → Upload new version
- **Sideload:** In Teams → Apps → Manage your apps → Upload a custom app
</details>
## Capabilities: RSC only vs Graph
### Teams RSC only (no Graph API permissions)
### With **Teams RSC only** (app installed, no Graph API permissions)
Works:
@@ -498,7 +611,7 @@ Does NOT work:
- Downloading attachments stored in SharePoint/OneDrive.
- Reading message history (beyond the live webhook event).
### Teams RSC plus Microsoft Graph application permissions
### With **Teams RSC + Microsoft Graph Application permissions**
Adds:
@@ -530,7 +643,7 @@ If you need images/files in **channels** or want to fetch **message history**, y
**Additional permission for user mentions:** User @mentions work out of the box for users in the conversation. However, if you want to dynamically search and mention users who are **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent.
## Known limitations
## Known Limitations
### Webhook timeouts
@@ -552,53 +665,40 @@ Teams markdown is more limited than Slack or Discord:
## Configuration
Grouped settings (see `/gateway/configuration` for shared channel patterns).
Key settings (see `/gateway/configuration` for shared channel patterns):
<AccordionGroup>
<Accordion title="Core and webhook">
- `channels.msteams.enabled`
- `channels.msteams.appId`, `appPassword`, `tenantId`: bot credentials
- `channels.msteams.webhook.port` (default `3978`)
- `channels.msteams.webhook.path` (default `/api/messages`)
</Accordion>
- `channels.msteams.enabled`: enable/disable the channel.
- `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
- `channels.msteams.webhook.port` (default `3978`)
- `channels.msteams.webhook.path` (default `/api/messages`)
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
- `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available.
- `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching and direct team/channel name routing.
- `channels.msteams.textChunkLimit`: outbound text chunk size.
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
- `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts).
- `channels.msteams.requireMention`: require @mention in channels/groups (default true).
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
- `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
- `channels.msteams.teams.<teamId>.tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.
- `channels.msteams.teams.<teamId>.toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported).
- `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
- `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
- `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
- `toolsBySender` keys should use explicit prefixes:
`id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only).
- `channels.msteams.actions.memberInfo`: enable or disable the Graph-backed member info action (default: enabled when Graph credentials are available).
- `channels.msteams.authType`: authentication type — `"secret"` (default) or `"federated"`.
- `channels.msteams.certificatePath`: path to PEM certificate file (federated + certificate auth).
- `channels.msteams.certificateThumbprint`: certificate thumbprint (optional, not required for auth).
- `channels.msteams.useManagedIdentity`: enable managed identity auth (federated mode).
- `channels.msteams.managedIdentityClientId`: client ID for user-assigned managed identity.
- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
<Accordion title="Authentication">
- `authType`: `"secret"` (default) or `"federated"`
- `certificatePath`, `certificateThumbprint`: federated + certificate auth (thumbprint optional)
- `useManagedIdentity`, `managedIdentityClientId`: federated + managed identity auth
</Accordion>
<Accordion title="Access control">
- `dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
- `allowFrom`: DM allowlist, prefer AAD object IDs; the wizard resolves names when Graph access is available
- `dangerouslyAllowNameMatching`: break-glass for mutable UPN/display-name and team/channel name routing
- `requireMention`: require @mention in channels/groups (default `true`)
</Accordion>
<Accordion title="Team and channel overrides">
All of these override the top-level defaults:
- `teams.<teamId>.replyStyle`, `.requireMention`
- `teams.<teamId>.tools`, `.toolsBySender`: per-team tool policy defaults
- `teams.<teamId>.channels.<conversationId>.replyStyle`, `.requireMention`
- `teams.<teamId>.channels.<conversationId>.tools`, `.toolsBySender`
`toolsBySender` keys accept `id:`, `e164:`, `username:`, `name:` prefixes (unprefixed keys map to `id:`). `"*"` is a wildcard.
</Accordion>
<Accordion title="Delivery, media, and actions">
- `textChunkLimit`: outbound text chunk size
- `chunkMode`: `length` (default) or `newline` (split on paragraph boundaries before length)
- `mediaAllowHosts`: inbound attachment host allowlist (defaults to Microsoft/Teams domains)
- `mediaAuthAllowHosts`: hosts that may receive Authorization headers on retries (defaults to Graph + Bot Framework)
- `replyStyle`: `thread | top-level` (see [Reply style](#reply-style-threads-vs-posts))
- `actions.memberInfo`: toggle the Graph-backed member info action (default on when Graph is available)
- `sharePointSiteId`: required for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats))
</Accordion>
</AccordionGroup>
## Routing and sessions
## Routing & Sessions
- Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):
- Direct messages share the main session (`agent:<agentId>:<mainKey>`).
@@ -606,7 +706,7 @@ Grouped settings (see `/gateway/configuration` for shared channel patterns).
- `agent:<agentId>:msteams:channel:<conversationId>`
- `agent:<agentId>:msteams:group:<conversationId>`
## Reply style: threads vs posts
## Reply Style: Threads vs Posts
Teams recently introduced two channel UI styles over the same underlying data model:
@@ -641,7 +741,7 @@ Teams recently introduced two channel UI styles over the same underlying data mo
}
```
## Attachments and images
## Attachments & Images
**Current limitations:**
@@ -724,7 +824,7 @@ Per-user sharing is more secure as only the chat participants can access the fil
Uploaded files are stored in a `/OpenClawShared/` folder in the configured SharePoint site's default document library.
## Polls (adaptive cards)
## Polls (Adaptive Cards)
OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).
@@ -733,7 +833,7 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API)
- The gateway must stay online to record votes.
- Polls do not auto-post result summaries yet (inspect the store file if needed).
## Presentation cards
## Presentation Cards
Send semantic presentation payloads to Teams users or conversations using the `message` tool or CLI. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.
@@ -821,7 +921,7 @@ Note: Without the `user:` prefix, names default to group/team resolution. Always
- Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point.
- See `/gateway/configuration` for `dmPolicy` and allowlist gating.
## Team and channel IDs
## Team and Channel IDs (Common Gotcha)
The `groupId` query parameter in Teams URLs is **NOT** the team ID used for configuration. Extract IDs from the URL path instead:
@@ -847,7 +947,7 @@ https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?gr
- Channel ID = path segment after `/channel/` (URL-decoded)
- **Ignore** the `groupId` query parameter
## Private channels
## Private Channels
Bots have limited support in private channels:
@@ -897,23 +997,12 @@ Bots have limited support in private channels:
- [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent)
- [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph)
- [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages)
- [@microsoft/teams.cli](https://www.npmjs.com/package/@microsoft/teams.cli) - Teams CLI for bot management
## Related
<CardGroup cols={2}>
<Card title="Channels overview" icon="list" href="/channels">
All supported channels.
</Card>
<Card title="Pairing" icon="link" href="/channels/pairing">
DM authentication and pairing flow.
</Card>
<Card title="Groups" icon="users" href="/channels/groups">
Group chat behavior and mention gating.
</Card>
<Card title="Channel routing" icon="route" href="/channels/channel-routing">
Session routing for messages.
</Card>
<Card title="Security" icon="shield" href="/gateway/security">
Access model and hardening.
</Card>
</CardGroup>
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

View File

@@ -209,6 +209,10 @@ Approval prompts generated by the bot itself (for example, "allow this action?"
- **Bot replies "gone to Mars":** credentials not configured or Gateway not started.
- **No inbound messages:** verify `appId` and `clientSecret` are correct, and the
bot is enabled on the QQ Open Platform.
- **Repeated self-replies:** OpenClaw records QQ outbound ref indexes as
bot-authored and ignores inbound events whose current `msgIdx` matches that
same bot account. This prevents platform echo loops while still allowing users
to quote or reply to previous bot messages.
- **Setup with `--token-file` still shows unconfigured:** `--token-file` only sets
the AppSecret. You still need `appId` in config or `QQBOT_APP_ID`.
- **Proactive messages not arriving:** QQ may intercept bot-initiated messages if

View File

@@ -489,6 +489,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `first`
- `all`
When reply threading is enabled and the original Telegram text or caption is available, OpenClaw includes a native Telegram quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units, so longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote.
Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored.
</Accordion>

View File

@@ -151,6 +151,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
- When `messages.removeAckAfterReply` is enabled, OpenClaw clears the WhatsApp ack reaction after a visible reply is delivered.
## Plugin hooks and privacy
@@ -243,6 +244,7 @@ content and identifiers.
- explicit WhatsApp mentions of the bot identity
- configured mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
- inbound voice-note transcripts for authorized group messages
- implicit reply-to-bot detection (reply sender matches bot identity)
Security note:
@@ -295,6 +297,11 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
- `<media:document>`
- `<media:sticker>`
Authorized group voice notes are transcribed before mention gating when the
body is only `<media:audio>`, so saying the bot mention in the voice note can
trigger the reply. If the transcript still does not mention the bot, the
transcript is kept in pending group history instead of the raw placeholder.
Location bodies use terse coordinate text. Location labels/comments and contact/vCard details are rendered as fenced untrusted metadata, not inline prompt text.
</Accordion>
@@ -361,9 +368,11 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
<Accordion title="Outbound media behavior">
- supports image, video, audio (PTT voice-note), and document payloads
- reply payloads preserve `audioAsVoice`; WhatsApp sends audio media as Baileys PTT voice notes
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded to Ogg/Opus before PTT delivery
- native Ogg/Opus audio is sent with `audio/ogg; codecs=opus` for voice-note compatibility
- audio media is sent through the Baileys `audio` payload with `ptt: true`, so WhatsApp clients render it as a push-to-talk voice note
- reply payloads preserve `audioAsVoice`; TTS voice-note output for WhatsApp stays on this PTT path even when the provider returns MP3 or WebM
- native Ogg/Opus audio is sent as `audio/ogg; codecs=opus` for voice-note compatibility
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery
- `/tts latest` sends the latest assistant reply as one voice note and suppresses repeat sends for the same reply; `/tts chat on|off|default` controls auto-TTS for the current WhatsApp chat
- animated GIF playback is supported via `gifPlayback: true` on video sends
- captions are applied to the first media item when sending multi-media reply payloads, except PTT voice notes send the audio first and visible text separately because WhatsApp clients do not render voice-note captions consistently
- media source can be HTTP(S), `file://`, or local paths

View File

@@ -98,7 +98,7 @@ Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as four balanced workers with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest`, then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles that flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
`extension-fast` is PR-only because push runs already execute the full bundled plugin shards. That keeps changed-plugin feedback for reviews without reserving an extra Blacksmith worker on `main` for coverage already present in `checks-node-extensions`.
@@ -132,7 +132,10 @@ pnpm test:channels
pnpm test:contracts:channels
pnpm check:docs # docs format + lint + broken links
pnpm build # build dist when CI artifact/build-smoke lanes matter
pnpm ci:timings # summarize the latest origin/main push CI run
pnpm ci:timings:recent # compare recent successful main CI runs
node scripts/ci-run-timings.mjs <run-id> # summarize wall time, queue time, and slowest jobs
node scripts/ci-run-timings.mjs --latest-main # ignore issue/comment noise and choose origin/main push CI
node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main CI runs
pnpm test:perf:groups --full-suite --allow-failures --output .artifacts/test-perf/baseline-before.json
pnpm test:perf:groups:compare .artifacts/test-perf/baseline-before.json .artifacts/test-perf/after-agent.json

View File

@@ -55,6 +55,7 @@ Detailed guidance: [Browser troubleshooting](/tools/browser#cdp-startup-failure-
```bash
openclaw browser status
openclaw browser doctor
openclaw browser doctor --deep
openclaw browser start
openclaw browser start --headless
openclaw browser stop
@@ -63,6 +64,8 @@ openclaw browser --browser-profile openclaw reset-profile
Notes:
- `doctor --deep` adds a live snapshot probe. It is useful when basic CDP
readiness is green but you want proof that the current tab can be inspected.
- For `attachOnly` and remote CDP profiles, `openclaw browser stop` closes the
active control session and clears temporary emulation overrides even when
OpenClaw did not launch the browser process itself.
@@ -138,6 +141,10 @@ the optional label, and the raw `targetId`. Agents should pass
`suggestedTargetId` back into `focus`, `close`, snapshots, and actions. You can
assign a label with `open --label`, `tab new --label`, or `tab label`; labels,
tab ids, raw target ids, and unique target-id prefixes are all accepted.
When Chromium replaces the underlying raw target during a navigation or form
submit, OpenClaw keeps the stable `tabId`/label attached to the replacement tab
when it can prove the match. Raw target ids remain volatile; prefer
`suggestedTargetId`.
## Snapshot / screenshot / actions
@@ -185,6 +192,10 @@ openclaw browser wait --text "Done"
openclaw browser evaluate --fn '(el) => el.textContent' --ref <ref>
```
Action responses return the current raw `targetId` after action-triggered page
replacement when OpenClaw can prove the replacement tab. Scripts should still
store and pass `suggestedTargetId`/labels for long-lived workflows.
File + dialog helpers:
```bash

View File

@@ -239,13 +239,13 @@ Security contract for remote rescue:
operation, where the runtime already has unsandboxed local authority.
- Require an explicit owner identity. Rescue must not accept wildcard sender
rules, open group policy, unauthenticated webhooks, or anonymous channels.
- Owner DMs only by default. Group/channel rescue requires explicit opt-in and
should still route approval prompts to the owner DM.
- Owner DMs only by default. Group/channel rescue requires explicit opt-in.
- Remote rescue cannot open the local TUI or switch into an interactive agent
session. Use local `openclaw` for agent handoff.
- Persistent writes still require approval, even in rescue mode.
- Audit every applied rescue operation, including channel, account, sender,
session key, operation, config hash before, and config hash after.
- Audit every applied rescue operation. Message-channel rescue records channel,
account, sender, and source-address metadata. Config-mutating operations also
record config hashes before and after.
- Never echo secrets. SecretRef inspection should report availability, not
values.
- If the Gateway is alive, prefer Gateway typed operations. If the Gateway is

View File

@@ -138,6 +138,10 @@ Delivery ownership note:
- `announce` fallback-delivers the final reply only when the agent did not send
directly to the resolved target. `webhook` posts the finished payload to a URL.
`none` disables runner fallback delivery.
- Reminders created from an active chat preserve the live chat delivery target
for fallback announce delivery. Internal session keys may be lowercase; do not
use them as a source of truth for case-sensitive provider IDs such as Matrix
room IDs.
## Common admin commands

View File

@@ -188,6 +188,9 @@ Notes:
- `gateway status` stays available for diagnostics even when the local CLI config is missing or invalid.
- Default `gateway status` proves service state, WebSocket connect, and the auth capability visible at handshake time. It does not prove read/write/admin operations.
- Diagnostic probes are non-mutating for first-time device auth: they reuse an
existing cached device token when one exists, but they do not create a new CLI
device identity or read-only device pairing record just to check status.
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
@@ -225,6 +228,8 @@ Interpretation:
- `Capability: read-only|write-capable|admin-capable|pairing-pending|connect-only` reports what the probe could prove about auth. It is separate from reachability.
- `Read probe: ok` means read-scope detail RPC calls (`health`/`status`/`system-presence`/`config.get`) also succeeded.
- `Read probe: limited - missing scope: operator.read` means connect succeeded but read-scope RPC is limited. This is reported as **degraded** reachability, not full failure.
- Like `gateway status`, probe reuses existing cached device auth but does not
create first-time device identity or pairing state.
- Exit code is non-zero only when no probed target is reachable.
JSON notes (`--json`):
@@ -319,6 +324,7 @@ Command options:
Notes:
- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`.
- Use `gateway restart` to restart a managed service. Do not chain `gateway stop` and `gateway start` as a restart substitute; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
- When token auth requires a token and `gateway.auth.token` is SecretRef-managed, `gateway install` validates that the SecretRef is resolvable but does not persist the resolved token into service environment metadata.
- If token auth requires a token and the configured token SecretRef is unresolved, install fails closed instead of persisting fallback plaintext.
- For password auth on `gateway run`, prefer `OPENCLAW_GATEWAY_PASSWORD`, `--password-file`, or a SecretRef-backed `gateway.auth.password` over inline `--password`.

View File

@@ -209,7 +209,8 @@ deprecation warning and forwards to `openclaw plugins install`.
Npm specs are **registry-only** (package name + optional **exact version** or
**dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency
installs run with `--ignore-scripts` for safety.
installs run project-local with `--ignore-scripts` for safety, even when your
shell has global npm install settings.
Bare specs and `@latest` stay on the stable track. If npm resolves either of
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a

View File

@@ -159,6 +159,7 @@ openclaw infer image generate --prompt "cinematic product photo of headphones" -
openclaw infer image generate --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "simple red circle sticker on a transparent background" --json
openclaw infer image generate --prompt "slow image backend" --timeout-ms 180000 --json
openclaw infer image edit --file ./logo.png --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "keep the logo, remove the background" --json
openclaw infer image edit --file ./poster.png --prompt "make this a vertical story ad" --size 2160x3840 --aspect-ratio 9:16 --resolution 4K --json
openclaw infer image describe --file ./photo.jpg --json
openclaw infer image describe --file ./ui-screenshot.png --model openai/gpt-4.1-mini --json
openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --json
@@ -167,6 +168,8 @@ openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --j
Notes:
- Use `image edit` when starting from existing input files.
- Use `--size`, `--aspect-ratio`, or `--resolution` with `image edit` for
providers/models that support geometry hints on reference-image edits.
- Use `--output-format png --background transparent` with
`--model openai/gpt-image-1.5` for transparent-background OpenAI PNG output;
`--openai-background` remains available as an OpenAI-specific alias. Providers

View File

@@ -384,6 +384,11 @@ Important behavior:
milliseconds of idle time (default 10 minutes; set `0` to disable) and
one-shot embedded runs clean them up at run end
Runtime adapters may normalize this shared registry into the shape their
downstream client expects. For example, embedded Pi consumes OpenClaw
`transport` values directly, while Claude Code and Gemini receive CLI-native
`type` values such as `http`, `sse`, or `stdio`.
## Saved MCP server definitions
OpenClaw also stores a lightweight MCP server registry in config for surfaces

View File

@@ -47,9 +47,10 @@ Notes:
- `models list` is read-only: it reads config, auth profiles, existing catalog
state, and provider-owned catalog rows, but it does not rewrite
`models.json`.
- `models list --all` includes bundled provider-owned static catalog rows even
when you have not authenticated with that provider yet. Those rows still show
as unavailable until matching auth is configured.
- `models list --all --provider <id>` can include provider-owned static catalog
rows from plugin manifests or bundled provider catalog metadata even when you
have not authenticated with that provider yet. Those rows still show as
unavailable until matching auth is configured.
- `models list` keeps native model metadata and runtime caps distinct. In table
output, `Ctx` shows `contextTokens/contextWindow` when an effective runtime
cap differs from the native context window; JSON rows include `contextTokens`
@@ -153,6 +154,9 @@ provider you choose.
`models auth login` runs a provider plugins auth flow (OAuth/API key). Use
`openclaw plugins list` to see which providers are installed.
Use `openclaw models auth --agent <id> <subcommand>` to write auth results to a
specific configured agent store. The parent `--agent` flag is honored by
`add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.
Examples:

View File

@@ -104,6 +104,7 @@ Manage the service:
```bash
openclaw node status
openclaw node start
openclaw node stop
openclaw node restart
openclaw node uninstall

View File

@@ -109,7 +109,8 @@ visibility and per-hook enablement, not package installation.
Npm specs are **registry-only** (package name + optional **exact version** or
**dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency
installs run with `--ignore-scripts` for safety.
installs run project-local with `--ignore-scripts` for safety, even when your
shell has global npm install settings.
Bare specs and `@latest` stay on the stable track. If npm resolves either of
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a
@@ -121,6 +122,9 @@ installs the bundled plugin directly. To install an npm package with the same
name, use an explicit scoped spec (for example `@scope/diffs`).
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
Native OpenClaw plugin archives must contain a valid `openclaw.plugin.json` at
the extracted plugin root; archives that only contain `package.json` are
rejected before OpenClaw writes install records.
Claude marketplace installs are also supported.
@@ -218,7 +222,8 @@ For runtime hook debugging:
from a module-loaded inspection pass.
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway,
service/process hints, config path, and RPC health.
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `agent_end`) require
- Non-bundled conversation hooks (`llm_input`, `llm_output`,
`before_agent_finalize`, `agent_end`) require
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
@@ -231,19 +236,20 @@ openclaw plugins install -l ./my-plugin
source path instead of copying over a managed install target.
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
the managed install ledger while keeping the default behavior unpinned.
the managed plugin index while keeping the default behavior unpinned.
### Install Ledger
### Plugin Index
Plugin install metadata is machine-managed state, not user config. New installs
Plugin install metadata is machine-managed state, not user config. Installs
and updates write it to `plugins/installs.json` under the active OpenClaw state
directory. The file includes a do-not-edit warning and is used by
`openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
Legacy `plugins.installs` entries in `openclaw.json` remain readable as a
deprecated compatibility fallback. When install/update/uninstall paths rewrite
plugin install state, OpenClaw writes the ledger file and removes
`plugins.installs` from the persisted config payload.
directory. Its top-level `installRecords` map is the durable source of install
metadata, including records for broken or missing plugin manifests. The
`plugins` array is the manifest-derived cold registry cache. The file includes a
do-not-edit warning and is used by `openclaw plugins update`, uninstall,
diagnostics, and the cold plugin registry.
When OpenClaw sees shipped legacy `plugins.installs` records in config, it moves
them into the plugin index and removes the config key; if either write fails,
the config records are kept so the install metadata is not lost.
### Uninstall
@@ -253,15 +259,12 @@ openclaw plugins uninstall <id> --dry-run
openclaw plugins uninstall <id> --keep-files
```
`uninstall` removes plugin records from `plugins.entries`, the managed install
ledger, the plugin allowlist, and linked `plugins.load.paths` entries when
applicable.
`uninstall` removes plugin records from `plugins.entries`, the persisted plugin
index, the plugin allowlist, and linked `plugins.load.paths` entries when
applicable. Unless `--keep-files` is set, uninstall also removes the tracked
managed install directory when it is inside OpenClaw's plugin extensions root.
For active memory plugins, the memory slot resets to `memory-core`.
By default, uninstall also removes the plugin install directory under the active
state-dir plugin root. Use
`--keep-files` to keep files on disk.
`--keep-config` is supported as a deprecated alias for `--keep-files`.
### Update
@@ -274,7 +277,7 @@ openclaw plugins update @openclaw/voice-call@beta
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
```
Updates apply to tracked plugin installs in the managed install ledger and
Updates apply to tracked plugin installs in the managed plugin index and
tracked hook-pack installs in `hooks.internal.installs`.
When you pass a plugin id, OpenClaw reuses the recorded install spec for that
@@ -364,8 +367,8 @@ Normal startup, provider owner lookup, channel setup classification, and plugin
inventory can read it without importing plugin runtime modules.
Use `plugins registry` to inspect whether the persisted registry is present,
current, or stale. Use `--refresh` to rebuild it from the durable install
ledger, config policy, and manifest/package metadata. This is a repair path, not
current, or stale. Use `--refresh` to rebuild it from the persisted plugin
index, config policy, and manifest/package metadata. This is a repair path, not
a runtime activation path.
`OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass

View File

@@ -75,7 +75,7 @@ Cancels a running background task.
openclaw tasks audit [--severity <warn|error>] [--code <name>] [--limit <n>] [--json]
```
Surfaces stale, lost, delivery-failed, or otherwise inconsistent task and Task Flow records.
Surfaces stale, lost, delivery-failed, or otherwise inconsistent task and Task Flow records. Lost tasks retained until `cleanupAfter` are warnings; expired or unstamped lost tasks are errors.
### `maintenance`

View File

@@ -32,7 +32,7 @@ openclaw --update
## Options
- `--no-restart`: skip restarting the Gateway service after a successful update.
- `--no-restart`: skip restarting the Gateway service after a successful update. Package-manager updates that do restart the Gateway verify the restarted service reports the expected updated version before the command succeeds.
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.

View File

@@ -27,6 +27,24 @@ harness implements the `codex` runtime. The config key is still named
`embeddedHarness` for compatibility, but user-facing docs and status output
should generally say runtime.
## Three things named Codex
Most confusion comes from three different surfaces sharing the Codex name:
| Surface | OpenClaw name/config | What it does |
| ---------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Codex OAuth provider route | `openai-codex/*` model refs | Uses ChatGPT/Codex subscription OAuth through the normal OpenClaw PI runner. |
| Native Codex app-server runtime | `embeddedHarness.runtime: "codex"` | Runs the embedded agent turn through the bundled Codex app-server harness. |
| Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. |
| Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. |
| OpenAI Platform API route for GPT/Codex-style models | `openai/*` model refs | Uses OpenAI API-key auth unless a runtime override, such as `runtime: "codex"`, runs the turn. |
Those surfaces are intentionally independent. Enabling the `codex` plugin makes
the native app-server features available; it does not rewrite
`openai-codex/*` into `openai/*`, does not change existing sessions, and does
not make ACP the Codex default. Selecting `openai-codex/*` means "use the Codex
OAuth provider route" unless you separately force a runtime.
The common Codex setup uses the `openai` provider with the `codex` runtime:
```json5
@@ -46,6 +64,33 @@ That means OpenClaw selects an OpenAI model ref, then asks the Codex app-server
runtime to run the embedded agent turn. It does not mean the channel, model
provider catalog, or OpenClaw session store becomes Codex.
When the bundled `codex` plugin is enabled, natural-language Codex control
should use the native `/codex` command surface (`/codex bind`, `/codex threads`,
`/codex resume`, `/codex steer`, `/codex stop`) instead of ACP. Use ACP for
Codex only when the user explicitly asks for ACP/acpx or is testing the ACP
adapter path. Claude Code, Gemini CLI, OpenCode, Cursor, and similar external
harnesses still use ACP.
This is the agent-facing decision tree:
1. If the user asks for **Codex bind/control/thread/resume/steer/stop**, use the
native `/codex` command surface when the bundled `codex` plugin is enabled.
2. If the user asks for **Codex as the embedded runtime**, use
`openai/<model>` with `embeddedHarness.runtime: "codex"`.
3. If the user asks for **Codex OAuth/subscription auth on the normal OpenClaw
runner**, use `openai-codex/<model>` and leave the runtime as PI.
4. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
ACP with `runtime: "acp"` and `agentId: "codex"`.
5. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
another external harness**, use ACP/acpx, not the native sub-agent runtime.
| You mean... | Use... |
| --------------------------------------- | -------------------------------------------- |
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
| Codex app-server embedded agent runtime | `embeddedHarness.runtime: "codex"` |
| OpenAI Codex OAuth on the PI runner | `openai-codex/*` model refs |
| Claude Code or other external harness | ACP/acpx |
For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and
[Model providers](/concepts/model-providers). For the Codex runtime support
contract, see [Codex harness](/plugins/codex-harness#v1-support-contract).
@@ -92,6 +137,18 @@ Explicit plugin runtimes fail closed by default. For example,
a broader fallback setting, so an agent-level `runtime: "codex"` is not silently
routed back to PI just because defaults used `fallback: "pi"`.
`auto` mode is intentionally conservative. Plugin runtimes can claim
provider/model pairs they understand, but the Codex plugin does not claim the
`openai-codex` provider in `auto` mode. That keeps
`openai-codex/*` as the explicit PI Codex OAuth route and avoids silently
moving subscription-auth configs onto the native app-server harness.
If `openclaw doctor` warns that the `codex` plugin is enabled while
`openai-codex/*` still routes through PI, treat that as a diagnosis, not a
migration. Keep the config unchanged when PI Codex OAuth is what you want.
Switch to `openai/<model>` plus `runtime: "codex"` only when you want native
Codex app-server execution.
## Compatibility contract
When a runtime is not PI, it should document what OpenClaw surfaces it supports.

View File

@@ -5,115 +5,114 @@ read_when:
- You are switching between the legacy engine and a plugin engine
- You are building a context engine plugin
title: "Context engine"
sidebarTitle: "Context engine"
---
A **context engine** controls how OpenClaw builds model context for each run:
which messages to include, how to summarize older history, and how to manage
context across subagent boundaries.
A **context engine** controls how OpenClaw builds model context for each run: which messages to include, how to summarize older history, and how to manage context across subagent boundaries.
OpenClaw ships with a built-in `legacy` engine and uses it by default — most
users never need to change this. Install and select a plugin engine only when
you want different assembly, compaction, or cross-session recall behavior.
OpenClaw ships with a built-in `legacy` engine and uses it by default — most users never need to change this. Install and select a plugin engine only when you want different assembly, compaction, or cross-session recall behavior.
## Quick start
Check which engine is active:
<Steps>
<Step title="Check which engine is active">
```bash
openclaw doctor
# or inspect config directly:
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
```
</Step>
<Step title="Install a plugin engine">
Context engine plugins are installed like any other OpenClaw plugin.
```bash
openclaw doctor
# or inspect config directly:
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
```
<Tabs>
<Tab title="From npm">
```bash
openclaw plugins install @martian-engineering/lossless-claw
```
</Tab>
<Tab title="From a local path">
```bash
openclaw plugins install -l ./my-context-engine
```
</Tab>
</Tabs>
### Installing a context engine plugin
Context engine plugins are installed like any other OpenClaw plugin. Install
first, then select the engine in the slot:
```bash
# Install from npm
openclaw plugins install @martian-engineering/lossless-claw
# Or install from a local path (for development)
openclaw plugins install -l ./my-context-engine
```
Then enable the plugin and select it as the active engine in your config:
```json5
// openclaw.json
{
plugins: {
slots: {
contextEngine: "lossless-claw", // must match the plugin's registered engine id
},
entries: {
"lossless-claw": {
enabled: true,
// Plugin-specific config goes here (see the plugin's docs)
</Step>
<Step title="Enable and select the engine">
```json5
// openclaw.json
{
plugins: {
slots: {
contextEngine: "lossless-claw", // must match the plugin's registered engine id
},
entries: {
"lossless-claw": {
enabled: true,
// Plugin-specific config goes here (see the plugin's docs)
},
},
},
},
},
}
```
}
```
Restart the gateway after installing and configuring.
Restart the gateway after installing and configuring.
To switch back to the built-in engine, set `contextEngine` to `"legacy"` (or
remove the key entirely — `"legacy"` is the default).
</Step>
<Step title="Switch back to legacy (optional)">
Set `contextEngine` to `"legacy"` (or remove the key entirely — `"legacy"` is the default).
</Step>
</Steps>
## How it works
Every time OpenClaw runs a model prompt, the context engine participates at
four lifecycle points:
Every time OpenClaw runs a model prompt, the context engine participates at four lifecycle points:
1. **Ingest** — called when a new message is added to the session. The engine
can store or index the message in its own data store.
2. **Assemble** — called before each model run. The engine returns an ordered
set of messages (and an optional `systemPromptAddition`) that fit within
the token budget.
3. **Compact** — called when the context window is full, or when the user runs
`/compact`. The engine summarizes older history to free space.
4. **After turn** — called after a run completes. The engine can persist state,
trigger background compaction, or update indexes.
<AccordionGroup>
<Accordion title="1. Ingest">
Called when a new message is added to the session. The engine can store or index the message in its own data store.
</Accordion>
<Accordion title="2. Assemble">
Called before each model run. The engine returns an ordered set of messages (and an optional `systemPromptAddition`) that fit within the token budget.
</Accordion>
<Accordion title="3. Compact">
Called when the context window is full, or when the user runs `/compact`. The engine summarizes older history to free space.
</Accordion>
<Accordion title="4. After turn">
Called after a run completes. The engine can persist state, trigger background compaction, or update indexes.
</Accordion>
</AccordionGroup>
For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by
projecting assembled context into Codex developer instructions and the current
turn prompt. Codex still owns its native thread history and native compactor.
For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by projecting assembled context into Codex developer instructions and the current turn prompt. Codex still owns its native thread history and native compactor.
### Subagent lifecycle (optional)
OpenClaw calls two optional subagent lifecycle hooks:
- **prepareSubagentSpawn** — prepare shared context state before a child run
starts. The hook receives parent/child session keys, `contextMode`
(`isolated` or `fork`), available transcript ids/files, and optional TTL.
If it returns a rollback handle, OpenClaw calls it when spawn fails after
preparation succeeds.
- **onSubagentEnded** — clean up when a subagent session completes or is swept.
<ParamField path="prepareSubagentSpawn" type="method">
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds.
</ParamField>
<ParamField path="onSubagentEnded" type="method">
Clean up when a subagent session completes or is swept.
</ParamField>
### System prompt addition
The `assemble` method can return a `systemPromptAddition` string. OpenClaw
prepends this to the system prompt for the run. This lets engines inject
dynamic recall guidance, retrieval instructions, or context-aware hints
without requiring static workspace files.
The `assemble` method can return a `systemPromptAddition` string. OpenClaw prepends this to the system prompt for the run. This lets engines inject dynamic recall guidance, retrieval instructions, or context-aware hints without requiring static workspace files.
## The legacy engine
The built-in `legacy` engine preserves OpenClaw's original behavior:
- **Ingest**: no-op (the session manager handles message persistence directly).
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline
in the runtime handles context assembly).
- **Compact**: delegates to the built-in summarization compaction, which creates
a single summary of older messages and keeps recent messages intact.
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline in the runtime handles context assembly).
- **Compact**: delegates to the built-in summarization compaction, which creates a single summary of older messages and keeps recent messages intact.
- **After turn**: no-op.
The legacy engine does not register tools or provide a `systemPromptAddition`.
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this
engine is used automatically.
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this engine is used automatically.
## Plugin engines
@@ -185,11 +184,15 @@ Required members:
`assemble` returns an `AssembleResult` with:
- `messages` — the ordered messages to send to the model.
- `estimatedTokens` (required, `number`) — the engine's estimate of total
tokens in the assembled context. OpenClaw uses this for compaction threshold
decisions and diagnostic reporting.
- `systemPromptAddition` (optional, `string`) — prepended to the system prompt.
<ParamField path="messages" type="Message[]" required>
The ordered messages to send to the model.
</ParamField>
<ParamField path="estimatedTokens" type="number" required>
The engine's estimate of total tokens in the assembled context. OpenClaw uses this for compaction threshold decisions and diagnostic reporting.
</ParamField>
<ParamField path="systemPromptAddition" type="string">
Prepended to the system prompt.
</ParamField>
Optional members:
@@ -204,34 +207,33 @@ Optional members:
### ownsCompaction
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays
enabled for the run:
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays enabled for the run:
- `true` — the engine owns compaction behavior. OpenClaw disables Pi's built-in
auto-compaction for that run, and the engine's `compact()` implementation is
responsible for `/compact`, overflow recovery compaction, and any proactive
compaction it wants to do in `afterTurn()`. OpenClaw may still run the
pre-prompt overflow safeguard; when it predicts the full transcript will
overflow, the recovery path calls the active engine's `compact()` before
submitting another prompt.
- `false` or unset — Pi's built-in auto-compaction may still run during prompt
execution, but the active engine's `compact()` method is still called for
`/compact` and overflow recovery.
<AccordionGroup>
<Accordion title="ownsCompaction: true">
The engine owns compaction behavior. OpenClaw disables Pi's built-in auto-compaction for that run, and the engine's `compact()` implementation is responsible for `/compact`, overflow recovery compaction, and any proactive compaction it wants to do in `afterTurn()`. OpenClaw may still run the pre-prompt overflow safeguard; when it predicts the full transcript will overflow, the recovery path calls the active engine's `compact()` before submitting another prompt.
</Accordion>
<Accordion title="ownsCompaction: false or unset">
Pi's built-in auto-compaction may still run during prompt execution, but the active engine's `compact()` method is still called for `/compact` and overflow recovery.
</Accordion>
</AccordionGroup>
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to
the legacy engine's compaction path.
<Warning>
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to the legacy engine's compaction path.
</Warning>
That means there are two valid plugin patterns:
- **Owning mode** — implement your own compaction algorithm and set
`ownsCompaction: true`.
- **Delegating mode** — set `ownsCompaction: false` and have `compact()` call
`delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use
OpenClaw's built-in compaction behavior.
<Tabs>
<Tab title="Owning mode">
Implement your own compaction algorithm and set `ownsCompaction: true`.
</Tab>
<Tab title="Delegating mode">
Set `ownsCompaction: false` and have `compact()` call `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use OpenClaw's built-in compaction behavior.
</Tab>
</Tabs>
A no-op `compact()` is unsafe for an active non-owning engine because it
disables the normal `/compact` and overflow-recovery compaction path for that
engine slot.
A no-op `compact()` is unsafe for an active non-owning engine because it disables the normal `/compact` and overflow-recovery compaction path for that engine slot.
## Configuration reference
@@ -247,47 +249,35 @@ engine slot.
}
```
The slot is exclusive at run time — only one registered context engine is
resolved for a given run or compaction operation. Other enabled
`kind: "context-engine"` plugins can still load and run their registration
code; `plugins.slots.contextEngine` only selects which registered engine id
OpenClaw resolves when it needs a context engine.
<Note>
The slot is exclusive at run time — only one registered context engine is resolved for a given run or compaction operation. Other enabled `kind: "context-engine"` plugins can still load and run their registration code; `plugins.slots.contextEngine` only selects which registered engine id OpenClaw resolves when it needs a context engine.
</Note>
## Relationship to compaction and memory
- **Compaction** is one responsibility of the context engine. The legacy engine
delegates to OpenClaw's built-in summarization. Plugin engines can implement
any compaction strategy (DAG summaries, vector retrieval, etc.).
- **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(...)`.
- **Session pruning** (trimming old tool results in-memory) still runs
regardless of which context engine is active.
<AccordionGroup>
<Accordion title="Compaction">
Compaction is one responsibility of the context engine. The legacy engine delegates to OpenClaw's built-in summarization. Plugin engines can implement any compaction strategy (DAG summaries, vector retrieval, etc.).
</Accordion>
<Accordion title="Memory plugins">
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(...)`.
</Accordion>
<Accordion title="Session pruning">
Trimming old tool results in-memory still runs regardless of which context engine is active.
</Accordion>
</AccordionGroup>
## Tips
- Use `openclaw doctor` to verify your engine is loading correctly.
- If switching engines, existing sessions continue with their current history.
The new engine takes over for future runs.
- Engine errors are logged and surfaced in diagnostics. If a plugin engine
fails to register or the selected engine id cannot be resolved, OpenClaw
does not fall back automatically; runs fail until you fix the plugin or
switch `plugins.slots.contextEngine` back to `"legacy"`.
- For development, use `openclaw plugins install -l ./my-engine` to link a
local plugin directory without copying.
See also: [Compaction](/concepts/compaction), [Context](/concepts/context),
[Plugins](/tools/plugin), [Plugin manifest](/plugins/manifest).
- If switching engines, existing sessions continue with their current history. The new engine takes over for future runs.
- Engine errors are logged and surfaced in diagnostics. If a plugin engine fails to register or the selected engine id cannot be resolved, OpenClaw does not fall back automatically; runs fail until you fix the plugin or switch `plugins.slots.contextEngine` back to `"legacy"`.
- For development, use `openclaw plugins install -l ./my-engine` to link a local plugin directory without copying.
## Related
- [Compaction](/concepts/compaction) — summarizing long conversations
- [Context](/concepts/context) — how context is built for agent turns
- [Plugin Architecture](/plugins/architecture) — registering context engine plugins
- [Compaction](/concepts/compaction) — summarizing long conversations
- [Plugin manifest](/plugins/manifest) — plugin manifest fields
- [Plugins](/tools/plugin) — plugin overview

View File

@@ -135,6 +135,11 @@ earlier conversations. This is opt-in via
**Only keyword matches?** Your embedding provider may not be configured. Check
`openclaw memory status --deep`.
**Local embeddings time out?** `ollama`, `lmstudio`, and `local` use a longer
inline batch timeout by default. If the host is simply slow, set
`agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds` and rerun
`openclaw memory index --force`.
**CJK text not found?** Rebuild the FTS index with
`openclaw memory index --force`.

View File

@@ -631,6 +631,11 @@ Notes:
- For OpenAI-compatible Completions proxies that need vendor-specific fields,
set `agents.defaults.models["provider/model"].params.extra_body` (or
`extraBody`) to merge extra JSON into the outbound request body.
- For vLLM chat-template controls, set
`agents.defaults.models["provider/model"].params.chat_template_kwargs`.
OpenClaw automatically sends `enable_thinking: false` and
`force_nonempty_content: true` for `vllm/nemotron-3-*` when the session
thinking level is off.
- If `baseUrl` is empty/omitted, OpenClaw keeps the default OpenAI behavior (which resolves to `api.openai.com`).
- For safety, an explicit `compat.supportsDeveloperRole: true` is still overridden on non-native `openai-completions` endpoints.

View File

@@ -50,6 +50,21 @@ pnpm qa:lab:watch
rebuilds that bundle on change, and the browser auto-reloads when the QA Lab
asset hash changes.
For a local OpenTelemetry trace smoke, run:
```bash
pnpm qa:otel:smoke
```
That script starts a local OTLP/HTTP trace receiver, runs the
`otel-trace-smoke` QA scenario with the `diagnostics-otel` plugin enabled, then
decodes the exported protobuf spans and asserts the release-critical shape:
`openclaw.run`, `openclaw.model.call`, `openclaw.context.assembled`, and
`openclaw.message.delivery` must be present; model calls must not export
`StreamAbandoned` on successful turns; raw diagnostic IDs and
`openclaw.content.*` attributes must stay out of the trace. It writes
`otel-smoke-summary.json` next to the QA suite artifacts.
For a transport-real Matrix smoke lane, run:
```bash

View File

@@ -35,15 +35,23 @@ cache-write size, directly lowering cost.
## Legacy image cleanup
OpenClaw also runs a separate idempotent cleanup for older legacy sessions that
persisted raw image blocks in history.
OpenClaw also builds a separate idempotent replay view for sessions that
persist raw image blocks or prompt-hydration media markers in history.
- It preserves the **3 most recent completed turns** byte-for-byte so prompt
cache prefixes for recent follow-ups stay stable.
- Older already-processed image blocks in `user` or `toolResult` history can be
replaced with `[image data removed - already processed by model]`.
- In the replay view, older already-processed image blocks from `user` or
`toolResult` history can be replaced with
`[image data removed - already processed by model]`.
- Older textual media references such as `[media attached: ...]`,
`[Image: source: ...]`, and `media://inbound/...` can be replaced with
`[media reference removed - already processed by model]`. Current-turn
attachment markers stay intact so vision models can still hydrate fresh
images.
- The raw session transcript is not rewritten, so history viewers can still
render the original message entries and their images.
- This is separate from normal cache-TTL pruning. It exists to stop repeated
image payloads from busting prompt caches on later turns.
image payloads or stale media refs from busting prompt caches on later turns.
## Smart defaults

View File

@@ -3,6 +3,7 @@ summary: "How OpenClaw manages conversation sessions"
read_when:
- You want to understand session routing and isolation
- You want to configure DM scope for multi-user setups
- You are debugging daily or idle session resets
title: "Session management"
---
@@ -59,13 +60,21 @@ Verify your setup with `openclaw security audit`.
Sessions are reused until they expire:
- **Daily reset** (default) -- new session at 4:00 AM local time on the gateway
host.
host. Daily freshness is based on when the current `sessionId` started, not
on later metadata writes.
- **Idle reset** (optional) -- new session after a period of inactivity. Set
`session.reset.idleMinutes`.
`session.reset.idleMinutes`. Idle freshness is based on the last real
user/channel interaction, so heartbeat, cron, and exec system events do not
keep the session alive.
- **Manual reset** -- type `/new` or `/reset` in chat. `/new <model>` also
switches the model.
When both daily and idle resets are configured, whichever expires first wins.
Heartbeat, cron, exec, and other system-event turns may write session metadata,
but those writes do not extend daily or idle reset freshness. When a reset
rolls the session, queued system-event notices for the old session are
discarded so stale background updates are not prepended to the first prompt in
the new session.
Sessions with an active provider-owned CLI session are not cut by the implicit
daily default. Use `/reset` or configure `session.reset` explicitly when those
@@ -79,6 +88,18 @@ session data.
- **Store:** `~/.openclaw/agents/<agentId>/sessions/sessions.json`
- **Transcripts:** `~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl`
`sessions.json` keeps separate lifecycle timestamps:
- `sessionStartedAt`: when the current `sessionId` began; daily reset uses this.
- `lastInteractionAt`: last user/channel interaction that extends idle lifetime.
- `updatedAt`: last store-row mutation; useful for listing and pruning, but not
authoritative for daily/idle reset freshness.
Older rows without `sessionStartedAt` are resolved from the transcript JSONL
session header when available. If an older row also lacks `lastInteractionAt`,
idle freshness falls back to that session start time, not to later bookkeeping
writes.
## Session maintenance
OpenClaw automatically bounds session storage over time. By default, it runs

View File

@@ -214,6 +214,10 @@ stale. The prompt also notes the public docs mirror, community Discord, and Claw
([https://clawhub.ai](https://clawhub.ai)) for skills discovery. It tells the model to
consult docs first for OpenClaw behavior, commands, configuration, or architecture, and to
run `openclaw status` itself when possible (asking the user only when it lacks access).
For configuration specifically, it points agents to the `gateway` tool action
`config.schema.lookup` for exact field-level docs and constraints, then to
`docs/gateway/configuration.md` and `docs/gateway/configuration-reference.md`
for broader guidance.
## Related

View File

@@ -1238,6 +1238,7 @@
"tools/tokenjuice",
"tools/loop-detection",
"tools/trajectory",
"tools/tts",
"tools/video-generation",
{
"group": "Web browser",
@@ -1301,6 +1302,7 @@
"providers/bedrock-mantle",
"providers/anthropic",
"providers/arcee",
"providers/azure-speech",
"providers/chutes",
"providers/claude-max-api-proxy",
"providers/cloudflare-ai-gateway",
@@ -1317,6 +1319,7 @@
"providers/groq",
"providers/huggingface",
"providers/inferrs",
"providers/inworld",
"providers/kilocode",
"providers/litellm",
"providers/lmstudio",
@@ -1435,11 +1438,12 @@
"group": "Health and diagnostics",
"pages": [
"gateway/health",
"gateway/diagnostics",
"gateway/heartbeat",
"gateway/doctor",
"gateway/logging",
"logging",
"gateway/opentelemetry",
"gateway/logging",
"gateway/diagnostics",
"gateway/troubleshooting"
]
},

View File

@@ -139,6 +139,7 @@ The Gateway writes a rolling log file (printed on startup as
- `bonjour: advertise failed ...`
- `bonjour: ... name conflict resolved` / `hostname conflict resolved`
- `bonjour: watchdog detected non-announced service ...`
- `bonjour: disabling advertiser after ... failed restarts ...`
## Debugging on iOS node
@@ -151,10 +152,96 @@ To capture logs:
The log includes browser state transitions and resultset changes.
## When to disable Bonjour
Disable Bonjour only when LAN multicast advertising is unavailable or harmful.
The common case is a Gateway running behind Docker bridge networking, WSL, or a
network policy that drops mDNS multicast. In those environments the Gateway is
still reachable through its published URL, SSH, Tailnet, or wide-area DNS-SD,
but LAN auto-discovery is not reliable.
Prefer the existing environment override when the problem is deployment-scoped:
```bash
OPENCLAW_DISABLE_BONJOUR=1
```
That disables LAN multicast advertising without changing plugin configuration.
It is safe for Docker images, service files, launch scripts, and one-off
debugging because the setting disappears when the environment does.
Use plugin configuration only when you intentionally want to turn off the
bundled LAN discovery plugin for that OpenClaw config:
```bash
openclaw plugins disable bonjour
```
## Docker gotchas
Bundled Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` for the Gateway service
by default. Docker bridge networks usually do not forward mDNS multicast
(`224.0.0.251:5353`) between the container and the LAN, so leaving Bonjour on can
produce repeated ciao `probing` or `announcing` failures without making discovery
work.
Important gotchas:
- Disabling Bonjour does not stop the Gateway. It only stops LAN multicast
advertising.
- Disabling Bonjour does not change `gateway.bind`; Docker still defaults to
`OPENCLAW_GATEWAY_BIND=lan` so the published host port can work.
- Disabling Bonjour does not disable wide-area DNS-SD. Use wide-area discovery
or Tailnet when the Gateway and node are not on the same LAN.
- Reusing the same `OPENCLAW_CONFIG_DIR` outside Docker does not inherit the
Compose default unless the environment still sets `OPENCLAW_DISABLE_BONJOUR`.
- Set `OPENCLAW_DISABLE_BONJOUR=0` only for host networking, macvlan, or another
network where mDNS multicast is known to pass.
## Troubleshooting disabled Bonjour
If a node no longer auto-discovers the Gateway after Docker setup:
1. Confirm whether the Gateway is intentionally suppressing LAN advertising:
```bash
docker compose config | grep OPENCLAW_DISABLE_BONJOUR
```
2. Confirm the Gateway itself is reachable through the published port:
```bash
curl -fsS http://127.0.0.1:18789/healthz
```
3. Use a direct target when Bonjour is disabled:
- Control UI or local tools: `http://127.0.0.1:18789`
- LAN clients: `http://<gateway-host>:18789`
- Cross-network clients: Tailnet MagicDNS, Tailnet IP, SSH tunnel, or
wide-area DNS-SD
4. If you deliberately enabled Bonjour in Docker with
`OPENCLAW_DISABLE_BONJOUR=0`, test multicast from the host:
```bash
dns-sd -B _openclaw-gw._tcp local.
```
If browsing is empty or the Gateway logs show repeated ciao watchdog
cancellations, restore `OPENCLAW_DISABLE_BONJOUR=1` and use a direct or
Tailnet route.
## Common failure modes
- **Bonjour doesnt cross networks**: use Tailnet or SSH.
- **Multicast blocked**: some WiFi networks disable mDNS.
- **Advertiser stuck in probing/announcing**: hosts with blocked multicast,
container bridges, WSL, or interface churn can leave the ciao advertiser in a
non-announced state. OpenClaw retries a few times and then disables Bonjour
for the current Gateway process instead of restarting the advertiser forever.
- **Docker bridge networking**: bundled Docker Compose disables Bonjour by
default with `OPENCLAW_DISABLE_BONJOUR=1`. Set it to `0` only for host,
macvlan, or another mDNS-capable network.
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
punctuation), then restart the Gateway. The service instance name derives from
@@ -173,6 +260,7 @@ sequences (e.g. spaces become `\032`).
- `openclaw plugins disable bonjour` disables LAN multicast advertising by disabling the bundled plugin.
- `openclaw plugins enable bonjour` restores the default LAN discovery plugin.
- `OPENCLAW_DISABLE_BONJOUR=1` disables LAN multicast advertising without changing plugin config; accepted truthy values are `1`, `true`, `yes`, and `on` (legacy: `OPENCLAW_DISABLE_BONJOUR`).
- Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` by default for bridge networking; override with `OPENCLAW_DISABLE_BONJOUR=0` only when mDNS multicast is available.
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
- `OPENCLAW_SSH_PORT` overrides the SSH port when `sshPort` is advertised (legacy: `OPENCLAW_SSH_PORT`).
- `OPENCLAW_TAILNET_DNS` publishes a MagicDNS hint in TXT when mDNS full mode is enabled (legacy: `OPENCLAW_TAILNET_DNS`).

View File

@@ -364,13 +364,15 @@ Time format in system prompt. Default: `auto` (OS preference).
- `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`.
- `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`.
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for API-key access or `openai-codex/gpt-5.5` for Codex OAuth). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default.
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, `extra_body`/`extraBody`).
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, `chat_template_kwargs`, `extra_body`/`extraBody`).
- Safe edits: use `openclaw config set agents.defaults.models '<json>' --strict-json --merge` to add entries. `config set` refuses replacements that would remove existing allowlist entries unless you pass `--replace`.
- Provider-scoped configure/onboarding flows merge selected provider models into this map and preserve unrelated providers already configured.
- For direct OpenAI Responses models, server-side compaction is enabled automatically. Use `params.responsesServerCompaction: false` to stop injecting `context_management`, or `params.responsesCompactThreshold` to override the threshold. See [OpenAI server-side compaction](/providers/openai#server-side-compaction-responses-api).
- `params`: global default provider parameters applied to all models. Set at `agents.defaults.params` (e.g. `{ cacheRetention: "long" }`).
- `params` merge precedence (config): `agents.defaults.params` (global base) is overridden by `agents.defaults.models["provider/model"].params` (per-model), then `agents.list[].params` (matching agent id) overrides by key. See [Prompt Caching](/reference/prompt-caching) for details.
- `params.extra_body`/`params.extraBody`: advanced pass-through JSON merged into `api: "openai-completions"` request bodies for OpenAI-compatible proxies. If it collides with generated request keys, the extra body wins; non-native completions routes still strip OpenAI-only `store` afterward.
- `params.chat_template_kwargs`: vLLM/OpenAI-compatible chat-template arguments merged into top-level `api: "openai-completions"` request bodies. For `vllm/nemotron-3-*` with thinking off, OpenClaw automatically sends `enable_thinking: false` and `force_nonempty_content: true`; explicit `chat_template_kwargs` override those defaults, and `extra_body.chat_template_kwargs` still has final precedence.
- `params.preserveThinking`: Z.AI-only opt-in for preserved thinking. When enabled and thinking is on, OpenClaw sends `thinking.clear_thinking: false` and replays prior `reasoning_content`; see [Z.AI thinking and preserved thinking](/providers/zai#thinking-and-preserved-thinking).
- `embeddedHarness`: default low-level embedded agent runtime policy. Omitted runtime defaults to OpenClaw Pi. Use `runtime: "pi"` to force the built-in PI harness, `runtime: "auto"` to let registered plugin harnesses claim supported models, or a registered harness id such as `runtime: "codex"`. Set `fallback: "none"` to disable automatic PI fallback. Explicit plugin runtimes such as `codex` fail closed by default unless you set `fallback: "pi"` in the same override scope. Keep model refs canonical as `provider/model`; select Codex, Claude CLI, Gemini CLI, and other execution backends through runtime config instead of legacy runtime provider prefixes. See [Agent runtimes](/concepts/agent-runtimes) for how this differs from provider/model selection.
- Config writers that mutate these fields (for example `/models set`, `/models set-image`, and fallback add/remove commands) save canonical object form and preserve existing fallback lists when possible.
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 4.
@@ -899,6 +901,14 @@ scripts/sandbox-browser-setup.sh # optional browser image
### `agents.list` (per-agent overrides)
Use `agents.list[].tts` to give an agent its own TTS provider, voice, model,
style, or auto-TTS mode. The agent block deep-merges over global
`messages.tts`, so shared credentials can stay in one place while individual
agents override only the voice or provider fields they need. The active agent's
override applies to automatic spoken replies, `/tts audio`, `/tts status`, and
the `tts` agent tool. See [Text-to-speech](/tools/tts#per-agent-voice-overrides)
for provider examples and precedence.
```json5
{
agents: {
@@ -915,6 +925,11 @@ scripts/sandbox-browser-setup.sh # optional browser image
fastModeDefault: false, // per-agent fast mode override
embeddedHarness: { runtime: "auto", fallback: "pi" },
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
tts: {
providers: {
elevenlabs: { voiceId: "EXAVITQu4vr4xnSDxMaL" },
},
},
skills: ["docs-search"], // replaces agents.defaults.skills when set
identity: {
name: "Samantha",
@@ -950,6 +965,7 @@ scripts/sandbox-browser-setup.sh # optional browser image
- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks). Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`.
- `params`: per-agent stream params merged over the selected model entry in `agents.defaults.models`. Use this for agent-specific overrides like `cacheRetention`, `temperature`, or `maxTokens` without duplicating the whole model catalog.
- `tts`: optional per-agent text-to-speech overrides. The block deep-merges over `messages.tts`, so keep shared provider credentials and fallback policy in `messages.tts` and set only persona-specific values such as provider, voice, model, style, or auto mode here.
- `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills.
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Applies when no per-message or session reasoning override is set.
@@ -1162,7 +1178,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
- `per-channel-peer`: isolate per channel + sender (recommended for multi-user inboxes).
- `per-account-channel-peer`: isolate per account + channel + sender (recommended for multi-account).
- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing.
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins. Daily reset freshness uses the session row's `sessionStartedAt`; idle reset freshness uses `lastInteractionAt`. Background/system-event writes such as heartbeat, cron wakeups, exec notifications, and gateway bookkeeping can update `updatedAt`, but they do not keep daily/idle sessions fresh.
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
- **`parentForkMaxTokens`**: max parent-session `totalTokens` allowed when creating a forked thread session (default `100000`).
- If parent `totalTokens` is above this value, OpenClaw starts a fresh thread session instead of inheriting parent transcript history.
@@ -1241,7 +1257,7 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
- Per-channel overrides: `channels.<channel>.ackReaction`, `channels.<channel>.accounts.<id>.ackReaction`.
- Resolution order: account → channel → `messages.ackReaction` → identity fallback.
- Scope: `group-mentions` (default), `group-all`, `direct`, `all`.
- `removeAckAfterReply`: removes ack after reply on Slack, Discord, and Telegram.
- `removeAckAfterReply`: removes ack after reply on reaction-capable channels such as Slack, Discord, Telegram, WhatsApp, and BlueBubbles.
- `messages.statusReactions.enabled`: enables lifecycle status reactions on Slack, Discord, and Telegram.
On Slack and Discord, unset keeps status reactions enabled when ack reactions are active.
On Telegram, set it explicitly to `true` to enable lifecycle status reactions.
@@ -1332,6 +1348,7 @@ Defaults for Talk mode (macOS/iOS/Android).
},
system: {},
},
speechLocale: "ru-RU",
silenceTimeoutMs: 1500,
interruptOnSpeech: true,
},
@@ -1346,6 +1363,7 @@ Defaults for Talk mode (macOS/iOS/Android).
- `providers.*.voiceAliases` lets Talk directives use friendly names.
- `providers.mlx.modelId` selects the Hugging Face repo used by the macOS local MLX helper. If omitted, macOS uses `mlx-community/Soprano-80M-bf16`.
- macOS MLX playback runs through the bundled `openclaw-mlx-tts` helper when present, or an executable on `PATH`; `OPENCLAW_MLX_TTS_BIN` overrides the helper path for development.
- `speechLocale` sets the BCP 47 locale id used by iOS/macOS Talk speech recognition. Leave unset to use the device default.
- `silenceTimeoutMs` controls how long Talk mode waits after user silence before it sends the transcript. Unset keeps the platform default pause window (`700 ms on macOS and Android, 900 ms on iOS`).
---

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