Compare commits

...

1086 Commits

Author SHA1 Message Date
pash
b23ff97ddc Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard
# Conflicts:
#	src/docker-build-cache.test.ts
#	src/scripts/test-projects.test.ts
2026-04-26 17:02:51 -07:00
pash
e705246619 Fix core support boundary test expectations 2026-04-26 17:00:59 -07:00
pash
f936f16cc5 Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard 2026-04-26 16:59:22 -07:00
Peter Steinberger
d2786fb969 test(docker): run observability harness with global tsx 2026-04-27 00:57:55 +01:00
Peter Steinberger
fa0729e145 test: auto-discover vitest suites 2026-04-27 00:55:06 +01:00
pash
fd48faa4ed Fix qa-lab merge CI and compaction review notes 2026-04-26 16:53:15 -07:00
Peter Steinberger
21c51bc140 test(docker): resolve otel decoder from plugin runtime 2026-04-27 00:51:47 +01:00
Vincent Koc
265bc6b6ea test(plugins): guard command cold registry paths
Add command-level sentinel coverage proving channel setup metadata, onboarding auth choices, and models-list provider ownership stay on manifest/registry paths without importing plugin runtime.\n\nLocal verification:\n- pnpm exec oxfmt --check --threads=1 src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm test:serial src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed\n- clean rebase sanity: git diff --check origin/main...HEAD\n\nPR CI had known unrelated main-red failures matching latest main run 24970053892; the new sentinel test passed in CI.
2026-04-26 16:51:36 -07:00
Peter Steinberger
42db865673 test(docker): run observability on shared image 2026-04-27 00:49:36 +01:00
Vincent Koc
5d7c6e6bda test(docker): add observability smoke
Add Docker aggregate observability coverage for QA-lab OTEL and Prometheus diagnostics.
2026-04-26 16:43:56 -07:00
pash
29f1cae867 Fix qa-lab private qa-channel import 2026-04-26 16:43:55 -07:00
Tak Hoffman
560ddd2f9b Fail package update on unhealthy restart (#72422) 2026-04-26 18:38:23 -05:00
pash
f58dd36a1d Harden session truncation concurrency guards 2026-04-26 16:34:05 -07:00
Peter Steinberger
998e37fcb3 ci: allow installer smoke baseline override 2026-04-27 00:31:30 +01:00
pash
33e3dccbea Fix update CLI service state test mocks 2026-04-26 16:29:55 -07:00
Vincent Koc
3cc52d9050 docs(changelog): note codex usage accounting fix 2026-04-26 16:27:23 -07:00
Vincent Koc
7902c769da fix(codex): normalize cached harness input tokens 2026-04-26 16:27:23 -07:00
Peter Steinberger
9be8d43c31 docs: document installer recovery cleanup 2026-04-27 00:26:02 +01:00
Peter Steinberger
eccb79db99 build: remove private QA package compat shims 2026-04-27 00:26:02 +01:00
pash
6fc954539f Harden session truncation rewrite 2026-04-26 16:20:29 -07:00
pash
fc13a0135e Fix stale e2e Docker cache test 2026-04-26 16:14:58 -07:00
pash
0ced62f512 Fix transcript truncation OOM guard 2026-04-26 16:10:10 -07:00
Peter Steinberger
09a635a28b test: fix main release validation forward-port 2026-04-27 00:07:31 +01:00
Peter Steinberger
5b257cb352 test(qa): drop brittle telegram workflow assertions
(cherry picked from commit b02fdb8264)
2026-04-27 00:07:31 +01:00
Peter Steinberger
efe940e9cb ci(qa): remove telegram beta approval gate
(cherry picked from commit 5e04b0f97a)
2026-04-27 00:07:31 +01:00
Peter Steinberger
8d909ed0da ci(docker): pass beta env to installer e2e
(cherry picked from commit 7677b4ca24)
2026-04-27 00:07:31 +01:00
Peter Steinberger
1bb46ce68a ci(docker): test release installer against beta
(cherry picked from commit d8c4dcb6a4)
2026-04-27 00:07:31 +01:00
Peter Steinberger
54e77a9ec4 ci(docker): use resolved pnpm for scheduled lanes
(cherry picked from commit 61a539a1b7)
2026-04-27 00:07:31 +01:00
Peter Steinberger
43e651db9a ci(docker): preserve pnpm path in scheduler lanes
(cherry picked from commit 2e8a089836)
2026-04-27 00:07:31 +01:00
Peter Steinberger
e7d069edcf test(qa): relax telegram mention reply assertion
(cherry picked from commit 7109251318)
2026-04-27 00:07:31 +01:00
Peter Steinberger
17094640f8 ci(release): trust release branch docker checks
(cherry picked from commit abf0ef9cd3)
2026-04-27 00:07:31 +01:00
Peter Steinberger
16c6a92c53 ci(release): allow npm telegram e2e from release branch
(cherry picked from commit 53f8e9de13)
2026-04-27 00:07:31 +01:00
Peter Steinberger
ef3309a986 fix(release): harden beta validation lanes
(cherry picked from commit 218bceaa14)
2026-04-27 00:07:31 +01:00
Peter Steinberger
95ae3c00bd docs: explain test routing model 2026-04-27 00:05:27 +01:00
Vincent Koc
97e64196a0 fix(hooks): use local timezone for session-memory filenames (#72408) 2026-04-26 16:04:10 -07:00
Peter Steinberger
41ad03dda4 fix(test): allow legacy qa inventory entry 2026-04-27 00:02:33 +01:00
Peter Steinberger
4a578740a2 refactor: deduplicate changed lane detection 2026-04-27 00:02:00 +01:00
Peter Steinberger
20d6daaeaa docs: document automatic bonjour container policy 2026-04-27 00:00:22 +01:00
Peter Steinberger
6018f29dbf ci: keep docker bonjour setting automatic 2026-04-27 00:00:22 +01:00
Peter Steinberger
989cfd1e33 fix(bonjour): auto-disable advertising in containers 2026-04-27 00:00:22 +01:00
Peter Steinberger
89ab39ca64 test: simplify changed test routing 2026-04-26 23:58:13 +01:00
Peter Steinberger
199d5f765f docs(test): explain cheap docker reruns 2026-04-26 23:56:14 +01:00
Peter Steinberger
2fe11020d2 refactor(test): split bundled channel docker scenarios 2026-04-26 23:56:14 +01:00
Peter Steinberger
1ddf6b4e39 ci: skip existing docker e2e images 2026-04-26 23:56:14 +01:00
Peter Steinberger
1a02d00eb4 test: add docker e2e rerun helpers 2026-04-26 23:56:14 +01:00
Peter Steinberger
cfe58387a7 docs: update changelog attribution guidance 2026-04-26 23:51:51 +01:00
Peter Steinberger
6077941d0b fix: restart package updates through updated install 2026-04-26 23:51:51 +01:00
Peter Steinberger
b5714b90ed refactor(test): share docker e2e shell helpers 2026-04-26 23:48:32 +01:00
Peter Steinberger
7a86448a6e ci: reuse docker e2e plan action 2026-04-26 23:48:32 +01:00
Peter Steinberger
6cba12caae test: add docker e2e planner guards 2026-04-26 23:48:32 +01:00
Rubén Cuevas
a08b65a90a fix(telegram): send fresh finals for stale previews (#72038)
* fix(telegram): send fresh finals for stale previews

* test(telegram): cover stale preview send fallback

* fix(telegram): keep stale archived preview fallback

* fix(telegram): clear stale active previews

* fix(telegram): reset preview state after fresh finals
2026-04-26 15:44:30 -07:00
Peter Steinberger
084dde89fd docs: clarify extension ownership boundaries 2026-04-26 23:39:18 +01:00
Peter Steinberger
2efc4a8233 docs(test): document docker e2e layout 2026-04-26 23:36:31 +01:00
Peter Steinberger
cd417f3b68 ci: derive docker e2e artifacts from plan 2026-04-26 23:36:31 +01:00
Peter Steinberger
a2adb05f74 refactor(test): split docker e2e planner 2026-04-26 23:36:31 +01:00
Peter Steinberger
c9c0ab3a44 fix(bonjour): keep ciao failure handling extension-owned 2026-04-26 23:29:40 +01:00
Peter Steinberger
0472b6197a chore: clarify bonjour fatal guard naming 2026-04-26 23:27:35 +01:00
Peter Steinberger
8a60e57846 fix: keep bonjour failures non-fatal 2026-04-26 23:27:08 +01:00
Vincent Koc
c6cf37068c fix(feishu): repair interactive card content extraction (#72397) 2026-04-26 15:26:53 -07:00
Peter Steinberger
ff6044f441 docs(changelog): note Ollama thinking validation fix 2026-04-26 23:25:05 +01:00
Peter Steinberger
5aa3779d8c ci: disable bonjour in install e2e docker 2026-04-26 23:20:08 +01:00
Peter Steinberger
ff9fefb79b fix(agents): validate thinking with model catalog 2026-04-26 23:16:05 +01:00
Peter Steinberger
3746e5b969 ci: cap Telegram E2E build cache 2026-04-26 23:11:21 +01:00
Peter Steinberger
9f5bc5465c style: format codex and loader tests 2026-04-26 23:10:33 +01:00
Peter Steinberger
d108110a89 ci: use packaged tarball for docker e2e 2026-04-26 23:10:33 +01:00
Peter Steinberger
1b1eea238c ci: preserve docker test runner path 2026-04-26 23:04:21 +01:00
Vincent Koc
d9e9e61e77 fix(logging): skip unserializable file log message parts 2026-04-26 15:01:19 -07:00
Vincent Koc
fc0e6e4650 docs(logging): document structured file fields 2026-04-26 15:01:19 -07:00
Vincent Koc
e8df081a1f feat(logging): add file log correlation fields 2026-04-26 15:01:19 -07:00
github-actions[bot]
5c4c33c7de chore(ui): refresh th control ui locale 2026-04-26 22:01:03 +00:00
Vincent Koc
070b55f336 UI: localize command palette labels (#72378) 2026-04-26 14:58:16 -07:00
Vincent Koc
364d49889e fix: allow trusted exec approvals home symlinks (#72377) 2026-04-26 14:57:01 -07:00
Peter Steinberger
baaad52389 ci: split docker e2e images 2026-04-26 22:55:00 +01:00
Peter Steinberger
3a8961af0f test: copy docker build helper in setup e2e 2026-04-26 22:54:27 +01:00
Peter Steinberger
ff570f3a61 fix(ollama): expose native thinking efforts 2026-04-26 22:49:13 +01:00
Peter Steinberger
2cd23957c0 build: use slim docker runtime 2026-04-26 22:47:48 +01:00
Vincent Koc
43a003b8a0 fix: short-circuit live model switch fallback redirects (#72375) 2026-04-26 14:45:02 -07:00
Vincent Koc
fa85e6c26e docs(changelog): note acp stdout fix 2026-04-26 14:42:37 -07:00
Vincent Koc
d46de6cff7 fix(acp): keep server logs off stdout 2026-04-26 14:42:22 -07:00
Peter Steinberger
018f2e78ba build: skip docker apt upgrades 2026-04-26 22:40:44 +01:00
Peter Steinberger
b61954919c ci: verify docker release attestations 2026-04-26 22:40:44 +01:00
Peter Steinberger
5abb717112 docs: add OpenClaw testing skill 2026-04-26 22:40:32 +01:00
Vincent Koc
8226238765 refactor(plugins): share lookup cache eviction 2026-04-26 14:28:15 -07:00
Peter Steinberger
b68b4b9151 ci: add targeted docker lane reruns 2026-04-26 22:27:45 +01:00
Josh Lehman
a3c51f91c5 fix: isolate cron context-engine session keys (#72292) 2026-04-26 14:21:01 -07:00
Vincent Koc
2edbdc42ae refactor(plugins): isolate loader cache state 2026-04-26 14:16:35 -07:00
Peter Steinberger
b28de9a7d9 ci: centralize docker build wrapper 2026-04-26 22:14:36 +01:00
Peter Steinberger
824c3e2b71 ci: enable docker image attestations 2026-04-26 22:14:36 +01:00
Vincent Koc
2194a8c64c docs(logging): document request trace scopes 2026-04-26 14:13:15 -07:00
Vincent Koc
410783c126 fix(diagnostics): chain run traces to request scope 2026-04-26 14:13:15 -07:00
Vincent Koc
3ae6f01d61 feat(logging): propagate request trace scopes 2026-04-26 14:13:14 -07:00
Peter Steinberger
e3cbad4fb6 ci: fix ACPX Docker update repair target 2026-04-26 22:13:00 +01:00
Peter Steinberger
c082cf892a docs: codify formatter tooling 2026-04-26 22:02:31 +01:00
Peter Steinberger
b4a9ac3516 ci: run release Docker chunks through scheduler 2026-04-26 22:02:31 +01:00
Vincent Koc
f0566e410a docs(diagnostics): document model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
c6e9849351 feat(diagnostics): capture model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
8e1755928c refactor(plugins): split plugin registry facade 2026-04-26 13:43:22 -07:00
Vincent Koc
9eb071c3f1 perf(plugins): reuse persisted registry fallback read 2026-04-26 13:43:22 -07:00
Vincent Koc
522eedc754 refactor(plugins): make provider discovery runtime explicit 2026-04-26 13:43:21 -07:00
Vincent Koc
71e361af8a refactor(plugins): split installed plugin index modules 2026-04-26 13:43:21 -07:00
Peter Steinberger
487f8c5d3a test(gateway): skip codex acp bind when auth is unavailable 2026-04-26 21:42:49 +01:00
Peter Steinberger
7a4574376a fix(ollama): honor native model capabilities 2026-04-26 21:40:22 +01:00
Josh Lehman
8ba82534e6 fix: preserve cron telegram topic delivery after timeout (#72317) 2026-04-26 13:30:54 -07:00
Peter Steinberger
ffa84cdc02 ci: chunk release Docker e2e jobs 2026-04-26 21:23:08 +01:00
pash-openai
67ffa3df8b Add Codex Computer Use setup for Codex mode (#71842)
* Add Codex Computer Use setup

* Tighten Codex Computer Use setup checks

* Handle fresh Codex Computer Use marketplace setup

* Fix channel setup manifest fixture

* Match Codex Computer Use marketplace loading

* Harden plugin manifest test fixtures

* Isolate auth choice legacy manifest test

* Update aggregate shard test expectation

* Improve Codex Computer Use first-run setup

* Harden Codex Computer Use auto-install

* Fix plugin auto-enable test fixture roots
2026-04-26 13:21:56 -07:00
Vincent Koc
df542f75a9 fix(logging): expose trace fields in file logs 2026-04-26 12:52:04 -07:00
Peter Steinberger
edf40ab6c9 test(gateway): retry gemini acp startup warmup timeout 2026-04-26 20:50:06 +01:00
Vincent Koc
406ae72fd2 fix(logging): redact persisted transcript text 2026-04-26 12:12:44 -07:00
Peter Steinberger
f99fb2af86 test(gateway): wait longer for codex harness subagent start 2026-04-26 20:11:16 +01:00
Peter Steinberger
244628f467 docs: clarify PR triage comments 2026-04-26 19:48:22 +01:00
Sally O'Malley
637bd33e69 fix(diagnostics): defer OTEL run span finalization (#72260) 2026-04-26 11:29:05 -07:00
Vincent Koc
e53c068d78 fix: repair skills and memory watcher refresh paths 2026-04-26 11:21:21 -07:00
Peter Steinberger
4e181d30fa test(gateway): classify stream fallback as empty live response 2026-04-26 19:15:00 +01:00
Peter Steinberger
e60cc50dff test(gateway): harden acp bind docker smoke 2026-04-26 19:14:58 +01:00
Peter Steinberger
f2dab9b334 fix(agents): keep responses web search reasoning compatible 2026-04-26 19:14:55 +01:00
Peter Steinberger
fc6cfbd418 fix(agents): honor bundle mcp tool allowlist 2026-04-26 19:14:51 +01:00
Vincent Koc
480a3f66c9 fix: shortcut live session model redirects during fallback 2026-04-26 11:14:05 -07:00
Vincent Koc
19e41a1e69 docs(logging): clarify redaction surfaces 2026-04-26 11:09:56 -07:00
Vincent Koc
b4cdd55f62 fix(discord): escalate repeated health-monitor restarts 2026-04-26 11:09:03 -07:00
Vincent Koc
6b6dcafcee fix(webchat): support non-image file attachments 2026-04-26 10:58:24 -07:00
Vincent Koc
303cde8f60 fix(auto-reply): poison inbound dedupe after partial turn failure
* fix(auto-reply): poison inbound dedupe after replay-unsafe failures

* fix(clownfish): address review for ghcrawl-165980-agentic-merge (1)
2026-04-26 10:58:19 -07:00
Vincent Koc
e672b61417 fix(whatsapp): stop reconnecting quiet sockets
Fixes #70678.\n\nKeeps quiet but healthy WhatsApp linked-device sessions connected by tracking WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Also cleans up transport activity listeners on failed connection-open paths.\n\nCarries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.\n\nValidation:\n- pnpm test:serial extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts extensions/whatsapp/src/connection-controller.test.ts\n- pnpm check:changed\n- codex review --base origin/main
2026-04-26 09:51:41 -07:00
Peter Steinberger
4a3030df9e fix: avoid PowerShell error variable collision 2026-04-26 16:26:31 +01:00
Peter Steinberger
30aa1b5223 fix(release): stabilize beta validation lanes 2026-04-26 16:22:12 +01:00
Peter Steinberger
b438a9cc08 test: align Parallels smoke guards 2026-04-26 16:20:58 +01:00
Peter Steinberger
a87edd732d fix: harden Windows Parallels smoke 2026-04-26 16:13:13 +01:00
Peter Steinberger
79ad635515 fix: pass Linux clock sync as epoch 2026-04-26 16:13:13 +01:00
Peter Steinberger
7e51866d23 fix: sync Parallels Linux clock 2026-04-26 16:13:13 +01:00
Peter Steinberger
73affb491a fix: bound dev update cleanup 2026-04-26 16:13:13 +01:00
Peter Steinberger
ddc2036956 fix: stabilize Parallels plugin smoke paths 2026-04-26 16:13:13 +01:00
Peter Steinberger
631552c554 perf: speed up dispatch-from-config tests 2026-04-26 14:14:12 +01:00
Peter Steinberger
dce35b90fe test(release): wait longer for dashboard smoke 2026-04-26 13:53:59 +01:00
Peter Steinberger
fc666cf42a test(qa): allow slower gateway rpc startup retries 2026-04-26 13:51:40 +01:00
Peter Steinberger
67b9167b80 test(extensions): restore transformed dynamic imports 2026-04-26 13:16:05 +01:00
Peter Steinberger
e97bd70264 perf: speed up slow test imports 2026-04-26 13:10:57 +01:00
Peter Steinberger
9089e6b595 fix(cli): keep channel add plugin install noninteractive
# Conflicts:
#	CHANGELOG.md
2026-04-26 12:59:19 +01:00
Peter Steinberger
7e13f3f514 test(plugin-sdk): tighten channel runtime shim scan 2026-04-26 12:17:49 +01:00
Peter Steinberger
760a1525fb docs(plugin-sdk): refresh api baseline 2026-04-26 12:15:14 +01:00
Peter Steinberger
760dd98ddc fix(ci): repair main type and lint failures 2026-04-26 12:09:35 +01:00
Peter Steinberger
ecf71da888 fix(voice-call): avoid duplicate webhook logs 2026-04-26 12:05:34 +01:00
Vincent Koc
8a63c898c8 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  fix(plugins): satisfy doctor compat lint
  chore(plugins): inventory doctor deprecation compat
  fix(plugins): record crabpot compat deprecations
  docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults
2026-04-26 04:05:11 -07:00
Vincent Koc
efaa66f70d fix(plugins): satisfy doctor compat lint 2026-04-26 04:04:27 -07:00
Vincent Koc
4c40cf8783 chore(plugins): inventory doctor deprecation compat 2026-04-26 04:04:26 -07:00
Vincent Koc
6dfb03ab2e fix(plugins): record crabpot compat deprecations 2026-04-26 04:04:26 -07:00
Vincent Koc
3a54bbb617 fix(plugins): persist synthetic auth refs in index 2026-04-26 04:04:11 -07:00
Vincent Koc
2a5d3ad5b9 docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults 2026-04-26 04:04:09 -07:00
Peter Steinberger
a97ee5c1d3 fix(google-meet): recover local chrome tabs 2026-04-26 12:04:00 +01:00
Vincent Koc
647e557869 docs(agent-workspace): rewrite with AccordionGroup for file map, Steps and Tabs for git backup, Warning callouts for sandbox and secret risks 2026-04-26 04:03:00 -07:00
Peter Steinberger
2a26c96000 docs(release): refine beta validation guidance 2026-04-26 12:02:26 +01:00
Vincent Koc
fa4bd05a3a docs(models): rewrite with CardGroup, Steps for selection order, AccordionGroup for picker behavior and merge precedence, ParamField for list/scan flags 2026-04-26 04:01:42 -07:00
Vincent Koc
209522e2e0 docs(model-failover): rewrite with Steps for runtime flow and rotation, AccordionGroup for cooldown buckets and chain rules, Tabs for which errors advance fallback 2026-04-26 03:59:53 -07:00
Vincent Koc
652e8af81e docs(multi-agent): rewrite with Steps for routing tiers, Tabs for common patterns, AccordionGroup for platform examples and tie-breaking 2026-04-26 03:57:19 -07:00
Vincent Koc
c7a0d9b188 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  test(models): stabilize provider index list mocks
  test(cli): cover lazy plugin inspect mocks
  fix(cli): lazy load plugin maintenance paths
  fix(models): keep cold catalog lookup registry indexed
  fix(models): avoid registry for configured list
  fix(cli): lazy load model commands
  fix(ui): remove ineffective dynamic imports
  test: type setup provider mocks
  fix(update): complete channel switch follow-up work
  test(parallels): harden smoke agent model setup
  fix: preserve provider-scoped model options
  fix: keep post-auth model policy cold
  docs: note faster onboarding auth setup
  test: cover setup provider auth selection
  refactor: keep openai setup auth lightweight
  fix: use setup providers for auth choices
  fix: scope provider auth runtime loading
  fix: keep onboarding setup paths cold
  fix: keep onboarding model prompts scoped
2026-04-26 03:51:08 -07:00
Vincent Koc
3013916232 Update docker.md 2026-04-26 03:50:31 -07:00
Vincent Koc
5411f9d217 test(models): stabilize provider index list mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
be388084c2 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
e76bac5d14 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:49:56 -07:00
Vincent Koc
aec1bfa0bb fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:49:43 -07:00
Vincent Koc
8740ca7dee fix(models): avoid registry for configured list 2026-04-26 03:49:43 -07:00
Vincent Koc
23710167cd fix(cli): lazy load model commands 2026-04-26 03:49:43 -07:00
Vincent Koc
3a9463edac test(models): stabilize provider index list mocks 2026-04-26 03:47:25 -07:00
Vincent Koc
fc483ef5d0 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:47:24 -07:00
Vincent Koc
38ea99ec74 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:47:23 -07:00
Vincent Koc
9c25c697dd fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:45:46 -07:00
Vincent Koc
b7533f5112 fix(models): avoid registry for configured list 2026-04-26 03:45:45 -07:00
Vincent Koc
c3a81166fc fix(cli): lazy load model commands 2026-04-26 03:45:45 -07:00
Peter Steinberger
ab0d0f677b fix(ui): remove ineffective dynamic imports
(cherry picked from commit b4ff947206)
2026-04-26 11:45:29 +01:00
Peter Steinberger
06fe67d719 test: type setup provider mocks
(cherry picked from commit ea9da71f03)
2026-04-26 11:41:14 +01:00
Peter Steinberger
6a00be5f90 fix(update): complete channel switch follow-up work 2026-04-26 11:38:44 +01:00
Peter Steinberger
cd8187d7ce test(parallels): harden smoke agent model setup 2026-04-26 11:38:33 +01:00
Shakker
8344fae387 fix: preserve provider-scoped model options 2026-04-26 11:36:32 +01:00
Shakker
3fe0718932 fix: keep post-auth model policy cold 2026-04-26 11:36:32 +01:00
Shakker
cd3b871122 docs: note faster onboarding auth setup 2026-04-26 11:36:32 +01:00
Shakker
edcb2326a1 test: cover setup provider auth selection 2026-04-26 11:36:32 +01:00
Shakker
b11dbb49f9 refactor: keep openai setup auth lightweight 2026-04-26 11:36:32 +01:00
Shakker
44183de706 fix: use setup providers for auth choices 2026-04-26 11:36:32 +01:00
Shakker
3fffa78164 fix: scope provider auth runtime loading 2026-04-26 11:36:32 +01:00
Shakker
2f81c5f580 fix: keep onboarding setup paths cold 2026-04-26 11:36:32 +01:00
Shakker
26b203e573 fix: keep onboarding model prompts scoped 2026-04-26 11:36:32 +01:00
Peter Steinberger
c74fb78194 test: harden cron MCP Docker smoke 2026-04-26 11:33:26 +01:00
Peter Steinberger
cd79e01be3 fix: load default memory plugin at startup 2026-04-26 11:32:58 +01:00
Peter Steinberger
0e490a3c26 fix(plugins): serialize bundled runtime mirrors 2026-04-26 11:32:07 +01:00
Peter Steinberger
4506bb2e02 fix: stabilize channel MCP Docker smoke 2026-04-26 11:31:25 +01:00
Peter Steinberger
74a4ff1adc fix: prefer mounted bundled plugin sources 2026-04-26 11:28:41 +01:00
Peter Steinberger
8a52c7b3d9 test: cover ClawHub plugin install uninstall 2026-04-26 11:28:18 +01:00
Peter Steinberger
3979fce4f9 test: satisfy compat registry lint 2026-04-26 11:28:07 +01:00
Peter Steinberger
8f4f33be78 test: keep compat registry guard-safe 2026-04-26 11:25:02 +01:00
Peter Steinberger
46d74c8f09 docs: update changelog for native require loader (#71122) (thanks @Effet) 2026-04-26 11:23:42 +01:00
Effet
75c9b216e5 fixup! perf(plugins): native-require fast path respects tryNative=false
Review feedback from @chatgpt-codex-connector (P1): callers that pass
`tryNative: false` rely on jiti's alias rewriting (e.g.
`bundled-capability-runtime` in Vitest+dist mode narrows the SDK
slice through shim aliases). Route everything through the jiti
loader when `tryNative` is false so those rewrites still apply.

Review feedback from @greptile-apps (P2): forward the full argument
tuple through to the jiti fallback with `...rest` so any future
loader option argument is not silently dropped by the wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Effet
b40b85c21a perf(plugins): use native require for compiled JS before jiti
Every CLI invocation reads the config snapshot, which pulls bundled
channel doctor contracts and setup surfaces through
`getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds
several seconds of per-load overhead on slower hosts (NAS profiling
shows ~78% of `openclaw config get` wall time spent inside the jiti
library), and that overhead is pure waste for the already-compiled
`.js` artifacts shipped in dist/.

Wrap the loader returned by `getCachedPluginJitiLoader` so that
compiled JS targets go through `tryNativeRequireJavaScriptModule`
first. Jiti stays on the hot path for:
- TS/TSX/MTS/CTS sources
- paths the native-require helper declines (Windows by default, or
  module-resolution fallbacks)

This centralises the fast path that already existed — inside
`doctor-contract-registry` and `channel-entry-contract` — and extends
it to every caller that goes through the jiti loader cache.

Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord
configured):

| command          | before | after |
|------------------|-------:|------:|
| config get X     |    24s |    6s |
| status           |    45s |   18s |
| devices list     |    55s |   26s |
| nodes status     |    55s |   26s |

Fixes the slow config/status/devices/nodes read paths reported in
openclaw#62842. Remaining time is dominated by non-jiti code paths
(config schema validation, eager provider-plugin module eval) that
are out of scope for this patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Vincent Koc
6d60b035b4 chore(plugins): finish compat registry cleanup 2026-04-26 03:17:25 -07:00
Peter Steinberger
bc49fb1cdf test: fix extension dynamic imports 2026-04-26 11:15:45 +01:00
Peter Steinberger
9694c0611c ci: fix main gate 2026-04-26 11:15:45 +01:00
Peter Steinberger
4b2056fcc1 docs: document plugin package entrypoints 2026-04-26 11:12:09 +01:00
Peter Steinberger
a75c3adc4f refactor: centralize plugin update outcome logging 2026-04-26 11:11:58 +01:00
Peter Steinberger
b7404399ef perf: cache bundled runtime dep manifests 2026-04-26 11:11:58 +01:00
Peter Steinberger
f337c9019c refactor: share plugin package entry resolution 2026-04-26 11:11:58 +01:00
Peter Steinberger
8ba9c9098a fix(agents): avoid provider startup scans 2026-04-26 11:11:37 +01:00
Peter Steinberger
8bc4d4bcd4 fix: prevent duplicate chat attachment send races 2026-04-26 11:10:42 +01:00
Vincent Koc
dc05c93c02 chore(docker): expose diagnostics observability settings 2026-04-26 03:05:10 -07:00
Peter Steinberger
4ed97f7e35 docs: update changelog for plugin fixes 2026-04-26 11:01:10 +01:00
Peter Steinberger
f33a812c07 fix: validate plugin package extension entries 2026-04-26 11:01:10 +01:00
Peter Steinberger
d22d6aed16 fix: respect plugin allowlist for bundled deps 2026-04-26 11:01:10 +01:00
Peter Steinberger
93f2d42259 fix: fail plugin update on update errors 2026-04-26 11:01:10 +01:00
Vincent Koc
861cd026d1 docs(release): add plugin deprecation sweep 2026-04-26 02:59:29 -07:00
Peter Steinberger
9a529ca78b chore: update dependencies 2026-04-26 10:54:58 +01:00
Vincent Koc
9f0cd3514c test(plugins): make compat window guard type-safe 2026-04-26 02:52:45 -07:00
Vincent Koc
bb2425e612 test(plugins): enforce compat removal window 2026-04-26 02:51:48 -07:00
Vincent Koc
5baf90ffef chore(plugins): cap compat removal windows 2026-04-26 02:51:48 -07:00
Vincent Koc
3308347a43 fix(security): keep web search credential checks cold 2026-04-26 02:51:48 -07:00
Vincent Koc
22044af066 fix(config): keep command alias validation cold 2026-04-26 02:51:48 -07:00
Vincent Koc
a9d243327c chore(plugins): complete compat registry inventory 2026-04-26 02:51:47 -07:00
Peter Steinberger
975fd5bc8d docs: add gif asset hygiene guidance 2026-04-26 10:48:06 +01:00
Peter Steinberger
bd95baa4f7 fix(bonjour): suppress ciao process crashes 2026-04-26 10:47:36 +01:00
Peter Steinberger
1be39ac847 fix: increase update step timeout 2026-04-26 10:46:55 +01:00
Peter Steinberger
b67d9bf7f0 fix: propagate update timeout to plugin installs 2026-04-26 10:45:11 +01:00
Vincent Koc
d1f40731e3 chore(ci): tune stale assigned triage 2026-04-26 02:42:09 -07:00
Peter Steinberger
4bc5e183ef fix: avoid CLI startup warmup leaks 2026-04-26 10:41:03 +01:00
Vincent Koc
64af2feda0 docs(context-engine): note that uninstalling the selected context engine plugin resets plugins.slots.contextEngine to the default (c6b7444d16) 2026-04-26 02:39:07 -07:00
Vincent Koc
8314b83f9d docs(agents): scope docs-only validation 2026-04-26 02:35:14 -07:00
Peter Steinberger
2aa375149f test: speed up agent hotspot tests 2026-04-26 10:28:04 +01:00
Peter Steinberger
0b301e9af4 fix: avoid eager channel setup loading 2026-04-26 10:27:35 +01:00
Peter Steinberger
6bc5fe6952 fix: harden plugin install and uninstall transactions 2026-04-26 10:27:23 +01:00
Vincent Koc
893f070560 docs(prometheus): rewrite with Steps quick start, Tabs for enable methods and pull-vs-push, AccordionGroup for label policy and troubleshooting; document the 2048-series cap and trusted-operator scope from the diagnostics-prometheus plugin code 2026-04-26 02:26:08 -07:00
Peter Steinberger
9eb0934492 test: tighten changed test routing 2026-04-26 10:25:04 +01:00
Peter Steinberger
87ac8b0456 refactor(discord): use Carbon request client for proxy fetch 2026-04-26 10:20:49 +01:00
Peter Steinberger
a3483acaab fix: stabilize gpt55 qa lab scenarios 2026-04-26 10:18:42 +01:00
Vincent Koc
0f2e7510cb feat(diagnostics-prometheus): add protected metrics exporter 2026-04-26 02:15:33 -07:00
Peter Steinberger
6cd047e7c2 refactor: clean up update and plugin uninstall helpers 2026-04-26 10:07:39 +01:00
Peter Steinberger
d58ede1b34 docs(changelog): keep discord fix scoped 2026-04-26 10:06:38 +01:00
Peter Steinberger
775c61ef5f fix(discord): ignore stale exec approval clicks 2026-04-26 10:06:38 +01:00
Vincent Koc
57a77ecdf9 docs(multi-agent-sandbox-tools): rewrite with CardGroup, AccordionGroup for examples and troubleshooting, Tabs for restrictions, Steps for filter order 2026-04-26 02:00:56 -07:00
Peter Steinberger
382c554786 docs(release): keep 2026.4.26 changelog marker empty 2026-04-26 09:59:42 +01:00
Peter Steinberger
e6c9123262 docs(release): codify beta train backport scan
(cherry picked from commit b7733c48c0)
2026-04-26 09:59:42 +01:00
Vincent Koc
e400295969 docs(cli-gateway): rewrite with CardGroup, ParamField for run/probe/install flags, AccordionGroup for status semantics and probe interpretation 2026-04-26 01:59:27 -07:00
Vincent Koc
da000ce511 docs(changelog): note subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a911eb748b test(qa): cover subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a1b6567059 fix(agents): fallback subagent completion delivery 2026-04-26 01:58:00 -07:00
Vincent Koc
8741a86f93 docs(broadcast-groups): rewrite with AccordionGroup for use cases and best practices, Tabs for strategy and contexts, Steps for message flow 2026-04-26 01:56:29 -07:00
Vincent Koc
ed537edacf docs(twitch): rewrite with Steps for setup, Tabs for install/auth/access patterns, ParamField for account config, AccordionGroup for troubleshooting 2026-04-26 01:55:13 -07:00
Vincent Koc
91666fe194 docs(cli-plugins): rewrite with CardGroup, AccordionGroup for install/update behavior, ParamField for list flags, Tabs for marketplace sources 2026-04-26 01:53:57 -07:00
Peter Steinberger
c6b7444d16 fix(plugins): reset context engine slot on uninstall 2026-04-26 09:50:34 +01:00
Peter Steinberger
42487d0dac fix(update): retry npm updates without optional deps 2026-04-26 09:50:27 +01:00
Peter Steinberger
832bdbc777 fix(update): repair package config after update 2026-04-26 09:50:19 +01:00
Peter Steinberger
d9c5040fc5 docs(tailscale): clarify Control UI pairing 2026-04-26 09:46:59 +01:00
Peter Steinberger
6f50253a4d fix: clarify install switching 2026-04-26 09:46:41 +01:00
Peter Steinberger
aad7b678b0 fix: pass config to plugin command specs 2026-04-26 09:45:05 +01:00
Peter Steinberger
e29d3516bf fix(gateway): skip Tailscale Control UI pairing 2026-04-26 09:42:25 +01:00
Peter Steinberger
5ab5b75348 fix: update Docker plugin registry smokes 2026-04-26 09:42:14 +01:00
Vincent Koc
2652c9eacf fix(configure): defer web search setup runtime
Keep web-search configure and channel command defaults on cold plugin metadata, harden persisted registry reads, and require active config for manifest command defaults.\n\nThanks @vincentkoc
2026-04-26 01:41:57 -07:00
Peter Steinberger
218636a0ea docs(changelog): split 2026.4.25 and 2026.4.26 notes 2026-04-26 09:40:00 +01:00
Vincent Koc
f164b8b357 docs(webchat): note that reasoning-flagged payloads are excluded from WebChat assistant content, transcript text, and audio blocks (4823288b3b) 2026-04-26 01:39:34 -07:00
Vincent Koc
abd5ec98ab fix(runtime): harden dependency install surfaces (#71997)
* fix(runtime): harden dependency surfaces

* fix(runtime): harden dependency install surfaces

* fix(runtime): address dependency surface review

* fix(runtime): address dependency surface review

* fix(channels): avoid read-only plugin loader cycle

* fix(channels): allow optional read-only loader workspace

* test(commands): refresh current main checks

* test(commands): keep provider metadata mock unique

* test(commands): keep doctor security read-only mock unique
2026-04-26 01:38:21 -07:00
Vincent Koc
eb6b35671a docs(changelog): flatten 27 multi-line bullets into single lines per AGENTS.md rule 2026-04-26 01:35:42 -07:00
Peter Steinberger
3b5463591b chore: bump version to 2026.4.26 2026-04-26 09:28:52 +01:00
Peter Steinberger
4ad8b613c9 test: update npm telegram workflow expectations 2026-04-26 09:24:10 +01:00
Peter Steinberger
1969452c3f fix: hide raw agent failures in group chats 2026-04-26 09:19:27 +01:00
Peter Steinberger
134cc64aff fix: keep host plugin registry out of live Docker state 2026-04-26 09:17:38 +01:00
Peter Steinberger
0c020cdb7a test: update ci expectation drift 2026-04-26 09:16:53 +01:00
Peter Steinberger
2f5e5e9a71 fix: break plugin command spec import cycle 2026-04-26 09:15:47 +01:00
Peter Steinberger
1323683d72 fix: stabilize qa lab capture store cleanup 2026-04-26 09:13:30 +01:00
Ayaan Zaidi
7e376e5aba ci: build npm telegram e2e image after approval 2026-04-26 13:39:18 +05:30
Peter Steinberger
e2ef5e2329 test: keep path alias temp dirs out of repo 2026-04-26 09:09:07 +01:00
Peter Steinberger
c99d72575e fix(release): reject staged runtime deps in packs 2026-04-26 09:08:54 +01:00
Shakker
5c0dc93d1e fix(doctor): keep service repair policy scoped 2026-04-26 09:08:36 +01:00
Shakker
6cf5a5fbcd docs: document external service repair policy 2026-04-26 09:08:36 +01:00
Shakker
0b6ebf3343 fix(doctor): honor external service repair policy 2026-04-26 09:08:35 +01:00
Vincent Koc
d24c6095ce docs(sdk-setup): rewrite with Tabs for package metadata and install paths, ParamField for openclaw fields, AccordionGroup for setup-entry rules and helpers 2026-04-26 01:07:59 -07:00
Vincent Koc
64a7a34c83 docs(trusted-proxy-auth): rewrite with Steps for handshake, Tabs for TLS, AccordionGroup for proxy examples and troubleshooting 2026-04-26 01:04:51 -07:00
Vincent Koc
f2744978a0 docs(slash-commands): rewrite with ParamField for config keys, AccordionGroup for command groups and surface notes 2026-04-26 01:02:55 -07:00
Shakker
5037298d82 test: update channel status label fixtures 2026-04-26 09:01:39 +01:00
Shakker
0a82c819bb fix: keep status channel metadata cold 2026-04-26 09:01:39 +01:00
Peter Steinberger
a434133aac fix: fail update on plugin sync errors 2026-04-26 09:01:18 +01:00
Peter Steinberger
4823288b3b fix(gateway): hide webchat reasoning payloads 2026-04-26 09:00:56 +01:00
Peter Steinberger
164aaa48db style: format gateway imports 2026-04-26 09:00:33 +01:00
Peter Steinberger
878e1a2201 fix(plugins): preload cli backend runtime owners 2026-04-26 08:59:41 +01:00
Vincent Koc
6360e1146f docs(media-understanding): rewrite with Steps for behavior and auto-detect, Tabs for config examples and entries, ParamField for attachments 2026-04-26 00:58:31 -07:00
Peter Steinberger
626313a397 fix: satisfy diagnostic trace lint 2026-04-26 08:57:49 +01:00
Peter Steinberger
606a7dbc75 test: stabilize telegram command pagination retry 2026-04-26 08:57:49 +01:00
Peter Steinberger
7cbe271d08 fix: keep channel command defaults read-only 2026-04-26 08:57:49 +01:00
Vincent Koc
06d409dc27 docs(mattermost): rewrite with Steps for setup and HMAC, Tabs for chatmodes, AccordionGroup for slash commands and troubleshooting 2026-04-26 00:56:05 -07:00
Shakker
295bcde7b8 test: update channel metadata mocks 2026-04-26 08:41:34 +01:00
Peter Steinberger
8d50cd82d3 docs(changelog): finalize 2026.4.25 release notes 2026-04-26 08:41:14 +01:00
Vincent Koc
32d3a820c8 docs(sdk-runtime): rewrite with AccordionGroup for runtime namespaces, Steps for store wiring, ParamField for top-level api fields 2026-04-26 00:40:41 -07:00
Vincent Koc
1dc57d4c31 docs(groups): rewrite with Tabs for sandbox patterns and copy-paste intents, AccordionGroup for per-channel notes, Steps for evaluation order 2026-04-26 00:38:20 -07:00
Vincent Koc
fe69b02951 docs(sandboxing): rewrite with Tabs for modes/backends/workspace, AccordionGroup for SSH/OpenShell details, Steps for image setup 2026-04-26 00:35:52 -07:00
Vincent Koc
3e2e26549a docs(cli-config): rewrite with Tabs for set modes, AccordionGroup for builder flags and dry-run details, Steps for the repair loop 2026-04-26 00:32:59 -07:00
Peter Steinberger
4c7a94aac4 fix: quote Windows UI runner paths 2026-04-26 08:31:00 +01:00
Vincent Koc
434c8a1c91 docs(heartbeat): rewrite with Steps for quick start, ParamField for field notes, AccordionGroup for delivery and tasks behavior 2026-04-26 00:30:47 -07:00
Shakker
04575333d3 chore: ignore local agent skills 2026-04-26 08:26:28 +01:00
Shakker
50558e0d56 docs: note channel runtime laziness fixes 2026-04-26 08:26:28 +01:00
Shakker
8fe449c883 fix: avoid channel runtime in format summaries 2026-04-26 08:26:27 +01:00
Shakker
8b32c31252 fix: keep thread placement metadata cold 2026-04-26 08:26:27 +01:00
Shakker
2e101e8413 fix: keep channel security checks cold 2026-04-26 08:26:27 +01:00
Vincent Koc
a77996dc56 fix(diagnostics): propagate trusted traceparent headers 2026-04-26 00:24:47 -07:00
Vincent Koc
5e8fda4c64 docs(memory-config): rewrite with CardGroup overview links, Steps for auto-detect, AccordionGroup for provider configs and QMD subsections 2026-04-26 00:21:28 -07:00
Peter Steinberger
76cf013df5 test: remove slow reply bypass from docker smoke 2026-04-26 08:19:23 +01:00
Vincent Koc
450dc3a206 docs(control-ui): rewrite with Steps for pairing, AccordionGroup for capabilities and chat behavior, Tabs for tailnet access 2026-04-26 00:18:47 -07:00
Peter Steinberger
7b438965bd test: stabilize docker mcp readiness smokes 2026-04-26 08:17:28 +01:00
Peter Steinberger
c5bbf83904 fix: skip unresolved delivery targets for no-deliver cron 2026-04-26 08:17:28 +01:00
Peter Steinberger
f4f74a2391 docs(changelog): organize unreleased notes 2026-04-26 08:15:34 +01:00
Vincent Koc
0c8f0aacf5 docs(plugin-architecture): rewrite with AccordionGroup for shapes and ownership, Steps for the architecture pipeline, Tabs for layering 2026-04-26 00:15:25 -07:00
Peter Steinberger
1de4aff06d fix: cover Windows pnpm and Lobster install regressions 2026-04-26 08:14:28 +01:00
Peter Steinberger
5b9be2cdb1 fix: migrate agent runtime config 2026-04-26 08:12:44 +01:00
Vincent Koc
9d6e79019f docs(secrets): rewrite with Tabs for SecretRef sources, AccordionGroup for providers and exec examples, Steps for the audit flow 2026-04-26 00:12:05 -07:00
Shakker
b5e4e2f257 Revert "fix(plugins): persist registry contribution metadata"
This reverts commit 1ee5654220.
2026-04-26 08:11:09 +01:00
Shakker
59d1fa65df docs: note plugin uninstall file cleanup 2026-04-26 08:11:09 +01:00
Shakker
6428440086 fix: remove plugins from recorded install roots 2026-04-26 08:11:09 +01:00
Peter Steinberger
d419fb561d feat(tts): resolve channel account config generically 2026-04-26 08:10:36 +01:00
Vincent Koc
6c60cd2b72 docs(mcp): rewrite with Steps for lifecycle, Tabs for client modes, ParamField for serve options, AccordionGroup for tools 2026-04-26 00:08:16 -07:00
Vincent Koc
1ee5654220 fix(plugins): persist registry contribution metadata 2026-04-26 00:03:21 -07:00
Peter Steinberger
54f8e4145e test: speed up provider and security tests 2026-04-26 07:59:32 +01:00
Peter Steinberger
d1e5f4bd3c fix(update): bound Windows scheduled task stop 2026-04-26 07:56:46 +01:00
Shakker
3ad29972d0 docs: note read-only channel command discovery 2026-04-26 07:55:00 +01:00
Shakker
43557b16a6 fix: keep channel command discovery read-only 2026-04-26 07:55:00 +01:00
Shakker
fd97f530e3 docs: note cold session metadata fix 2026-04-26 07:55:00 +01:00
Shakker
bbed91bf71 fix: avoid session metadata channel runtime fallback 2026-04-26 07:55:00 +01:00
Shakker
49b106d357 docs: note cold native command defaults 2026-04-26 07:55:00 +01:00
Shakker
7a7728db13 fix: keep native command auto defaults cold 2026-04-26 07:55:00 +01:00
Shakker
aee4c92344 docs: note provider index disablement fix 2026-04-26 07:55:00 +01:00
Shakker
78fb0ade09 fix: honor plugin disablement in provider index rows 2026-04-26 07:55:00 +01:00
Vincent Koc
f48dc96d43 docs(opentelemetry): document harness lifecycle metric, span, and diagnostic events from 82ddcf24f5 2026-04-25 23:54:30 -07:00
Vincent Koc
ff7f0df871 docs(config-tools): rewrite with AccordionGroup for provider examples and field details, ParamField for loop detectors 2026-04-25 23:51:26 -07:00
Peter Steinberger
4ee537a04a fix(node-runtime): keep node-host recovering after gateway restarts 2026-04-26 07:49:45 +01:00
Vincent Koc
c7ead7d8a9 fix(cli): lazy-load model auth runtime 2026-04-25 23:49:06 -07:00
Vincent Koc
62869c8502 docs(bluebubbles): rewrite with Steps for setup, Tabs for DM/groups and coalescing, AccordionGroup for actions and config 2026-04-25 23:48:13 -07:00
Vincent Koc
bb0ef5ef18 docs(troubleshooting): rewrite with AccordionGroup for symptom signatures, Steps for fix flows, and Warning callouts 2026-04-25 23:44:25 -07:00
Harry Xie
77719899f3 fix(gateway): refresh stale embedded service tokens
Refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation.

Fixes #70752.
Thanks @hyspacex.

Co-authored-by: Harry Xie <harryhsieh963@yahoo.com>
2026-04-26 07:42:14 +01:00
Vincent Koc
8c87a637e9 docs(doctor): rewrite with Tabs for headless flags and AccordionGroup for the 19+ detailed behaviors 2026-04-25 23:40:24 -07:00
Vincent Koc
c4a39a6819 docs(model-providers): rewrite with AccordionGroup, CardGroup, Tabs, and Steps for cleaner provider scan 2026-04-25 23:36:01 -07:00
Vincent Koc
82ddcf24f5 feat(diagnostics): add harness lifecycle telemetry 2026-04-25 23:34:34 -07:00
Peter Steinberger
8bbb143ab8 fix: enforce device token scope containment 2026-04-26 07:28:21 +01:00
Peter Steinberger
26e4eb8e40 fix(update): ignore plugin install stages in dist verify 2026-04-26 07:28:02 +01:00
Peter Steinberger
8368026986 fix(installer): preserve PowerShell host on failure 2026-04-26 07:23:48 +01:00
Peter Steinberger
1fae716a04 fix: recover stale cron task records 2026-04-26 07:23:39 +01:00
Ayaan Zaidi
9d21200049 test(e2e): cover npm onboarding runtime deps 2026-04-26 11:53:17 +05:30
Peter Steinberger
7091dbe2bf docs: prefer ghcrawl for OpenClaw issue triage 2026-04-26 07:19:00 +01:00
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
Vincent Koc
64582bb3a7 docs(diagnostics-otel): clarify genai semconv exports 2026-04-25 12:30:14 -07:00
Peter Steinberger
d4971aad2c docs: require feasible live verification 2026-04-25 20:27:21 +01:00
Peter Steinberger
30325f567c fix: use prompt snapshots for live context diagnostics 2026-04-25 20:25:44 +01:00
Peter Steinberger
b732f21a86 fix: clarify voice-call setup diagnostics 2026-04-25 20:24:36 +01:00
Vincent Koc
44648440a5 fix(diagnostics-otel): stabilize genai token metric model attr 2026-04-25 12:22:55 -07:00
Peter Steinberger
75d64cd4b8 feat: expose generic image background option 2026-04-25 20:21:46 +01:00
Peter Steinberger
03fd7df929 fix: remove duplicate diagnostic stability case 2026-04-25 20:21:39 +01:00
Peter Steinberger
d757396785 test(ui): consolidate chat jsdom suites 2026-04-25 20:17:23 +01:00
Peter Steinberger
7436e395d5 test(node-host): cache native binary fixture lookup 2026-04-25 20:17:23 +01:00
Peter Steinberger
f34513ac66 perf(memory): avoid duplicate session store reads 2026-04-25 20:17:22 +01:00
Vincent Koc
5815ca93d9 fix(diagnostics-otel): honor genai usage semconv opt-in 2026-04-25 12:13:50 -07:00
Peter Steinberger
86d897cfaa feat(android): expose talk mode
Co-authored-by: alex-latitude <213670856+alex-latitude@users.noreply.github.com>
2026-04-25 20:12:38 +01:00
Peter Steinberger
791ad0864a fix: strip invalid thinking replay signatures
Fixes #45010.
Supersedes #70054.

Co-authored-by: Chris Staples <chris.staples@sophos.com>
Co-authored-by: Fourier <yang.fourier@gmail.com>
2026-04-25 20:12:30 +01:00
Peter Steinberger
47a63f7acf fix(logging): merge duplicate context diagnostic case 2026-04-25 20:11:08 +01:00
Peter Steinberger
e6ab61762a fix(check): pass lock env to changed lint lanes 2026-04-25 20:11:08 +01:00
Peter Steinberger
1e7ae07772 fix(cli): dedupe onboard auth flags for completion cache 2026-04-25 20:11:08 +01:00
Peter Steinberger
d9486c683b fix: stabilize macos npm update smoke 2026-04-25 20:09:32 +01:00
Peter Steinberger
17401e31de fix: avoid changed gate lint self-lock 2026-04-25 20:09:00 +01:00
mushuiyu_xydt
0e1ef93e84 fix(minimax): use dedicated image generation endpoint (#61155)
* fix(minimax): use dedicated image generation endpoint

MiniMax image generation uses a dedicated API endpoint
(api.minimax.io/v1/image_generation) that is separate from the
text/chat API endpoint (api.minimax.io/anthropic).

Previously, the resolveMinimaxImageBaseUrl function would extract
the origin from the provider's configured baseUrl. If a user had
configured their baseUrl to the chat endpoint (e.g.,
api.minimax.chat/anthropic), the image generation would incorrectly
use that endpoint, resulting in "invalid api key" errors.

This fix always uses the dedicated image generation endpoint,
ignoring the provider's baseUrl configuration for image generation.

Fixes #61149

* fix(minimax): support CN endpoint for image generation

Respect MINIMAX_API_HOST environment variable to determine whether
to use the global (api.minimax.io) or CN (api.minimaxi.com) endpoint
for image generation.

This ensures that CN users who configure MINIMAX_API_HOST to use
api.minimaxi.com will continue to use the CN endpoint for image
generation, while global users continue to use api.minimax.io.

The original bug was caused by the code extracting the origin from
the provider's configured baseUrl, which could be set to incorrect
endpoints like api.minimax.chat. This fix uses the dedicated image
generation endpoints instead.

Fixes #61149

* fix(minimax): infer CN endpoint from provider config when env is unset

When MINIMAX_API_HOST is not set, fall back to checking the provider's
configured baseUrl to determine whether to use the CN or global image
endpoint. This ensures CN users who went through onboarding (which sets
models.providers.minimax.baseUrl to https://api.minimaxi.com/anthropic)
are correctly routed to the CN image endpoint.

The isMinimaxCnHost check ensures we only use the baseUrl origin for
CN detection - invalid endpoints like api.minimax.chat would not match
minimaxi.com and would correctly fall through to the global default.

Fixes #61149

* test(minimax): cover dedicated image endpoints

* fix(logging): handle context assembly diagnostics

* Revert "fix(logging): handle context assembly diagnostics"

This reverts commit f51d2f7d67f8193268dd37553ac77e80a0423390.

* test(minimax): isolate image endpoint env

* docs(changelog): credit minimax image fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 20:07:52 +01:00
Quratulain-bilal
7d58362f3f docs(browser): note tilde expansion also covers per-profile paths (#71601)
* docs(browser): note tilde expansion also covers per-profile paths

The 95a2c9b fix expanded "~" for both `browser.executablePath` and
per-profile `profiles.<name>.executablePath` (config.ts:382 calls
`normalizeExecutablePath` for profile overrides). Per-profile
`userDataDir` on existing-session profiles is also tilde-expanded
(config.ts:391 via `resolveUserPath`). The configuration reference
only mentioned the top-level `browser.executablePath` case.

* docs(browser): align tilde path config help

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 20:05:03 +01:00
Vincent Koc
5671fdca87 feat(diagnostics-otel): add genai usage span identity 2026-04-25 12:03:10 -07:00
Peter Steinberger
5eab16e086 fix: improve google meet setup diagnostics 2026-04-25 20:01:24 +01:00
Peter Steinberger
e36b77c13e docs(changelog): drop self-thanks 2026-04-25 20:01:00 +01:00
Peter Steinberger
d68574653e docs(changelog): split 2026.4.24 and 2026.4.25 notes 2026-04-25 19:59:54 +01:00
Quratulain-bilal
8170df9127 docs(browser): document local startup timeout bounds (#71672)
* docs(browser): document local startup timeout bounds

The new browser.localLaunchTimeoutMs and browser.localCdpReadyTimeoutMs
options are clamped to MAX_BROWSER_STARTUP_TIMEOUT_MS (120000 ms) by
normalizeStartupTimeoutMs in extensions/browser/src/browser/config.ts,
and zero/negative/non-finite values fall back to the defaults. Without
this in the configuration reference, users setting a higher value see
no error and silently get the 120 s ceiling, or set 0 expecting 'no
timeout' and silently get the default.

* docs(browser): clarify startup timeout validation

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 19:59:53 +01:00
Peter Steinberger
b66f01bdca fix: expose transparent image infer options 2026-04-25 19:58:41 +01:00
Vincent Koc
cd7a8f870b feat(diagnostics-otel): add genai usage span attrs 2026-04-25 11:56:13 -07:00
91wan
bb2b68b34e fix(acp): pass Codex ACP model thinking overrides
Fix ACP Codex model/thinking override propagation.\n\nThanks @91wan.
2026-04-25 19:56:03 +01:00
Peter Steinberger
e9d9726f2d fix: handle context assembled diagnostics 2026-04-25 19:54:28 +01:00
Peter Steinberger
a018db771d fix: preserve omitted thinking replay turns 2026-04-25 19:54:28 +01:00
Peter Steinberger
690c98ad99 test(plugins): align install ledger mocks 2026-04-25 19:54:12 +01:00
Vincent Koc
c410e48382 fix(plugins): keep onboarding install records out of config 2026-04-25 11:52:19 -07:00
Peter Steinberger
bbc0884e23 docs(changelog): restore 2026.4.24 release notes 2026-04-25 19:51:11 +01:00
Vincent Koc
9bd348fdec fix(plugins): harden install ledger path handling 2026-04-25 11:48:17 -07:00
Vincent Koc
dc19069d71 feat(diagnostics-otel): add genai operation duration metric 2026-04-25 11:48:10 -07:00
Peter Steinberger
81307fc11d test: hoist backup archive mocks 2026-04-25 19:48:03 +01:00
Peter Steinberger
599ae7fed8 docs: clarify tool result details persistence 2026-04-25 19:47:19 +01:00
Peter Steinberger
fecf1e9b8f fix: align plugin install tests with ledger store 2026-04-25 19:44:11 +01:00
Peter Steinberger
4c0e9a4b2e fix(plugins): honor inferred agent model defaults 2026-04-25 19:40:32 +01:00
Peter Steinberger
cd8cb8254a fix(logging): remove duplicate context diagnostic case 2026-04-25 19:39:20 +01:00
Peter Steinberger
2055e6ceba fix(logging): include context assembly diagnostics in stability log 2026-04-25 19:39:20 +01:00
Peter Steinberger
8ea3099cd3 test(codex): accept visible session model reply 2026-04-25 19:39:20 +01:00
Peter Steinberger
e4f544790c test: isolate gateway live model sessions 2026-04-25 19:39:20 +01:00
Peter Steinberger
02639d3ec8 fix(plugins): alias wildcard runtime dependency exports 2026-04-25 19:39:20 +01:00
Peter Steinberger
14c9cfb637 fix(plugins): alias runtime dependency export subpaths 2026-04-25 19:39:20 +01:00
Peter Steinberger
9e9aa4722a fix(plugins): load mirrored runtime deps through ESM-safe aliases 2026-04-25 19:39:20 +01:00
Peter Steinberger
d2ab6b4fd5 fix(plugins): preserve package deps for runtime mirrors 2026-04-25 19:39:19 +01:00
Troy Hitch
63241bf1e0 fix(bonjour): suppress ciao cancellation across plugin runtime copies
Fix the bundled Bonjour gateway discovery crash-loop caused by ciao probe cancellation rejections after the Bonjour plugin migration.

The plugin entry now wires the existing rejection handler into the advertiser, and the unhandled-rejection handler registry is anchored on globalThis so staged plugin SDK module copies register into the same process-level handler set used by the host.

Verification:
- pnpm test:serial extensions/bonjour/src/advertiser.test.ts src/infra/unhandled-rejections.fatal-detection.test.ts
- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed partially completed: conflict markers plus core/core-test/extensions/extension-test typecheck passed; local lint lane hit a self-lock and was stopped.
2026-04-25 11:38:30 -07:00
Vincent Koc
888448facc feat(plugins): move install records to managed ledger 2026-04-25 11:37:10 -07:00
Peter Steinberger
e473577eaa test(voice): harden live STT transcript checks 2026-04-25 19:36:01 +01:00
Vincent Koc
f204f0c999 docs(logging): document new OTEL metrics and spans from recent diagnostics-otel feats
Five recent diagnostics-otel feat commits added user-facing OpenTelemetry
surfaces but did not update docs/logging.md, so the listed metrics and
spans drifted out of sync with what the plugin actually exports:

- 7bbd47349e adds gen_ai.client.token.usage histogram (GenAI semconv)
- b8a41739d5 adds memory heap/rss histograms, pressure counter and span
- d6ef1fcf24 adds openclaw.tool.loop counters and span
- ff172f46a5 adds openclaw.context.assembled span
- 44114328b4 adds openclaw.provider.request_id_hash attr on
  openclaw.model.call spans

Append the new metrics under existing model-usage and exec sections,
add a 'Diagnostics internals' subsection for memory + tool-loop
metrics, and add the three new spans (context.assembled, tool.loop,
memory.pressure) plus the request-id-hash attribute to the spans
listing.
2026-04-25 11:35:20 -07:00
Vincent Koc
7bbd47349e feat(diagnostics-otel): add genai token usage metric 2026-04-25 11:31:45 -07:00
Peter Steinberger
73706ca244 test: stabilize QA session memory ranking 2026-04-25 19:30:28 +01:00
Peter Steinberger
de0097a23c fix: support transparent OpenAI image generation 2026-04-25 19:28:56 +01:00
Peter Steinberger
0bf4876add fix: sanitize assembled diagnostic context 2026-04-25 19:23:51 +01:00
Peter Steinberger
a00c225899 test: split pure tool-card coverage 2026-04-25 19:23:51 +01:00
Peter Steinberger
e1495c3372 test: streamline memory and tts suites 2026-04-25 19:23:51 +01:00
Peter Steinberger
75fcb8c56d perf: lazy-load heavy test imports 2026-04-25 19:23:51 +01:00
Peter Steinberger
31456e3326 fix(providers): handle proxied DeepSeek V4 replay 2026-04-25 19:23:15 +01:00
Vincent Koc
b8a41739d5 feat(diagnostics-otel): export memory diagnostics 2026-04-25 11:22:19 -07:00
Peter Steinberger
1380dc170e fix(browser): avoid restart hint for external profiles 2026-04-25 19:18:06 +01:00
Vincent Koc
d6ef1fcf24 feat(diagnostics-otel): export tool loop events 2026-04-25 11:11:56 -07:00
Peter Steinberger
830bd2e236 fix: recover stale runtime deps locks 2026-04-25 19:09:09 +01:00
Poo-Squirry
fd3840cb00 Fix context usage display and active-run reload interruptions
Fixes context usage display regressions and prevents active runs from being interrupted by channel reloads. Adds persisted tool-result detail bounds so large tool metadata stays out of model/session payloads.
2026-04-25 19:07:52 +01:00
Chris Zhang
c3bfd328ad feat(litellm): add image generation provider (#70246)
* feat(litellm): add image generation provider

Registers litellm as an image-generation provider so model refs like
litellm/gpt-image-2 route through the LiteLLM proxy, and
agents.defaults.imageGenerationModel.fallbacks entries of the form
litellm/... resolve without "No image-generation provider registered
for litellm" errors.

Implementation uses the OpenAI-compatible /images/generations and
/images/edits endpoints that LiteLLM proxies for. BaseUrl resolves from
models.providers.litellm.baseUrl (default http://localhost:4000). Private
network is auto-allowed when baseUrl is a loopback/RFC1918 address, which
covers the common self-hosted LiteLLM proxy case without needing
OPENCLAW_PROVIDER_ALLOW_PRIVATE_NETWORK. Public baseUrls keep normal SSRF
defaults.

Default model is gpt-image-2 (matching upstream 4.21+ OpenAI default).
Advertises the same 2K/4K sizes OpenAI now exposes, plus legacy
256/512/1024 for dall-e-3. Supports both generate and edit.

Local patch. LiteLLM has no upstream image-generation support yet; revisit
if upstream adds one.

* ci: rerun after upstream main hot-fix

* fix(litellm): harden image generation provider

---------

Co-authored-by: Chris Zhang <chris@ChrisdeMac-mini.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 19:06:51 +01:00
Chunyue Wang
930d81aa41 fix(agents): prevent Bedrock replay death loop on empty assistant content (#71627)
* fix(agents): prevent Bedrock replay death loop on empty assistant content

  Fixes #71572

* docs: document Bedrock replay repair (#71627) (thanks @openperf)

* fix(diagnostics): share diagnostic event state across sdk graphs

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 19:04:40 +01:00
Vincent Koc
ff172f46a5 feat(diagnostics-otel): add context assembly spans 2026-04-25 11:03:46 -07:00
Peter Steinberger
afd6b5d6fc fix(opencode-go): route DeepSeek V4 through OpenAI transport 2026-04-25 18:58:08 +01:00
Vincent Koc
275c128e99 feat(plugins): add sanitized model call hooks 2026-04-25 10:56:40 -07:00
Peter Steinberger
9ffe764416 fix(whatsapp): send voice note text separately 2026-04-25 18:55:03 +01:00
Peter Steinberger
617e1dd6bf fix(browser): honor remote CDP open timeouts 2026-04-25 18:52:57 +01:00
Peter Steinberger
d623354a0e fix(infra): share diagnostic event state across loaders 2026-04-25 18:52:38 +01:00
Vincent Koc
44114328b4 feat(diagnostics): surface provider request id hashes 2026-04-25 10:46:10 -07:00
Peter Steinberger
2e0ae56b1a test(plugins): satisfy readonly index lint 2026-04-25 18:44:29 +01:00
Peter Steinberger
cd6c64d2ee test(plugins): avoid readonly index mutation 2026-04-25 18:42:25 +01:00
Peter Steinberger
649a645492 test(core): trim sync test overhead 2026-04-25 18:41:21 +01:00
Peter Steinberger
39488dfd68 test(pairing): reduce fixture io overhead 2026-04-25 18:41:20 +01:00
Peter Steinberger
8c93745f0f test(memory): speed up host fixture setup 2026-04-25 18:41:20 +01:00
Vincent Koc
f56bf63b06 fix(plugins): reject stale registry policy reads 2026-04-25 10:35:36 -07:00
Vincent Koc
61b3c04424 test(plugins): cover registry refresh mutations 2026-04-25 10:35:36 -07:00
Vincent Koc
3ec92dfac0 fix(plugins): deprecate registry disable break glass 2026-04-25 10:35:36 -07:00
Vincent Koc
4324855a9d docs(plugins): document persisted registry repair 2026-04-25 10:35:35 -07:00
Vincent Koc
fd8a8789d0 fix(plugins): satisfy registry lint 2026-04-25 10:35:35 -07:00
Vincent Koc
2f622acec6 fix(plugins): normalize startup config from registry 2026-04-25 10:35:35 -07:00
Vincent Koc
f14aa65bcc fix(plugins): refresh registry after chat toggles 2026-04-25 10:35:35 -07:00
Vincent Koc
29988335fc feat(plugins): resolve provider owners from registry 2026-04-25 10:35:35 -07:00
Vincent Koc
674d188153 feat(plugins): plan gateway startup from registry 2026-04-25 10:35:35 -07:00
Vincent Koc
feb8d3a4bd fix(plugins): label registry list state as enabled 2026-04-25 10:35:34 -07:00
Vincent Koc
5677a26385 docs(changelog): note registry-backed plugin list 2026-04-25 10:35:34 -07:00
Vincent Koc
5859dcd298 feat(plugins): list from registry snapshot 2026-04-25 10:35:34 -07:00
Vincent Koc
caf25fac91 feat(plugins): add registry repair command 2026-04-25 10:35:34 -07:00
Vincent Koc
521e75dea0 feat(plugins): prefer persisted registry reads 2026-04-25 10:35:09 -07:00
Vincent Koc
a7de722f4f fix(diagnostics-otel): align GenAI semconv attrs 2026-04-25 10:33:13 -07:00
Peter Steinberger
5f4bc6ec02 fix: surface external agent errors 2026-04-25 18:30:16 +01:00
Peter Steinberger
f545872cbc test(ui): streamline session controls async tests 2026-04-25 18:27:23 +01:00
Peter Steinberger
847c00d409 test(ui): speed up chat icon mocks 2026-04-25 18:27:23 +01:00
Peter Steinberger
88df8fe09d fix(browser): clarify Browserless CDP attach handling 2026-04-25 18:26:57 +01:00
Peter Steinberger
0bbb0eb735 fix(image): honor generation timeout config 2026-04-25 18:25:26 +01:00
Peter Steinberger
80739731dd docs: clarify pi-ai generic failover (#71647) 2026-04-25 18:22:06 +01:00
willamhou
4b5c2f9aa3 fix(agents/failover): classify bare pi-ai stream wrapper as timeout regardless of provider (#71620) 2026-04-25 18:22:06 +01:00
Vincent Koc
dcdf97685b fix(diagnostics): trust internal trace parents (#71574)
* fix(diagnostics): trust internal trace parents

* fix(diagnostics): harden trusted trace metadata

* fix(tooling): honor explicit oxlint threads

* fix(agents): use stable nonmutating sort helpers

* chore(plugin-sdk): refresh api baseline

* fix(diagnostics): gate internal event subscriptions

* fix(diagnostics): isolate listener event copies

* chore(plugin-sdk): refresh internal diagnostics baseline

* chore(plugin-sdk): refresh diagnostics event baseline

* fix(diagnostics): keep event state module local

* fix(diagnostics): harden internal subscription capability

* fix(diagnostics): freeze listener metadata
2026-04-25 10:18:52 -07:00
Peter Steinberger
8e7d382c37 refactor(tts): clarify text media directives 2026-04-25 18:18:34 +01:00
Peter Steinberger
67506ac2a9 fix(xai): support video reference images 2026-04-25 18:14:51 +01:00
Peter Steinberger
768bbc7cc0 docs: update OpenAI GPT-5.5 API guidance 2026-04-25 18:14:10 +01:00
Peter Steinberger
390be8138f fix: add OpenCode Go DeepSeek V4 models 2026-04-25 18:11:59 +01:00
Vincent Koc
0d274ef6c2 docs(control-ui): note assistant avatar uploads stay browser-local
Val Alexander's c65aa1d2a6 (#71639) changed assistant avatar uploads
from gateway config persistence to localStorage, mirroring the existing
user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md
'Personal identity (browser-local)' section only documented the user
identity. Add a paragraph noting the assistant avatar override follows
the same browser-local pattern, while keeping the ui.assistant.avatar
config field reachable for non-UI clients writing the field directly.
2026-04-25 10:08:59 -07:00
Peter Steinberger
6b3e4b88d6 test: update QA parity fixtures for GPT-5.5 2026-04-25 18:05:28 +01:00
Peter Steinberger
39343088ed fix(tts): keep media-only no-reply payloads 2026-04-25 18:04:54 +01:00
Peter Steinberger
f3ba962fd0 fix(subagents): explain browser tool profile filtering 2026-04-25 17:59:05 +01:00
Peter Steinberger
e27e29c66e refactor: split Crestodian planner backend selection 2026-04-25 17:56:46 +01:00
Peter Steinberger
60f9358348 fix(tts): preserve legacy tool voice hints 2026-04-25 17:56:37 +01:00
Peter Steinberger
dc7c703425 test: lazy-load global cleanup helpers 2026-04-25 17:49:16 +01:00
Peter Steinberger
8bead989da fix(telegram): frame audio transcripts as untrusted 2026-04-25 17:45:40 +01:00
Peter Steinberger
8659495384 test: make live cron probe agent-generic 2026-04-25 17:42:32 +01:00
Val Alexander
c65aa1d2a6 fix(control-ui): persist assistant avatar override locally (#71639)
* fix(control-ui): rebalance quick settings into stable 3-col bento

Pair Appearance with Automations and let Channels stand alone in the
middle column so all three top-row columns reach similar heights.
Promote Personal to a full-width row with a horizontal body
(identity tiles | emoji + actions) so the avatar block stops fighting
for half-width space. Drops the unused .qs-stack--wide hook.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair

Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities.

Changes:
- Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure)
- Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group
- Remove .qs-personal-body and .qs-personal-form wrapper divs
- Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing
- Responsive collapse to 1-column at ≤760px

Tests:
- config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form)
- config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure
- All 10 quick settings view tests passing
- All 20 schema regression tests passing

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* chore: ignore .vmux worktree paths

* fix(control-ui): persist assistant avatar override locally instead of via gateway config

Mirrors the user-avatar pattern: assistant avatar uploads now go to
localStorage and overlay the gateway-resolved identity at bootstrap and on
agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap
that rejected uploaded data URLs as 'Too big: expected string to have
<=200 characters', removes one config.patch RPC from the avatar path, and
collapses the upload handler from a 44-line async/loadConfig dance into a
plain synchronous setter.

Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to
2,000,000 to match the user-avatar size budget for non-UI clients writing
the field directly, and adds a content-aware text/image normalizer in
ui/src/ui/assistant-identity.ts so short-text avatars stay short while
data URLs survive round-tripping.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 11:17:48 -05:00
Darshan Paccha
95b7a85f06 fix(ui): remove duplicate config section headers
Fix duplicate section title and description rendering in single-section Control UI config pages.\n\nKeeps root multi-section card headers intact, keeps single-section hero copy as the only visible section title, and adds browser coverage for both single-section and root views.\n\nFixes #68003.\n\nThanks @d1rshan.
2026-04-25 09:43:50 -05:00
Vincent Koc
c070509b7f fix(security): bound archive and MIME parser work (#71561)
* fix(security): bound archive and MIME parser work

* fix(security): harden zip preflight accounting

* fix(plugins): keep update channel sync on bundled path helpers

* fix(lint): avoid boolean literal comparisons

* fix(lint): keep agent spawn assertion immutable

* test(auto-reply): relax slow model directive regression timeout
2026-04-25 06:22:56 -07:00
Peter Steinberger
4e3bf7ce6a test: scope gateway restart signal assertion 2026-04-25 14:09:31 +01:00
Peter Steinberger
5c6a5afe81 test: use non-mutating sort in cli runner spec 2026-04-25 14:07:24 +01:00
Peter Steinberger
cd392b947c test: dedupe memory and context suites 2026-04-25 14:06:26 +01:00
Peter Steinberger
2413c0f5a5 perf: split chat UI test dependencies 2026-04-25 14:06:26 +01:00
Peter Steinberger
3db60f7eab perf: trim agent workspace imports 2026-04-25 14:06:26 +01:00
Vincent Koc
9b1dd9e573 docs(browser): document Chrome MCP per-profile mcpCommand/mcpArgs and cdpUrl mapping
Vincent's commit ab1d1a5c9e (#71560) added user-facing config keys to
existing-session profiles for the Chrome DevTools MCP launch path:

- browser.profiles.<name>.mcpCommand
- browser.profiles.<name>.mcpArgs

Plus runtime behavior changes:

- cdpUrl http(s) -> --browserUrl, cdpUrl ws(s) -> --wsEndpoint
- endpoint flags and userDataDir are mutually exclusive

The CHANGELOG entry covered the change, but docs/tools/browser.md
existing-session reference did not. Add a 'Custom Chrome MCP launch'
subsection describing the new fields and the cdpUrl endpoint mapping
rules.
2026-04-25 05:54:54 -07:00
Chunyue Wang
bc73141e82 fix(cli): key gemini cli auth epoch on google account identity (#71076)
Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
2026-04-25 20:47:58 +08:00
Vincent Koc
ab1d1a5c9e fix(browser): configure Chrome MCP existing-session launch (#71560) 2026-04-25 05:46:39 -07:00
Peter Steinberger
dd78b7f773 fix: harden OpenCode ACP bind dispatch 2026-04-25 13:38:58 +01:00
Peter Steinberger
42514156e0 fix: yield while waiting for subagent completions 2026-04-25 13:29:47 +01:00
skylee-01
f7b71abf48 fix(agents): pass Claude system prompt via file 2026-04-25 17:59:25 +05:30
Vincent Koc
ed650b652f fix(test): detect partial sparse core roots 2026-04-25 05:18:25 -07:00
Peter Steinberger
b26367e22f test: add Crestodian QA lab setup scenario 2026-04-25 13:15:11 +01:00
Peter Steinberger
c977643460 perf(browser): precompute browser help 2026-04-25 13:07:15 +01:00
Sahil Satralkar
3064ea78ab fix(telegram): recover incomplete preview finalization (#71554)
Fix Telegram partial-stream preview finalization so ambiguous final edit failures fall back to a final send when the visible preview is a strict prefix of the answer.

Includes archived-preview regression coverage and generated config metadata refresh.

Thanks @sahilsatralkar.

Co-authored-by: Sahil Satralkar <62758655+sahilsatralkar@users.noreply.github.com>
2026-04-25 13:01:10 +01:00
Peter Steinberger
e25b3c6056 fix(browser): align bare ws cdp readiness 2026-04-25 13:00:22 +01:00
Vincent Koc
2b822f6ed0 fix(plugins): preserve default enablement for relocation 2026-04-25 04:59:53 -07:00
Vincent Koc
f70d77b0bd docs(plugins): clarify registry-derived relocations 2026-04-25 04:59:53 -07:00
Vincent Koc
0abb2a571f fix(plugins): derive bundled relocation from registry 2026-04-25 04:59:53 -07:00
Vincent Koc
7177492487 fix(plugins): keep enabled-only registry migration fresh 2026-04-25 04:59:53 -07:00
Vincent Koc
0cc2b0e283 feat(plugins): refresh registry after plugin mutations 2026-04-25 04:59:53 -07:00
Vincent Koc
53c3c949d0 feat(plugins): bridge externalized bundled updates 2026-04-25 04:59:52 -07:00
Vincent Koc
ad8296e685 fix(plugins): harden registry migration guards 2026-04-25 04:59:52 -07:00
Vincent Koc
f22a2f7e8b fix(plugins): migrate only enabled registry entries 2026-04-25 04:59:52 -07:00
Vincent Koc
d7cf803705 fix(plugins): preflight registry install migration 2026-04-25 04:59:52 -07:00
Vincent Koc
81aefb9a18 feat(plugins): migrate plugin registry on install 2026-04-25 04:59:52 -07:00
Peter Steinberger
a48998d8c8 test(qqbot): cover voice utility contracts 2026-04-25 12:57:23 +01:00
Peter Steinberger
c307700db0 test(whatsapp): cover group generated media delivery 2026-04-25 12:56:53 +01:00
Peter Steinberger
d6e9ae53fe perf: split chat strip helper 2026-04-25 12:52:27 +01:00
Peter Steinberger
56573185f2 perf: split canvas a2ui shared imports 2026-04-25 12:52:27 +01:00
Peter Steinberger
40e4a00c8e perf: slim crestodian rescue tests 2026-04-25 12:52:27 +01:00
Peter Steinberger
2b8105598e perf: lazy load support bundle zip 2026-04-25 12:52:27 +01:00
Peter Steinberger
1888242bd3 perf: split trajectory export paths 2026-04-25 12:52:27 +01:00
Peter Steinberger
4a76a66872 perf: slim memory host imports 2026-04-25 12:52:27 +01:00
Peter Steinberger
6eec38ad5a feat(discord): allow voice model override 2026-04-25 12:47:46 +01:00
Ayaan Zaidi
d0ed938351 fix: make subagent session errors actionable (#67790) (thanks @stainlu) 2026-04-25 17:15:36 +05:30
stainlu
835f768036 fix(agents): make sessions_spawn mode=session errors actionable when thread binding is unavailable 2026-04-25 17:15:36 +05:30
Peter Steinberger
3507efa4ec fix(media): preserve oversized video generation delivery 2026-04-25 12:41:43 +01:00
Roman Godz
150f3e472b fix: sync Claude CLI OAuth credentials (#70902) (thanks @starvex) 2026-04-25 17:07:27 +05:30
Peter Steinberger
84dc9f12f1 test(agents): cover single image generation media delivery 2026-04-25 12:32:43 +01:00
Vincent Koc
e174d96cc0 refactor(media): move sharp image ops into media runtime (#71519)
* refactor(media): move sharp image ops into plugin

* fix(media): pass image pixel budget to sharp plugin

* refactor(media): reuse media understanding sharp runtime

* test(build): allow staged runtime core graphs
2026-04-25 04:31:10 -07:00
Peter Steinberger
b2b898c2a8 feat(browser): configure local startup timeouts 2026-04-25 12:30:35 +01:00
Peter Steinberger
4ac6729d12 test: expand Crestodian first-run Docker smoke 2026-04-25 12:30:26 +01:00
Peter Steinberger
9ab51bb66e test: stabilize qa lab live scenarios 2026-04-25 12:30:08 +01:00
Peter Steinberger
c5fe80ad58 fix: make qa config apply retries idempotent 2026-04-25 12:30:07 +01:00
Peter Steinberger
67436918f3 fix: deliver subagent completions via external requester route 2026-04-25 12:30:07 +01:00
Vincent Koc
924271385b fix(cron): record interrupted startup runs
* fix(cron): record interrupted startup runs

* test(cron): update interrupted startup expectations
2026-04-25 04:28:11 -07:00
Val Alexander
fc5920fb51 fix(ui): polish assistant identity settings
Polishes the basic config identity layout, aligns assistant avatar rendering with chat, and adds a Control UI assistant avatar override with IDENTITY.md fallback.
2026-04-25 06:27:22 -05:00
Vincent Koc
443b837bd5 fix(build): harden bundled plugin runtime staging
Copy bundled plugin skill trees into dist-runtime, broaden Windows symlink-copy fallbacks, and harden runtime-deps fingerprinting.
2026-04-25 04:27:17 -07:00
Donetta Flatley
f408bba9de fix(memory-host-sdk): use TRUSTED_ENV_PROXY mode for remote embeddings in proxy environments (#71506)
* fix(memory-host-sdk): use TRUSTED_ENV_PROXY mode in withRemoteHttpResponse

When a HTTP/HTTPS proxy is configured via environment variables
(HTTPS_PROXY, HTTP_PROXY, ALL_PROXY), the withRemoteHttpResponse
function now passes mode=TRUSTED_ENV_PROXY to fetchWithSsrFGuard.

This causes DNS resolution to skip the local resolver and route
through the configured proxy, fixing 'fetch failed' errors for
remote memory embeddings (including GitHub Copilot embeddings) in
proxy environments (e.g. Clash TUN, corporate proxies).

Previously, without an explicit mode, fetchWithSsrFGuard defaulted
to STRICT mode which performs local DNS pre-resolution via
resolvePinnedHostnameWithPolicy(), failing in proxy environments
where DNS must go through the proxy.

Fixes: openclaw/openclaw#52162

* fix: harden memory env proxy guard (#71506) (thanks @DhtIsCoding)

---------

Co-authored-by: Dht <dht@openclaw.ai>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 12:24:09 +01:00
Peter Steinberger
f1470b52fb fix(agents): fall back for threadless completion delivery 2026-04-25 12:23:42 +01:00
Ayaan Zaidi
bdba4fa1bf fix: isolate active memory auth health (#71539)
* fix(agents): scope helper auth failures

* fix(active-memory): isolate recall auth health

* fix: isolate active memory auth health (#71539)

* fix: avoid auth policy import cycle (#71539)
2026-04-25 16:50:38 +05:30
Peter Steinberger
be1d716427 refactor(plugin-sdk): narrow CLI runtime exports 2026-04-25 12:20:34 +01:00
Vincent Koc
f8a41e5e9c fix(test): serialize changed checks locally 2026-04-25 04:19:09 -07:00
Peter Steinberger
b511250e5c feat(media): add voice conversion and speech plugins 2026-04-25 12:12:33 +01:00
Peter Steinberger
16b7dee1ef test(crestodian): complete tui overview mock 2026-04-25 12:07:52 +01:00
Peter Steinberger
de652afffd fix: use random restart intent temp suffix 2026-04-25 12:04:37 +01:00
Peter Steinberger
e6fd1ccfd7 perf(ui): trim chat test imports 2026-04-25 12:04:17 +01:00
Peter Steinberger
4484772e7d test(logger): isolate rolling file cleanup 2026-04-25 12:04:17 +01:00
Peter Steinberger
4d00c47072 perf(crestodian): reduce test import overhead 2026-04-25 12:04:17 +01:00
Vincent Koc
84a22a64be fix(feishu): finish streaming card closeout 2026-04-25 04:04:03 -07:00
Peter Steinberger
935cd34e9f fix(openai): omit Azure image deployment model body 2026-04-25 12:02:26 +01:00
Peter Steinberger
89755d1c79 refactor(browser): simplify lazy CLI placeholders 2026-04-25 11:48:59 +01:00
deepkilo
df6c58cf30 fix(gateway): use secure dashboard links when TLS is enabled (#71499)
Fixes #71494.

- Render Control UI links with https:// when gateway TLS is enabled.
- Render websocket links with wss:// through the shared link resolver.
- Add daemon status handoff coverage and TLS scheme docs.

Co-authored-by: deepkilord <wang_hgang@msn.com>
2026-04-25 11:45:15 +01:00
Peter Steinberger
8cbb62d93c docs(browser): document headless start override 2026-04-25 11:42:04 +01:00
Peter Steinberger
c52ec520c7 feat(browser): add one-shot headless start override 2026-04-25 11:42:03 +01:00
Peter Steinberger
51e6f9c27e fix(reply): narrow empty-body history guard 2026-04-25 11:41:36 +01:00
jindongfu
1559e28d6b fix(get-reply): include inboundUserContext in empty-body guard (#71489)
The empty-body guard only checked baseBodyFinal (current message body)
and softResetTail, ignoring inboundUserContext which includes
InboundHistory from group chat context. This caused the bot to reject
bare @mentions in Feishu group chats where prior messages provided the
conversation context via InboundHistory.

Now hasUserBody also checks whether inboundUserContext has content,
matching the behavior before the 2026.4.12 refactor.
2026-04-25 11:41:36 +01:00
Vincent Koc
1549ded4ac docs(control-ui): document PWA install and web push
Eduardo Cruz's PWA web push feat (21b7ad5805, #44590) added a substantial
user-facing surface — manifest.webmanifest, sw.js, gateway push.web.*
methods, persisted vapid-keys.json/web-push-subscriptions.json, and
OPENCLAW_VAPID_* env overrides — but did not touch any docs/.

Add a 'PWA install and web push' section to docs/web/control-ui.md
covering the new persisted state files, env vars, and the four scope-gated
gateway methods (push.web.vapidPublicKey, push.web.subscribe,
push.web.unsubscribe, push.web.test). Distinguish from the existing
APNS relay-backed iOS push path.
2026-04-25 03:40:38 -07:00
Peter Steinberger
776d2ab65d fix(browser): lazy-load browser CLI runtime
Co-authored-by: pandego <7780875+pandego@users.noreply.github.com>
Co-authored-by: Tianworld <3580442280@qq.com>
2026-04-25 11:40:20 +01:00
Ayaan Zaidi
27aae62d99 fix: stop heartbeat prompt leaking into user runs (#69278) (thanks @stainlu) 2026-04-25 16:09:56 +05:30
stainlu
06c058b21d fix(agents): stop injecting heartbeat system prompt on non-heartbeat runs (#69079) 2026-04-25 16:09:56 +05:30
Val Alexander
151befb90b chore: keep superpowers plans local (#71530)
* docs: add control ui setup guidance design

* chore: keep superpowers plans local
2026-04-25 05:35:23 -05:00
Vincent Koc
0c9dacf902 fix(test): ignore local check opt-out in dev wrappers 2026-04-25 03:32:01 -07:00
Peter Steinberger
87aa0f813c fix(cli): forward video generation options 2026-04-25 11:31:09 +01:00
Val Alexander
b85b106b10 docs: add application modernization plan (#71528)
* docs: add application modernization plan

* docs: clarify frontend skill target
2026-04-25 05:29:57 -05:00
Vincent Koc
e0546edd98 fix(cron): normalize flat legacy job rows 2026-04-25 03:29:30 -07:00
Ayaan Zaidi
bbd6dfbe92 fix: cover CLI session prompt hash reuse (#69236) (thanks @stainlu) 2026-04-25 15:58:19 +05:30
Peter Steinberger
7711df0669 fix: default proxy completions tool choice (#71472) (thanks @Speed-maker) 2026-04-25 11:23:33 +01:00
Speed-maker
9a6b769e6e fix(agents): default proxy completions tool choice 2026-04-25 11:23:33 +01:00
Peter Steinberger
6a71c19839 fix: simplify Crestodian startup greeting 2026-04-25 11:20:59 +01:00
Peter Steinberger
a0c70c4f5a fix(google): guard veo rest polling 2026-04-25 11:17:23 +01:00
Peter Steinberger
9b48e4c0b6 fix(browser): fall back to headless on Linux without display 2026-04-25 11:13:42 +01:00
Peter Steinberger
b5a1b7d44d fix(google): guard veo video downloads 2026-04-25 11:12:49 +01:00
Peter Steinberger
978f869fcd fix(google): type veo fallback operation state 2026-04-25 11:11:14 +01:00
Peter Steinberger
94686c63fb fix(google): fall back to rest for veo sdk 404 2026-04-25 11:11:14 +01:00
Vincent Koc
814409a3b3 fix(test): keep local Vitest checks serialized 2026-04-25 03:07:27 -07:00
Peter Steinberger
5e0cca5e24 fix(google): narrow veo api key for uri download 2026-04-25 11:07:16 +01:00
Peter Steinberger
c11337149b fix(google): download direct veo video uri 2026-04-25 11:07:16 +01:00
Vincent Koc
455eba7f94 fix(feishu): coalesce streaming card final delivery 2026-04-25 03:06:38 -07:00
Peter Steinberger
38703ed9a1 fix(discord): identify voice attachment metadata 2026-04-25 11:05:38 +01:00
Peter Steinberger
5985e1d8b9 test: speed up import-heavy tests 2026-04-25 11:04:16 +01:00
Peter Steinberger
b9ea631b4b fix(openai): use gpt 5.5 for codex image responses 2026-04-25 11:03:53 +01:00
Eduardo Cruz
21b7ad5805 feat: add Control UI PWA web push support (#44590)
Adds browser PWA manifest and service worker support for the Control UI, plus gateway RPC methods and persisted Web Push subscription handling.

Maintainer verification:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/infra/push-web.test.ts src/gateway/server-methods/push.test.ts src/gateway/control-ui.test.ts src/gateway/protocol/push.test.ts
- pnpm check:changed passed before final GitHub update-branch merge commit
- pnpm build

Source head: 0720024368
2026-04-25 05:03:00 -05:00
Peter Steinberger
385da2db60 feat: run Crestodian in TUI shell 2026-04-25 10:59:49 +01:00
Peter Steinberger
9fe35a0c62 fix(discord): restore voice note audio preflight 2026-04-25 10:57:37 +01:00
Peter Steinberger
936f27dcab docs: clarify minimax music changelog scope 2026-04-25 10:55:54 +01:00
Peter Steinberger
e6713c0a61 test(minimax): cover default music model normalization 2026-04-25 10:55:54 +01:00
Peter Steinberger
ed8384d32d fix(minimax): default music generation to music 2.6 2026-04-25 10:55:54 +01:00
Vincent Koc
c1f359c276 fix(test): reuse heavy-check lock in boundary prep 2026-04-25 02:49:45 -07:00
Vincent Koc
678d2c327c docs(changelog): backfill missing PR refs and reporter credits in top Unreleased
Three of my (vincentkoc) entries were missing closing PR refs, and
several maintainer-fix entries were missing credit for the user who
reported the underlying issue:

- Diagnostics/OTEL outbound delivery: add (#71471) and credit @jlapenna
  whose #70424 framed the broader tracing work.
- Cron malformed legacy jobs: add (#71509).
- OpenAI/Codex OAuth region failures: add (#71501) and credit reporter
  @wulala-xjj (#51175).
- Telegram duplicate pollers: credit reporter @Co-Messi (#56230).
- MCP/CLI one-shot retire: credit reporter @spartoviMD (#71457).
- OpenAI/Codex image baseUrl canonicalize: credit reporter @GodsBoy
  (#71460).
- Feishu TTS Ogg/Opus: credit reporters @sg1416-zg (#61249) and
  @ycjlb2023-peteryi (#37868).
- MiniMax TTS portal OAuth: credit reporter @zx15210404690-hash
  (#55017).
- MCP config reload disposal: credit reporter @xieyuanqing (#60656).
2026-04-25 02:49:37 -07:00
Peter Steinberger
815e9b493c fix: improve openrouter model scan fallback 2026-04-25 10:46:20 +01:00
Peter Steinberger
da2c61fe6e fix: render authenticated control ui avatars 2026-04-25 10:46:14 +01:00
Yunsu
9c64a0ca23 fix(google): avoid doubled media generation API version
Strip configured trailing /v1beta from Google music/video generation base URLs before calling the Google GenAI SDK.\n\nFixes #63240.\n\nThanks @Hybirdss.
2026-04-25 10:45:38 +01:00
Val Alexander
0bef73d151 chore: remove repo PR assets (#71510) 2026-04-25 04:40:29 -05:00
Vincent Koc
2896107153 fix(cron): tolerate malformed legacy jobs 2026-04-25 02:39:06 -07:00
Peter Steinberger
a7604f8170 fix(minimax): support token plan tts auth 2026-04-25 10:36:12 +01:00
Peter Steinberger
7fcefd56b7 chore: bump version to 2026.4.25 2026-04-25 10:31:52 +01:00
Vincent Koc
65ea6a0d94 fix(auth): clarify Codex OAuth region failures (#71501) 2026-04-25 02:31:42 -07:00
Peter Steinberger
c6770d3694 fix: align native think menus with session models 2026-04-25 10:30:49 +01:00
Peter Steinberger
4f91d81e1d fix(googlechat): preserve reply text after typing update failures
Preserve Google Chat reply text when typing indicator cleanup or update fails.

- Extract Google Chat reply delivery into a focused module
- Retry the failed first text chunk as a new message after placeholder update failure
- Cover media caption and chunk fallback regressions

Thanks @colin-lgtm.
2026-04-25 10:30:41 +01:00
Vincent Koc
0ee9e8188d Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  feat: add crestodian local planner fallback
  fix(control-ui): clarify chat context details
  fix(telegram): keep polling watchdog active for wedged runner
2026-04-25 02:22:02 -07:00
Peter Steinberger
9056d4f708 feat: add crestodian local planner fallback 2026-04-25 10:20:02 +01:00
Val Alexander
388270ffce fix(control-ui): clarify chat context details
Summary:
- Show full date and time in Control UI chat message footers.
- Collapse assistant model/token/context metadata behind an explicit Context disclosure.
- Update changelog attribution guidance to allow multi-author credited entries.

Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub CI passed on f071a38177
2026-04-25 04:19:56 -05:00
Vincent Koc
c52c161f5a refactor(plugins): compact package json index metadata 2026-04-25 02:18:56 -07:00
Vincent Koc
c959c18fc7 fix(plugins): persist registry enabled snapshot 2026-04-25 02:18:56 -07:00
Vincent Koc
00f47f01fe refactor(plugins): trim persisted plugin registry state 2026-04-25 02:18:56 -07:00
Vincent Koc
3556f8441a feat(plugins): add plugin registry facade 2026-04-25 02:18:56 -07:00
Vincent Koc
36219b0ffc fix(plugins): invalidate index on policy changes 2026-04-25 02:18:56 -07:00
Vincent Koc
b001b8c947 feat(plugins): inspect persisted plugin index state 2026-04-25 02:18:55 -07:00
Vincent Koc
74a384d887 feat(plugins): persist installed plugin index snapshots 2026-04-25 02:18:55 -07:00
Vincent Koc
dfac36ee01 feat(plugins): add cold installed index owner APIs 2026-04-25 02:18:55 -07:00
Vincent Koc
ceace83556 fix(telegram): keep polling watchdog active for wedged runner 2026-04-25 02:18:49 -07:00
Peter Steinberger
f6a3b42cfa fix(browser): keep transient fetch errors retryable
Co-authored-by: jriff <jriff@users.noreply.github.com>
2026-04-25 10:09:15 +01:00
Peter Steinberger
2483d1dc12 fix(browser): drop redundant setuid sandbox flag
Co-authored-by: Sebastian Krueger <150018+sebykrueger@users.noreply.github.com>
2026-04-25 10:09:15 +01:00
Peter Steinberger
41ed7fa535 fix(browser): manage isolated downloads
Co-authored-by: Pearce Kieser <5055971+Pearcekieser@users.noreply.github.com>
2026-04-25 10:09:13 +01:00
Peter Steinberger
b756dfcb2b perf: speed up boundary and provider tests 2026-04-25 10:08:46 +01:00
Vincent Koc
c5e6f4bbc0 docs(agents): document sparse changed gate 2026-04-25 02:07:15 -07:00
Peter Steinberger
2377f1a4cd test(elevenlabs): cover eleven_v3 tts catalog 2026-04-25 10:06:42 +01:00
itsuzef
0fc68a5ed4 feat(elevenlabs): register eleven_v3 in TTS model allowlist
eleven_v3 already works end-to-end (model_id passes through to the API
without validation), but was missing from ELEVENLABS_TTS_MODELS so it
never appeared in the in-product model picker or catalog metadata.
2026-04-25 10:06:42 +01:00
hcl
fd74fc5a4f fix(heartbeat): clamp scheduler delay to Node setTimeout cap (#71414) (#71478)
* fix(heartbeat): clamp scheduler delay to Node setTimeout cap (#71414)

When `agents.defaults.heartbeat.every` resolves to >2_147_483_647 ms
(~24.85d), the previous scheduleNext() called setTimeout with the raw
delay. Node clamps any delay > 2^31-1 to 1 ms, fires the callback, and
the heartbeat re-arms with the same oversized value - a tight loop that
floods the log with TimeoutOverflowWarning and crashes the gateway with
exit code 1.

Clamp the computed delay to HEARTBEAT_MAX_TIMEOUT_MS (2_147_483_647)
before calling setTimeout. The worst case is now one heartbeat every
~24.85d instead of crash-loop. Warn once per process when clamping
fires, so a misconfigured "365d" remains visible without flooding.

This is a defense-in-depth fix at the scheduler layer; loadConfig-level
rejection is a broader change with more blast radius and a separate
question (some users may legitimately want "every: 365d" to mean
"effectively never"). The clamped behaviour is closer to that intent
than the crash is.

Test: new scheduler test sets heartbeat.every="365d" with fake timers,
advances 60s, and asserts runSpy was never called (with the bug, it
would be called ~60_000 times).

* style: format heartbeat scheduler clamp

* fix: share safe timeout delay clamp (#71478) (thanks @hclsys)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 10:03:43 +01:00
Vincent Koc
a33f7b7d05 fix(test): make changed typechecks sparse-safe 2026-04-25 02:02:57 -07:00
Peter Steinberger
ed0210a187 test: streamline slow import-heavy suites 2026-04-25 10:02:02 +01:00
Peter Steinberger
f7d276b842 perf: cache guard inventory checks 2026-04-25 10:02:02 +01:00
Peter Steinberger
70b3ba2fed test: speed up Docker live scheduling 2026-04-25 10:01:50 +01:00
Val Alexander
6bdf87de87 fix(ui): remove quick config API keys card
Remove the misleading API Keys card from the quick settings page.

The card was hardcoded to a fixed env-var provider list and routed all actions to the broad Environment config section, which made the Add/Change affordances look more precise than they were. This removes the dead surface and keeps the quick settings grid focused on meaningful controls.

Verified:
- pnpm test ui/src/ui/views/config-quick.test.ts
- CI passed on PR #71496
2026-04-25 04:00:53 -05:00
Chinar Amrutkar
bf34fde235 fix(telegram): remove offset confirmation getUpdates call
Remove the startup persisted-offset getUpdates preflight so polling restarts do not self-conflict before the grammY runner starts.\n\nFixes #69304.\n\nThanks @chinar-amrutkar.
2026-04-25 09:53:50 +01:00
Peter Steinberger
19017bad96 docs(browser): explain actionable aria snapshot refs 2026-04-25 09:51:34 +01:00
Peter Steinberger
ec8dbc4595 feat(tts): add xiaomi mimo speech provider 2026-04-25 09:48:05 +01:00
Peter Steinberger
e10f20032a fix(browser): resolve aria snapshot refs via DOM markers
Co-authored-by: MrKipler <mrkipler@kiphausen.com>
2026-04-25 09:44:31 +01:00
Behnam Shahbazi
207f0341e0 docs: use target flag in README message example 2026-04-25 01:44:21 -07:00
Vincent Koc
01bf61fcfd fix(media): remove express from media host (#71436)
* fix(media): remove express from media host

* fix(media): harden media host responses

* fix(msteams): stage express runtime dependency

* fix(browser): align profile facade exports

* fix(msteams): keep setup entry narrow

* fix(types): satisfy extension setup gates

* fix(msteams): use generic setup config type
2026-04-25 01:39:42 -07:00
Peter Steinberger
3169886a21 fix(telegram): guard duplicate polling leases 2026-04-25 09:38:51 +01:00
Vincent Koc
c88c2328c2 docs: align Node minimum requirement 2026-04-25 01:36:01 -07:00
Vincent Koc
ec1f72b6c5 fix(gateway): preserve restart drain for active runs
Fixes https://github.com/openclaw/openclaw/issues/65485
2026-04-25 01:35:47 -07:00
Vincent Koc
734748d4f4 fix(test): cap native worker pools for serial Vitest 2026-04-25 01:31:30 -07:00
Peter Steinberger
bc21f500d4 fix: align Codex Responses instructions payload 2026-04-25 09:30:34 +01:00
Peter Steinberger
bf0221c5b3 fix(plugins): preserve bundled cli metadata skip 2026-04-25 09:29:16 +01:00
Peter Steinberger
87e92c71a4 docs(release): require changelog rewrite from commits 2026-04-25 09:29:16 +01:00
Peter Steinberger
689a353621 fix(plugins): load packaged runtime mirrors from canonical sources 2026-04-25 09:29:16 +01:00
Peter Steinberger
8503935a21 test: speed up changed unit checks 2026-04-25 09:27:59 +01:00
Peter Steinberger
9ad14f3639 fix: restore msteams channel plugin api type 2026-04-25 09:27:59 +01:00
Vincent Koc
bf0d2d70be fix(session): clean up rollover resources 2026-04-25 01:27:16 -07:00
Peter Steinberger
b0c55eb659 fix(feishu): transcode voice TTS audio 2026-04-25 09:26:42 +01:00
Vincent Koc
bd32b1a906 feat(diagnostics): add outbound delivery lifecycle events
Add bounded outbound message delivery lifecycle diagnostics and OTEL export without message body, recipient, room, media path, or raw channel result data.
2026-04-25 01:26:34 -07:00
Peter Steinberger
9e149519fe fix: keep control ui bundle browser-safe 2026-04-25 09:22:49 +01:00
Peter Steinberger
65b607245a fix(browser): ignore handled route navigation races
Co-authored-by: Richard Steadman <198648604+Steady-ai@users.noreply.github.com>
2026-04-25 09:22:31 +01:00
Val Alexander
af56926e2f Polish markdown preview chrome
Polish the Control UI markdown preview chrome and sidebar raw-text behavior.

- Add the upgraded preview dialog/sidebar chrome and tighten related CSS coverage.
- Show workspace-relative paths in the markdown preview dialog instead of absolute filesystem paths.
- Preserve raw markdown source for idempotent raw-text toggles.
- Align browser plugin-sdk facade export parity for DEFAULT_BROWSER_ACTION_TIMEOUT_MS.
- Stabilize the gateway update channel test by waiting for the async update runner call.

Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/views/agents.test.ts ui/src/ui/views/chat.test.ts src/plugins/contracts/plugin-sdk-subpaths.test.ts src/gateway/server.roles-allowlist-update.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub checks green on ebbe96fc88
2026-04-25 03:16:54 -05:00
Peter Steinberger
0e9156d205 test: stabilize Docker CLI backend lane 2026-04-25 09:12:48 +01:00
Peter Steinberger
5ac36c9719 fix(browser): detect more Linux Chromium installs (#48563)
Co-authored-by: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
2026-04-25 09:12:09 +01:00
Vincent Koc
0da58302cf docs(changelog): restore co-credits I dropped on Diagnostics/OTEL and tool-result pruning entries
Three entries were missing co-credits I should have preserved:

- Diagnostics/OTEL exec-process spans (#71451): @vincentkoc implemented,
  but @jlapenna's #70424 proposed the broader tracing work this entry
  builds on. Now credits both.
- Diagnostics/OTEL preloaded SDK (#71450): same pattern — credits
  @vincentkoc and @jlapenna.
- Agents/tool-result pruning (#51267): @cgdusek's PR explicitly built
  on prior work in #39331 by @alvinttang and #34980 by @coffeexcoin.
  Now credits all three.
2026-04-25 01:11:47 -07:00
Vincent Koc
56fbd72171 docs(changelog): correct PR refs and credits in top Unreleased section
- Two Diagnostics/OTEL Changes entries credited issue #70424 (jlapenna's
  open meta-tracing proposal) as the PR ref. The actual implementing
  PRs landed as #71451 (exec-process telemetry) and #71450 (preloaded
  SDK mode), both authored by @vincentkoc — corrected.
- Telegram/webhook fix had no Thanks credit. Issue #71392 reporter
  @joelforsberg46-source identified the delivery-retry behaviour, so
  credit them on the entry.
2026-04-25 01:07:48 -07:00
Peter Steinberger
24e9924d6a docs: credit blank reply recovery 2026-04-25 09:02:23 +01:00
Sanjay Santhanam
1f06dbd04c fix(agent): recover blank streamed replies from final answer 2026-04-25 09:02:23 +01:00
Quratulain-bilal
bc2d53dacd test(browser): cover tilde edge cases for executablePath (#71439)
* test(browser): cover tilde edge cases for executablePath

Adds coverage for cases the original tilde-expansion fix in 95a2c9b
intentionally supports but does not assert:

- bare "~" expands to the home directory
- Windows-style "~\AppData\..." expands to $HOME on Windows
- a stray "~" mid-path (e.g. /opt/~chromium/chrome) is preserved verbatim,
  guarding the regex anchor against future regressions

No production code changes; tests only.

* test(browser): skip Windows-style ~\ tilde test on POSIX

path.resolve treats backslashes as literal characters on POSIX, so
"~\AppData\..." cannot resolve to "$HOME/AppData/..." on Linux/macOS.
Gate that case to win32 to keep the assertion meaningful.
2026-04-25 09:01:57 +01:00
Peter Steinberger
a4fc6c2409 test: speed up slow unit checks 2026-04-25 08:59:53 +01:00
Peter Steinberger
2011de69d3 feat: add Crestodian setup helper 2026-04-25 08:58:21 +01:00
Peter Steinberger
e0bee76fb0 fix: retire one-shot agent MCP runtimes 2026-04-25 08:58:02 +01:00
a410979729-sys
8fd15ed0e5 fix(github-copilot): preserve encrypted reasoning ids with encrypted_content (#71448)
Preserve encrypted Copilot Responses reasoning item IDs during replay and harden the live Copilot replay probe.

Thanks @a410979729-sys.
2026-04-25 08:57:47 +01:00
github-actions[bot]
10ed007fb4 chore(ui): refresh th control ui locale 2026-04-25 07:57:39 +00:00
github-actions[bot]
4714a134d2 chore(ui): refresh pl control ui locale 2026-04-25 07:57:31 +00:00
github-actions[bot]
fb3efcf659 chore(ui): refresh id control ui locale 2026-04-25 07:57:27 +00:00
github-actions[bot]
6b4d8924eb chore(ui): refresh uk control ui locale 2026-04-25 07:57:12 +00:00
github-actions[bot]
4ca173a41c chore(ui): refresh tr control ui locale 2026-04-25 07:57:04 +00:00
github-actions[bot]
3019163e2e chore(ui): refresh ja-JP control ui locale 2026-04-25 07:56:53 +00:00
github-actions[bot]
e699b184af chore(ui): refresh fr control ui locale 2026-04-25 07:56:49 +00:00
github-actions[bot]
390f0487e8 chore(ui): refresh ko control ui locale 2026-04-25 07:56:45 +00:00
github-actions[bot]
2536fec538 chore(ui): refresh es control ui locale 2026-04-25 07:56:35 +00:00
github-actions[bot]
02ea62917e chore(ui): refresh zh-CN control ui locale 2026-04-25 07:56:10 +00:00
github-actions[bot]
812bc2a441 chore(ui): refresh de control ui locale 2026-04-25 07:56:06 +00:00
github-actions[bot]
7b58ffde85 chore(ui): refresh pt-BR control ui locale 2026-04-25 07:56:05 +00:00
github-actions[bot]
9dc608f54b chore(ui): refresh zh-TW control ui locale 2026-04-25 07:56:01 +00:00
Vincent Koc
ebb08dc70e fix(ui): use current context usage in Control UI 2026-04-25 00:54:33 -07:00
Vincent Koc
73d72204a0 fix(tooling): harden changed checks for sparse worktrees 2026-04-25 00:51:34 -07:00
Peter Steinberger
1ca029e888 fix: clean up infer MCP runtimes 2026-04-25 08:49:27 +01:00
Peter Steinberger
2b2a300b35 fix: align browser profile facade exports 2026-04-25 08:46:13 +01:00
Peter Steinberger
0f4b6f81d9 fix: canonicalize Codex image base URLs 2026-04-25 08:45:41 +01:00
Peter Steinberger
5163a2fbf7 docs: document Talk MLX config 2026-04-25 08:42:27 +01:00
Peter Steinberger
eafb25afc1 docs(google-meet): document dry run exports 2026-04-25 08:42:06 +01:00
Peter Steinberger
d78cef1d71 feat(google-meet): add export dry run manifests 2026-04-25 08:42:06 +01:00
Peter Steinberger
4a80e61680 fix: reap MCP runtimes on config reload 2026-04-25 08:40:45 +01:00
Peter Steinberger
7251551960 docs(google-meet): document export manifests 2026-04-25 08:38:43 +01:00
Peter Steinberger
388e0eb605 feat(google-meet): add export manifests and tool parity 2026-04-25 08:38:43 +01:00
Vincent Koc
13f4657b88 test(plugins): cover install ledger reload indexing 2026-04-25 00:37:06 -07:00
Vincent Koc
8fd3f4cef2 test(plugins): lock installed index source contract 2026-04-25 00:37:06 -07:00
Vincent Koc
28eb56dd21 fix(plugins): index install ledger source facts 2026-04-25 00:37:05 -07:00
Vincent Koc
15d27d1527 feat(channels): read setup discovery from installed index 2026-04-25 00:37:05 -07:00
Vincent Koc
0b2bc8c5f6 feat(models): read provider owners from installed index 2026-04-25 00:37:05 -07:00
Vincent Koc
ea3e390346 feat(plugins): split cold provider contributions 2026-04-25 00:37:05 -07:00
Vincent Koc
fb4eec54a7 feat(plugins): add installed plugin index 2026-04-25 00:37:04 -07:00
Peter Steinberger
7a71a66571 perf: cache provider env var lookups 2026-04-25 08:35:57 +01:00
Peter Steinberger
e9b27ed2a6 perf: speed up auth choice tests 2026-04-25 08:31:40 +01:00
Peter Steinberger
5fe333ada8 docs(google-meet): update export workflow notes 2026-04-25 08:28:34 +01:00
Peter Steinberger
03484b74ab feat(google-meet): polish exports and calendar previews 2026-04-25 08:28:34 +01:00
Peter Steinberger
e0beea97aa perf: speed up focused tests 2026-04-25 08:26:28 +01:00
Peter Steinberger
7132ca5766 feat(browser): include safe tab urls in agent responses 2026-04-25 08:24:46 +01:00
Peter Steinberger
e8191e5b8f fix: ack Telegram webhooks before update handling 2026-04-25 08:23:03 +01:00
Peter Steinberger
a44800e929 fix(google-meet): preserve lazy cli import 2026-04-25 08:17:55 +01:00
Peter Steinberger
e1cf94f49a docs(google-meet): document export and calendar lookup 2026-04-25 08:17:54 +01:00
Peter Steinberger
d3595d7c3f feat(google-meet): add calendar export attendance workflows 2026-04-25 08:17:54 +01:00
2952 changed files with 159959 additions and 42110 deletions

View File

@@ -7,6 +7,22 @@ description: Review, triage, close, label, comment on, or land OpenClaw PRs/issu
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Start issue and PR triage with ghcrawl
- Anytime you inspect OpenClaw issues or PRs, check local `ghcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
- Use `ghcrawl` for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
- If `ghcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `ghcrawl refresh`, `ghcrawl embed`, or `ghcrawl cluster` unless the user asked to update the local store or the stale data is blocking the decision.
Common read-only path:
```bash
ghcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
ghcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
ghcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --json
ghcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
## Apply close and triage labels correctly
- If an issue or PR matches an auto-close reason, apply the label and let `.github/workflows/auto-response.yml` handle the comment/close/lock flow.
@@ -35,6 +51,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`.
@@ -44,9 +75,9 @@ Use this skill for maintainer-facing GitHub workflow, not for ordinary code chan
## Search broadly before deciding
- Prefer targeted keyword search before proposing new work or closing something as duplicate.
- Use `--repo openclaw/openclaw` with `--match title,body` first.
- Add `--match comments` when triaging follow-up discussion.
- Prefer `ghcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Use `--repo openclaw/openclaw` with `--match title,body` first when using `gh search`.
- Add `--match comments` when triaging follow-up discussion or closed-as-duplicate chains.
- Do not stop at the first 500 results when the task requires a full search.
Examples:
@@ -68,6 +99,7 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
- Keep commit messages concise and action-oriented.
- Group related changes; avoid bundling unrelated refactors.
- Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues.
- Do not commit PR-only artifacts such as screenshots under `.github/pr-assets`; attach them to the PR/comment or use an external artifact store instead.
## Extra safety

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

@@ -25,15 +25,36 @@ Use this skill for release and publish-time workflow. Keep ordinary development
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
changelog rewrite immediately before creating the release branch.
- During release planning, inspect both `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
before final publish. For every deprecated or removal-pending compatibility
record whose `removeAfter` date is on or before the release date, either
remove the compatibility path where safe and validate the affected tests, or
write down why removal is blocked and get explicit maintainer approval before
shipping the expired compatibility path.
- When removing deprecated runtime/config compatibility, preserve any doctor
migration, repair, or hint that is still needed by supported upgrade paths.
Doctor-side compatibility should stay tracked in
`src/commands/doctor/shared/deprecation-compat.ts` until maintainers confirm
the repair is no longer needed.
- Revalidate compatibility replacement text during release planning. The
recommended replacement can shift as plugin ownership, externalization, and
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after they leave the machine. If a
published or pushed beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, run the full pre-npm test roster before publishing
each beta. After a beta is published, run the smaller published-install roster
focused on install/update/Docker/Parallels. If anything fails, fix it on the
release branch, commit/push/pull, increment beta number, and repeat. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
the release branch, commit/push/pull, increment beta number, and repeat. Run
the full expensive roster at least once before stable/latest promotion; for
later beta attempts, rerun only lanes whose evidence changed unless the fix
touches broad release, install/update, plugin, Docker, Parallels, or live QA
behavior. After each beta is published, scan current `main` once for critical
fixes that landed after the release branch cut and backport only important
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
after 4 failed beta attempts, stop and report.
- Use `/changelog` before version/tag preparation so the top changelog section
is deduped and ordered by user impact.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
@@ -75,6 +96,11 @@ Use this skill for release and publish-time workflow. Keep ordinary development
parallel, publish npm from the successful npm preflight, then start published
npm install/update, Docker, and Parallels verification while mac artifacts
continue.
- After a beta is published, overlap remote/manual release rosters where useful,
but avoid piling local Docker, Parallels, and QA-Lab work onto the same host
when it would create system-load noise. Use selective reruns after failures or
fixes, but keep proof that Docker, Parallels, and QA-Lab each passed at least
once before stable/latest promotion.
- Mac packaging may be built from a slight release-branch variation of the
tagged commit when the delta is mac packaging, signing, workflow, or
validation-only release machinery. If mac packaging needs release-branch-only
@@ -97,11 +123,23 @@ Use this skill for release and publish-time workflow. Keep ordinary development
## Build changelog-backed release notes
- Before release branching or tagging, rewrite the target `CHANGELOG.md`
section from commit history, not just from existing notes: scan commits since
the last reachable release tag, add missed user-facing changes, dedupe
overlapping entries, and sort each section from most to least interesting for
users.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies must use the full matching
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.D` through the line before the
next level-2 heading and use that complete block as the release notes.
- When preparing release notes, scan `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` for compatibility records
with `warningStarts` or `removeAfter` within 7 days after the release date.
Add an `Upcoming deprecations` note to the release notes when any exist,
including the compatibility code, target date, replacement, and a link to the
record's `docsPath` or `/plugins/compatibility` when no more specific
deprecation page exists.
- When cutting a mac release with a beta GitHub prerelease:
- tag `vYYYY.M.D-beta.N` from the release commit
- create a prerelease titled `openclaw YYYY.M.D-beta.N`
@@ -197,10 +235,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
@@ -281,9 +325,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Docker install/update coverage that exercises the published beta package
- published npm Telegram proof: dispatch Actions > `NPM Telegram Beta E2E`
from `main` with `package_spec=openclaw@<beta-version>` and
`provider_mode=mock-openai`, approve `npm-release`, and require success.
This is the default button path for installed-package onboarding,
Telegram setup, and real Telegram E2E against the published npm package.
`provider_mode=mock-openai`, and require success. This workflow is
maintainer-dispatched and intentionally has no `npm-release` approval gate;
`qa-live-shared` only supplies the shared QA secrets. This is the default
button path for installed-package onboarding, Telegram setup, and real
Telegram E2E against the published npm package.
Use the local `pnpm test:docker:npm-telegram-live` lane with the matching
`OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC` and Convex CI env only as a fallback
or debugging path.
@@ -480,8 +526,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
6. Create `release/YYYY.M.D` from that post-changelog `main` commit.
7. Make every repo version location match the beta tag before creating it.
8. Commit release preparation changes on the release branch and push the branch.
9. Run the local build, Docker, and Parallels parts of the full pre-npm beta
test roster from the release branch before any npm preflight or publish.
9. Run the fast local beta preflight from the release branch before any npm
preflight or publish. Keep expensive Docker, Parallels, and published-package
install/update lanes for after the beta is live unless the operator asks to
run them before beta publication.
10. For beta releases, skip mac app build/sign/notarize unless beta scope or a
release blocker specifically requires it. For stable releases, include the
mac app, signing, notarization, and appcast path.
@@ -518,10 +566,16 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
21. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
22. Run postpublish verification:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
23. Run the post-published beta verification roster. If any lane fails after
the beta tag/package is pushed or published, fix, commit/push/pull,
increment to the next beta tag, and restart at the full pre-npm beta test
roster for the new beta. The roster includes the manual Actions >
23. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to
the next beta if the fix must change the already-published package. If any
lane fails after the beta tag/package is pushed or published, fix,
commit/push/pull, increment to the next beta tag, and rerun the affected
beta evidence. Once the beta is live, start remote/manual rosters where they
can overlap safely, but keep local Docker and Parallels load controlled.
Ensure the full expensive roster has passed at least once before
stable/latest promotion. The roster includes the manual Actions >
`NPM Telegram Beta E2E` workflow against the exact published beta package.
If a pre-npm lane fails before any tag/package leaves the machine, fix and
rerun the same intended beta attempt. Repeat up to the operator's

View File

@@ -0,0 +1,244 @@
---
name: openclaw-testing
description: Choose, run, rerun, or debug OpenClaw tests, CI checks, Docker E2E lanes, release validation, and the cheapest safe verification path.
---
# OpenClaw Testing
Use this skill when deciding what to test, debugging failures, rerunning CI,
or validating a change without wasting hours.
## Read First
- `docs/reference/test.md` for local test commands.
- `docs/ci.md` for CI scope, release checks, Docker chunks, and runner behavior.
- Scoped `AGENTS.md` files before editing code under a subtree.
## Default Rule
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- source: `pnpm changed:lanes --json`, then `pnpm check:changed`
- tests only: `pnpm test:changed`
- one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
2. Reproduce narrowly before fixing.
3. Fix root cause.
4. Rerun the same narrow proof.
5. Broaden only when the touched contract demands it.
## Guardrails
- Do not kill unrelated processes or tests. If something is running elsewhere, treat it as owned by the user or another agent.
- Do not run expensive local Docker, full release checks, full `pnpm test`, or full `pnpm check` unless the user asks or the change genuinely requires it.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
## Local Test Shortcuts
```bash
pnpm changed:lanes --json
pnpm check:changed # changed typecheck/lint/guards; no Vitest
pnpm test:changed # cheap smart changed Vitest targets
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed
pnpm test <path-or-filter> -- --reporter=verbose
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
```
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
`pnpm test` wrapper so project routing, workers, and setup stay correct.
## Command Semantics
- `pnpm check` and `pnpm check:changed` do not run Vitest tests. They are for
typecheck, lint, and guard proof.
- `pnpm test` and `pnpm test:changed` run Vitest tests.
- `pnpm test:changed` is intentionally cheap by default: direct test edits,
sibling tests, explicit source mappings, and import-graph dependents.
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` is the explicit broad
fallback for harness/config/package edits that genuinely need it.
- Do not run extension sweeps just because core changed. If a core edit is for a
specific plugin bug, run that plugin's tests explicitly. If a public SDK or
contract change needs consumer proof, choose the smallest representative
plugin/contract tests first, then broaden only when the risk justifies it.
- The test wrapper prints a short `[test] passed|failed|skipped ... in ...`
line. Vitest's own duration is still the per-shard detail.
## Routing Model
- `pnpm changed:lanes --json` answers "which check lanes does this diff touch?"
It is used by `pnpm check:changed` for typecheck/lint/guard selection.
- `pnpm test:changed` answers "which Vitest targets are worth running now?" It
uses the same changed path list, but applies a cheaper test-target resolver.
- Direct test edits run themselves. Source edits prefer explicit mappings,
sibling `*.test.ts`, then import-graph dependents. Shared harness/config/root
edits are skipped by default unless they have precise mapped tests.
- Public SDK or contract edits do not automatically run every plugin test.
`check:changed` proves extension type contracts; the agent chooses the
smallest plugin/contract Vitest proof that matches the actual risk.
- Use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when a harness,
config, package, or unknown-root edit really needs the broad Vitest fallback.
## CI Debugging
Start with current run state, not logs for everything:
```bash
gh run list --branch main --limit 10
gh run view <run-id> --json status,conclusion,headSha,url,jobs
gh run view <run-id> --job <job-id> --log
```
- Check exact SHA. Ignore newer unrelated `main` unless asked.
- For cancelled same-branch runs, confirm whether a newer run superseded it.
- Fetch full logs only for failed or relevant jobs.
## Docker
Docker is expensive. First inspect the scheduler without running Docker:
```bash
OPENCLAW_DOCKER_ALL_DRY_RUN=1 pnpm test:docker:all
OPENCLAW_DOCKER_ALL_DRY_RUN=1 OPENCLAW_DOCKER_ALL_LANES=install-e2e pnpm test:docker:all
OPENCLAW_DOCKER_ALL_LANES=install-e2e node scripts/test-docker-all.mjs --plan-json
```
Run one failed lane locally only when explicitly asked or when GitHub is not
usable:
```bash
OPENCLAW_DOCKER_ALL_LANES=<lane> \
OPENCLAW_DOCKER_ALL_BUILD=0 \
OPENCLAW_DOCKER_ALL_PREFLIGHT=0 \
OPENCLAW_SKIP_DOCKER_BUILD=1 \
OPENCLAW_DOCKER_E2E_BARE_IMAGE='<prepared-bare-image>' \
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE='<prepared-functional-image>' \
pnpm test:docker:all
```
For release validation, prefer the reusable GitHub workflow input:
```yaml
docker_lanes: install-e2e
```
Multiple lanes are allowed:
```yaml
docker_lanes: install-e2e bundled-channel-update-acpx
```
That skips the three chunk matrix and runs one targeted Docker job against the
prepared GHCR images and a fresh OpenClaw npm tarball for the selected ref.
Reruns usually need that new tarball because the fix being tested changed the
package contents even if the SHA-tagged GHCR Docker image can be reused.
Live-only targeted reruns skip the E2E images and build only the live-test
image. Release-path normal mode remains max three Docker chunk jobs:
- `core`
- `package-update`
- `plugins-integrations`
Docker E2E images never copy repo sources as the app under test: the bare image
is a Node/Git runner, and the functional image installs the same prebuilt npm
tarball that bare lanes mount. `scripts/package-openclaw-for-docker.mjs` is the
single packer for local scripts and CI and validates the tarball inventory
before Docker consumes it. `scripts/test-docker-all.mjs --plan-json` is the
scheduler-owned CI plan for image kind, package, live image, lane, and
credential needs. Docker lane definitions live in the single scenario catalog
`scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in
`scripts/lib/docker-e2e-plan.mjs`. `scripts/docker-e2e.mjs` converts plan and
summary JSON into GitHub outputs and step summaries. Every scheduler run writes
`.artifacts/docker-tests/**/summary.json` plus `failures.json`. Read those
before rerunning. Lane entries include `command`, `rerunCommand`, status,
timing, timeout state, image kind, and log file path. The summary also includes
top-level phase timings for preflight, image build, package prep, lane pools,
and cleanup. Use `pnpm test:docker:timings <summary.json>` to rank slow lanes
and phases before deciding whether a broader rerun is justified.
## Cheap Docker Reruns
First derive the smallest rerun command from artifacts:
```bash
pnpm test:docker:rerun <github-run-id>
pnpm test:docker:rerun .artifacts/docker-tests/<run>/failures.json
```
The script downloads Docker E2E artifacts for a GitHub run, reads
`summary.json`/`failures.json`, and prints a combined targeted workflow command
plus per-lane commands. Prefer the combined targeted command when several lanes
failed for the same patch:
```bash
gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
-f ref=<sha> \
-f include_repo_e2e=false \
-f include_release_path_suites=false \
-f include_openwebui=false \
-f docker_lanes='install-e2e bundled-channel-update-acpx' \
-f include_live_suites=false \
-f live_models_only=false
```
That path still runs the prepare job, so it creates a new tarball for `<sha>`.
If the SHA-tagged GHCR bare/functional image already exists, CI skips rebuilding
that image and only uploads the fresh package artifact before the targeted lane
job. Do not rerun the full three-chunk release path unless the failed lane list
or touched surface really requires it.
## Docker Expected Timings
Treat these as ballpark. Blacksmith queue time, GHCR pull speed, provider
latency, npm cache state, and Docker daemon health can dominate.
Current local timing artifact (`.artifacts/docker-tests/lane-timings.json`) has
these rough bands:
- Tiny lanes, seconds to under 1 minute:
`agents-delete-shared-workspace` ~3s, `plugin-update` ~7s,
`config-reload` ~14s, `pi-bundle-mcp-tools` ~15s, `onboard` ~18s,
`session-runtime-context` ~20s, `gateway-network` ~34s, `qr` ~44s.
- Medium deterministic lanes, ~1-5 minutes:
`npm-onboard-channel-agent` ~96s, `openai-image-auth` ~99s,
bundled channel/update lanes usually ~90-300s, `openwebui` ~225s,
`mcp-channels` ~274s.
- Heavy deterministic lanes, ~6-10 minutes:
`bundled-channel-root-owned` ~429s,
`bundled-channel-setup-entry` ~420s,
`bundled-channel-load-failure` ~383s,
`cron-mcp-cleanup` ~567s.
- Live provider lanes, often ~15-20 minutes:
`live-gateway` ~958s, `live-models` ~1054s.
- Installer/release lanes:
`install-e2e` and package-update paths can vary widely with npm, provider,
and package registry behavior. Budget tens of minutes; prefer GitHub targeted
reruns over local repeats.
Default fallback lane timeout is 120 minutes. A timeout usually means debug the
lane log/artifacts first, not “run the whole thing again.”
## Failure Workflow
1. Identify exact failing job, SHA, lane, and artifact path.
2. Read `failures.json`, `summary.json`, and the failed lane log tail.
3. Use `pnpm test:docker:rerun <run-id|failures.json>` to generate targeted
GitHub rerun commands.
4. If the lane has `rerunCommand`, use that only as a local starting point.
5. For Docker release failures, dispatch targeted `docker_lanes=<failed-lane>`
on GitHub before considering local Docker.
6. Patch narrowly, then rerun the failed file/lane only.
7. Broaden to `pnpm check:changed` or CI only after the isolated proof passes.
## When To Escalate
- Public SDK/plugin contract changes: run changed gate plus relevant extension
validation.
- Build output, lazy imports, package boundaries, or published surfaces:
include `pnpm build`.
- Workflow edits: run `pnpm check:workflows`.
- Release branch or tag validation: use release docs and GitHub workflows; avoid
local Docker unless Peter explicitly asks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "OpenClaw Testing"
short_description: "Choose cheap, targeted OpenClaw validation"
default_prompt: "Use $openclaw-testing to choose the cheapest safe test or CI verification path, inspect failures, and rerun only the relevant OpenClaw lane."

View File

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

View File

@@ -0,0 +1,145 @@
name: Docker E2E plan and hydrate
description: >
Create a Docker E2E lane plan, expose GitHub outputs, and optionally hydrate
the prebuilt package artifact plus shared Docker images needed by the plan.
inputs:
mode:
description: prepare, chunk, or targeted.
required: true
chunk:
description: Release-path chunk for mode=chunk.
required: false
default: ""
lanes:
description: Comma/space separated lane names for targeted or prepare mode.
required: false
default: ""
include-openwebui:
description: Whether Open WebUI is included when planning release/prepare coverage.
required: false
default: "true"
include-release-path-suites:
description: Whether prepare mode should plan all release-path suites.
required: false
default: "false"
hydrate-artifacts:
description: Whether to download/pull artifacts required by the plan.
required: false
default: "true"
outputs:
credentials:
description: Comma-separated credential groups required by selected lanes.
value: ${{ steps.plan.outputs.credentials }}
needs_bare_image:
description: "1 when selected lanes require the bare Docker E2E image."
value: ${{ steps.plan.outputs.needs_bare_image }}
needs_e2e_image:
description: "1 when selected lanes require any Docker E2E image."
value: ${{ steps.plan.outputs.needs_e2e_image }}
needs_functional_image:
description: "1 when selected lanes require the functional Docker E2E image."
value: ${{ steps.plan.outputs.needs_functional_image }}
needs_live_image:
description: "1 when selected lanes require building the live Docker image."
value: ${{ steps.plan.outputs.needs_live_image }}
needs_package:
description: "1 when selected lanes require the OpenClaw package tarball."
value: ${{ steps.plan.outputs.needs_package }}
plan_json:
description: Path to the generated plan JSON.
value: ${{ steps.plan.outputs.plan_json }}
runs:
using: composite
steps:
- name: Plan Docker E2E lanes
id: plan
shell: bash
env:
MODE: ${{ inputs.mode }}
CHUNK: ${{ inputs.chunk }}
LANES: ${{ inputs.lanes }}
INCLUDE_OPENWEBUI: ${{ inputs.include-openwebui }}
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include-release-path-suites }}
run: |
set -euo pipefail
mkdir -p .artifacts/docker-tests
case "$MODE" in
prepare)
plan_path=".artifacts/docker-tests/plan.json"
if [[ "$INCLUDE_RELEASE_PATH_SUITES" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_PLAN_RELEASE_ALL=1
elif [[ -n "$LANES" ]]; then
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
elif [[ "$INCLUDE_OPENWEBUI" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_LANES=openwebui
fi
;;
chunk)
if [[ -z "$CHUNK" ]]; then
echo "chunk input is required for Docker E2E chunk planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_CHUNK="$CHUNK"
plan_path=".artifacts/docker-tests/release-${CHUNK}-plan.json"
;;
targeted)
if [[ -z "$LANES" ]]; then
echo "lanes input is required for Docker E2E targeted planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
plan_path=".artifacts/docker-tests/targeted-plan.json"
;;
*)
echo "mode must be prepare, chunk, or targeted. Got: $MODE" >&2
exit 1
;;
esac
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="$INCLUDE_OPENWEBUI"
node scripts/test-docker-all.mjs --plan-json > "$plan_path"
node scripts/docker-e2e.mjs github-outputs "$plan_path" >> "$GITHUB_OUTPUT"
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"
- name: Download OpenClaw Docker E2E package
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
with:
name: docker-e2e-package
path: .artifacts/docker-e2e-package
- name: Pull shared bare Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_bare_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
- name: Pull shared functional Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_functional_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
- name: Validate Docker E2E credentials
if: inputs.hydrate-artifacts == 'true'
shell: bash
env:
CREDENTIALS: ${{ steps.plan.outputs.credentials }}
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi

30
.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:
@@ -227,6 +233,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-otel/**"
"extensions: diagnostics-prometheus":
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-prometheus/**"
"extensions: llm-task":
- changed-files:
- any-glob-to-any-file:
@@ -307,6 +317,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:
@@ -315,6 +330,11 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/lmstudio/**"
"extensions: litellm":
- changed-files:
- any-glob-to-any-file:
- "extensions/litellm/**"
- "docs/providers/litellm.md"
"extensions: openai":
- changed-files:
- any-glob-to-any-file:
@@ -351,6 +371,11 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/qianfan/**"
"extensions: senseaudio":
- changed-files:
- any-glob-to-any-file:
- "extensions/senseaudio/**"
- "docs/providers/senseaudio.md"
"extensions: synthetic":
- changed-files:
- any-glob-to-any-file:
@@ -367,6 +392,11 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/together/**"
"extensions: tts-local-cli":
- changed-files:
- any-glob-to-any-file:
- "extensions/tts-local-cli/**"
- "docs/tools/tts.md"
"extensions: venice":
- changed-files:
- any-glob-to-any-file:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

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

@@ -1,6 +1,7 @@
name: CI
on:
workflow_dispatch:
push:
branches: [main]
paths-ignore:
@@ -13,8 +14,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha)) }}
cancel-in-progress: true
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -75,6 +76,7 @@ jobs:
submodules: false
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@@ -82,11 +84,12 @@ jobs:
- name: Detect docs-only changes
id: docs_scope
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/detect-docs-changes
- name: Detect changed scopes
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
@@ -101,7 +104,7 @@ jobs:
- name: Detect changed extensions
id: changed_extensions
if: steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
@@ -125,19 +128,19 @@ jobs:
- name: Build CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_DOCS_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ github.event_name == 'workflow_dispatch' && '{"include":[]}' || steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
run: |
node --input-type=module <<'EOF'
@@ -1231,6 +1234,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

@@ -63,7 +63,7 @@ jobs:
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
# Build amd64 images (default + slim share the build stage cache)
# Build amd64 image. Default and slim tags point to the same slim runtime.
build-amd64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -74,7 +74,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -117,12 +116,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -159,28 +153,15 @@ jobs:
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
# Build arm64 images (default + slim share the build stage cache)
# Build arm64 image. Default and slim tags point to the same slim runtime.
build-arm64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -191,7 +172,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -234,12 +214,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -276,25 +251,12 @@ jobs:
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
# Create multi-platform manifests
@@ -351,16 +313,11 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create and push default manifest
- name: Create and push manifest
shell: bash
env:
TAGS: ${{ steps.tags.outputs.value }}
@@ -378,20 +335,94 @@ jobs:
"${AMD64_DIGEST}" \
"${ARM64_DIGEST}"
- name: Create and push slim manifest
verify-attestations:
needs: [create-manifest]
if: ${{ always() && needs.create-manifest.result == 'success' }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image refs
id: refs
shell: bash
env:
SLIM_TAGS: ${{ steps.tags.outputs.slim }}
AMD64_SLIM_DIGEST: ${{ needs.build-amd64.outputs.slim-digest }}
ARM64_SLIM_DIGEST: ${{ needs.build-arm64.outputs.slim-digest }}
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
run: |
set -euo pipefail
mapfile -t tags <<< "${SLIM_TAGS}"
args=()
for tag in "${tags[@]}"; do
[ -z "$tag" ] && continue
args+=("-t" "$tag")
done
docker buildx imagetools create "${args[@]}" \
"${AMD64_SLIM_DIGEST}" \
"${ARM64_SLIM_DIGEST}"
multi_refs=()
slim_multi_refs=()
amd64_refs=()
arm64_refs=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
multi_refs+=("${IMAGE}:main")
slim_multi_refs+=("${IMAGE}:main-slim")
amd64_refs+=("${IMAGE}:main-amd64" "${IMAGE}:main-slim-amd64")
arm64_refs+=("${IMAGE}:main-arm64" "${IMAGE}:main-slim-arm64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
multi_refs+=("${IMAGE}:${version}")
slim_multi_refs+=("${IMAGE}:${version}-slim")
amd64_refs+=("${IMAGE}:${version}-amd64" "${IMAGE}:${version}-slim-amd64")
arm64_refs+=("${IMAGE}:${version}-arm64" "${IMAGE}:${version}-slim-arm64")
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
multi_refs+=("${IMAGE}:latest")
slim_multi_refs+=("${IMAGE}:slim")
fi
fi
if [[ ${#multi_refs[@]} -eq 0 || ${#amd64_refs[@]} -eq 0 || ${#arm64_refs[@]} -eq 0 ]]; then
echo "::error::No Docker image refs resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "multi<<EOF"
printf "%s\n" "${multi_refs[@]}" "${slim_multi_refs[@]}"
echo "EOF"
echo "amd64<<EOF"
printf "%s\n" "${amd64_refs[@]}"
echo "EOF"
echo "arm64<<EOF"
printf "%s\n" "${arm64_refs[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Verify Docker attestations
shell: bash
env:
MULTI_REFS: ${{ steps.refs.outputs.multi }}
AMD64_REFS: ${{ steps.refs.outputs.amd64 }}
ARM64_REFS: ${{ steps.refs.outputs.arm64 }}
run: |
set -euo pipefail
mapfile -t multi_refs <<< "${MULTI_REFS}"
mapfile -t amd64_refs <<< "${AMD64_REFS}"
mapfile -t arm64_refs <<< "${ARM64_REFS}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
--platform linux/arm64 \
"${multi_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
"${amd64_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/arm64 \
"${arm64_refs[@]}"

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

@@ -10,6 +10,11 @@ on:
required: false
default: false
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
workflow_call:
inputs:
ref:
@@ -21,6 +26,11 @@ on:
required: false
default: true
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
permissions:
contents: read
@@ -103,7 +113,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -114,7 +123,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:
@@ -204,7 +227,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -318,7 +340,7 @@ jobs:
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: latest
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh

View File

@@ -34,106 +34,39 @@ env:
PNPM_VERSION: "10.33.0"
jobs:
validate_dispatch_ref:
name: Validate dispatch ref
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Require main workflow ref
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "NPM Telegram beta E2E must be dispatched from main so workflow logic stays controlled." >&2
exit 1
fi
approve_release_manager:
name: Approve npm Telegram beta E2E
needs: validate_dispatch_ref
runs-on: ubuntu-latest
environment: npm-release
steps:
- name: Record approval
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
run: echo "Approved npm Telegram beta E2E for ${PACKAGE_SPEC}"
prepare_docker_e2e_image:
name: Prepare Docker E2E image
needs: validate_dispatch_ref
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout main
- name: Checkout dispatch ref
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Resolve Docker E2E image tag
id: image
shell: bash
env:
SELECTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
max-cache-size-mb: 800000
- name: Build and push Docker E2E image
- name: Build Docker E2E image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
platforms: linux/amd64
tags: ${{ steps.image.outputs.image }}
tags: openclaw-docker-e2e:local
load: true
push: false
provenance: false
push: true
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
needs: [approve_release_manager, prepare_docker_e2e_image]
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: read
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -178,7 +111,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
@@ -186,6 +119,7 @@ jobs:
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
run: |
set -euo pipefail

View File

@@ -23,6 +23,11 @@ on:
required: false
default: true
type: boolean
docker_lanes:
description: Comma/space separated Docker scheduler lane names to run against the prepared image
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -54,6 +59,11 @@ on:
required: false
default: true
type: boolean
docker_lanes:
description: Comma/space separated Docker scheduler lane names to run against the prepared image
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -182,6 +192,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
INPUT_REF: ${{ inputs.ref }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
set -euo pipefail
@@ -189,9 +200,15 @@ jobs:
trusted_reason=""
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
fi
if git merge-base --is-ancestor "$selected_sha" refs/remotes/origin/main; then
trusted_reason="main-ancestor"
elif [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] &&
[[ "$selected_sha" == "$(git rev-parse "refs/remotes/origin/${WORKFLOW_REF_NAME}")" ]]; then
trusted_reason="release-branch-head"
elif git tag --points-at "$selected_sha" | grep -Eq '^v'; then
trusted_reason="release-tag"
else
@@ -208,7 +225,7 @@ jobs:
if [[ -z "$trusted_reason" ]]; then
echo "Ref '${INPUT_REF}' resolved to $selected_sha, which is not trusted for secret-bearing live/E2E checks." >&2
echo "Allowed refs must be on main, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
echo "Allowed refs must be on main, match the current release branch head, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
exit 1
fi
@@ -303,7 +320,7 @@ jobs:
requires_live_suites: false
- suite_id: openai-ws-stream-live-e2e
label: OpenAI WebSocket live E2E
command: pnpm test:e2e -- src/agents/openai-ws-stream.e2e.test.ts
command: pnpm test:e2e src/agents/openai-ws-stream.e2e.test.ts
timeout_minutes: 90
requires_repo_e2e: false
requires_live_suites: true
@@ -363,83 +380,23 @@ jobs:
validate_docker_e2e:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_release_path_suites
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: docker-onboard
label: Onboarding Docker E2E
command: pnpm test:docker:onboard
timeout_minutes: 60
release_path: true
- suite_id: docker-npm-onboard-channel-agent
label: Npm Onboard Channel Agent Docker E2E
command: pnpm test:docker:npm-onboard-channel-agent
timeout_minutes: 90
release_path: true
- suite_id: docker-gateway-network
label: Gateway Network Docker E2E
command: pnpm test:docker:gateway-network
timeout_minutes: 60
release_path: true
- suite_id: docker-openai-web-search-minimal
label: OpenAI Web Search Minimal Docker E2E
command: pnpm test:docker:openai-web-search-minimal
timeout_minutes: 60
release_path: true
- suite_id: docker-mcp-channels
label: MCP Channels Docker E2E
command: pnpm test:docker:mcp-channels
timeout_minutes: 60
release_path: true
- suite_id: docker-pi-bundle-mcp-tools
label: Pi Bundle MCP Tools Docker E2E
command: pnpm test:docker:pi-bundle-mcp-tools
timeout_minutes: 60
release_path: true
- suite_id: docker-cron-mcp-cleanup
label: Cron MCP Cleanup Docker E2E
command: pnpm test:docker:cron-mcp-cleanup
timeout_minutes: 60
release_path: true
- suite_id: docker-plugins
label: Plugins Docker E2E
command: pnpm test:docker:plugins
timeout_minutes: 75
release_path: true
- suite_id: docker-plugin-update
label: Plugin Update Docker E2E
command: pnpm test:docker:plugin-update
timeout_minutes: 60
release_path: true
- suite_id: docker-config-reload
label: Config Reload Docker E2E
command: pnpm test:docker:config-reload
timeout_minutes: 60
release_path: true
- suite_id: docker-bundled-channel-deps
label: Bundled Channel Runtime Deps Docker E2E
command: pnpm test:docker:bundled-channel-deps
timeout_minutes: 75
release_path: true
- suite_id: docker-doctor-switch
label: Doctor Install Switch Docker E2E
command: pnpm test:docker:doctor-switch
timeout_minutes: 60
release_path: true
- suite_id: docker-qr
label: QR Import Docker E2E
command: pnpm test:docker:qr
timeout_minutes: 60
release_path: true
- suite_id: docker-install-e2e
label: Installer Docker E2E
command: pnpm test:install:e2e
- chunk_id: core
label: core
timeout_minutes: 120
release_path: true
- chunk_id: package-update
label: package/update
timeout_minutes: 180
- chunk_id: plugins-integrations
label: plugins/integrations
timeout_minutes: 180
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -486,7 +443,12 @@ jobs:
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
DOCKER_E2E_CHUNK: ${{ matrix.chunk_id }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
@@ -511,45 +473,188 @@ jobs:
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
- name: Plan and hydrate Docker E2E chunk
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: chunk
chunk: ${{ matrix.chunk_id }}
include-openwebui: ${{ inputs.include_openwebui }}
- name: Run Docker E2E chunk
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
echo "OPENCLAW_E2E_MODELS=both" >> "$GITHUB_ENV"
;;
esac
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_CHUNK="${DOCKER_E2E_CHUNK}"
export OPENCLAW_DOCKER_ALL_BUILD=0
export OPENCLAW_DOCKER_ALL_PREFLIGHT=0
export OPENCLAW_DOCKER_ALL_FAIL_FAST=0
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json"
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
- name: Validate suite credentials
pnpm test:docker:all
- name: Summarize Docker E2E chunk
if: always()
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for installer Docker E2E." >&2
exit 1
}
if [[ -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for installer Docker E2E." >&2
exit 1
fi
;;
esac
summary=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker chunk summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/docker-e2e.mjs summary "$summary" "Docker E2E chunk: ${DOCKER_E2E_CHUNK:-unknown}" >> "$GITHUB_STEP_SUMMARY"
- name: Run ${{ matrix.label }}
run: ${{ matrix.command }}
- name: Upload Docker E2E chunk artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: docker-e2e-${{ matrix.chunk_id }}
path: .artifacts/docker-tests/
if-no-files-found: ignore
validate_docker_lanes:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 180
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
DOCKER_E2E_LANES: ${{ inputs.docker_lanes }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Log in to GHCR for shared Docker E2E image
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Plan and hydrate targeted Docker E2E lanes
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: targeted
lanes: ${{ inputs.docker_lanes }}
include-openwebui: ${{ inputs.include_openwebui }}
- name: Run targeted Docker E2E lanes
shell: bash
run: |
set -euo pipefail
export OPENCLAW_DOCKER_ALL_LANES="${DOCKER_E2E_LANES}"
export OPENCLAW_DOCKER_ALL_PREFLIGHT=0
export OPENCLAW_DOCKER_ALL_FAIL_FAST=0
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-timings.json"
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
pnpm test:docker:live-build
fi
export OPENCLAW_DOCKER_ALL_BUILD=0
pnpm test:docker:all
- name: Summarize targeted Docker E2E lanes
if: always()
shell: bash
run: |
set -euo pipefail
summary=".artifacts/docker-tests/targeted/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker targeted summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/docker-e2e.mjs summary "$summary" "Docker E2E targeted lanes" >> "$GITHUB_STEP_SUMMARY"
- name: Upload targeted Docker E2E artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: docker-e2e-targeted
path: .artifacts/docker-tests/
if-no-files-found: ignore
validate_docker_openwebui:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_openwebui
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 75
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
steps:
- name: Checkout selected ref
@@ -586,7 +691,7 @@ jobs:
prepare_docker_e2e_image:
needs: validate_selected_ref
if: inputs.include_release_path_suites || inputs.include_openwebui
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
permissions:
@@ -594,6 +699,13 @@ jobs:
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
bare_image: ${{ steps.image.outputs.bare_image }}
functional_image: ${{ steps.image.outputs.functional_image }}
needs_bare_image: ${{ steps.plan.outputs.needs_bare_image }}
needs_e2e_image: ${{ steps.plan.outputs.needs_e2e_image }}
needs_functional_image: ${{ steps.plan.outputs.needs_functional_image }}
needs_live_image: ${{ steps.plan.outputs.needs_live_image }}
needs_package: ${{ steps.plan.outputs.needs_package }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
@@ -604,7 +716,7 @@ jobs:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Resolve shared Docker E2E image tag
- name: Resolve shared Docker E2E image tags
id: image
shell: bash
env:
@@ -612,31 +724,127 @@ jobs:
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
bare_image="ghcr.io/${repository}-docker-e2e-bare:${SELECTED_SHA}"
functional_image="ghcr.io/${repository}-docker-e2e-functional:${SELECTED_SHA}"
image="$functional_image"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Shared Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
echo "bare_image=$bare_image" >> "$GITHUB_OUTPUT"
echo "functional_image=$functional_image" >> "$GITHUB_OUTPUT"
echo "Shared Docker E2E bare image: \`$bare_image\`" >> "$GITHUB_STEP_SUMMARY"
echo "Shared Docker E2E functional image: \`$functional_image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Plan Docker E2E images
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: prepare
lanes: ${{ inputs.docker_lanes }}
include-release-path-suites: ${{ inputs.include_release_path_suites }}
include-openwebui: ${{ inputs.include_openwebui }}
hydrate-artifacts: "false"
- name: Setup Node environment
if: steps.plan.outputs.needs_package == '1'
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Pack OpenClaw package for Docker E2E
if: steps.plan.outputs.needs_package == '1'
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/docker-e2e-package
node scripts/package-openclaw-for-docker.mjs \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz
- name: Upload OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1'
uses: actions/upload-artifact@v7
with:
name: docker-e2e-package
path: .artifacts/docker-e2e-package/openclaw-current.tgz
if-no-files-found: error
- name: Log in to GHCR
if: steps.plan.outputs.needs_e2e_image == '1'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Check existing shared Docker E2E images
id: image_exists
if: steps.plan.outputs.needs_e2e_image == '1'
shell: bash
run: |
set -euo pipefail
bare_exists=0
functional_exists=0
needs_build=0
if [[ "${{ steps.plan.outputs.needs_bare_image }}" == "1" ]]; then
if docker manifest inspect "${{ steps.image.outputs.bare_image }}" >/dev/null 2>&1; then
bare_exists=1
echo "Shared Docker E2E bare image already exists: ${{ steps.image.outputs.bare_image }}"
else
needs_build=1
fi
fi
if [[ "${{ steps.plan.outputs.needs_functional_image }}" == "1" ]]; then
if docker manifest inspect "${{ steps.image.outputs.functional_image }}" >/dev/null 2>&1; then
functional_exists=1
echo "Shared Docker E2E functional image already exists: ${{ steps.image.outputs.functional_image }}"
else
needs_build=1
fi
fi
echo "bare_exists=$bare_exists" >> "$GITHUB_OUTPUT"
echo "functional_exists=$functional_exists" >> "$GITHUB_OUTPUT"
echo "needs_build=$needs_build" >> "$GITHUB_OUTPUT"
- name: Setup Docker builder
if: steps.image_exists.outputs.needs_build == '1'
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Build and push shared Docker E2E image
- name: Build and push bare Docker E2E image
if: steps.plan.outputs.needs_bare_image == '1' && steps.image_exists.outputs.bare_exists != '1'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
target: bare
platforms: linux/amd64
cache-from: type=gha,scope=docker-e2e
cache-to: type=gha,mode=max,scope=docker-e2e
tags: ${{ steps.image.outputs.image }}
provenance: false
cache-from: type=gha,scope=docker-e2e-bare
cache-to: type=gha,mode=max,scope=docker-e2e-bare
tags: ${{ steps.image.outputs.bare_image }}
sbom: true
provenance: mode=max
push: true
- name: Build and push functional Docker E2E image
if: steps.plan.outputs.needs_functional_image == '1' && steps.image_exists.outputs.functional_exists != '1'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./scripts/e2e/Dockerfile
target: functional
build-contexts: |
openclaw_package=.artifacts/docker-e2e-package
platforms: linux/amd64
cache-from: |
type=gha,scope=docker-e2e-bare
type=gha,scope=docker-e2e-functional
cache-to: type=gha,mode=max,scope=docker-e2e-functional
tags: ${{ steps.image.outputs.functional_image }}
sbom: true
provenance: mode=max
push: true
validate_live_models_docker:

View File

@@ -29,7 +29,7 @@ jobs:
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests (primary)
- name: Mark stale unassigned issues and pull requests (primary)
id: stale-primary
continue-on-error: true
uses: actions/stale@v10
@@ -56,12 +56,60 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (primary)
id: assigned-issue-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (primary)
id: assigned-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Check stale state cache
id: stale-state
if: always()
@@ -86,7 +134,7 @@ jobs:
core.warning(`Failed to check stale state cache: ${message}`);
core.setOutput("has_state", "false");
}
- name: Mark stale issues and pull requests (fallback)
- name: Mark stale unassigned issues and pull requests (fallback)
if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
@@ -112,12 +160,58 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (fallback)
if: (steps.assigned-issue-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (fallback)
if: (steps.assigned-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:
permissions:

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"

38
.gitignore vendored
View File

@@ -97,6 +97,38 @@ USER.md
# local tooling
.serena/
# Local project-agent skill installs. Only repo-owned skills are visible by
# default; promoting a new repo skill should require an intentional `git add -f`.
.agents/skills/*
!.agents/skills/blacksmith-testbox/
!.agents/skills/blacksmith-testbox/**
!.agents/skills/openclaw-ghsa-maintainer/
!.agents/skills/openclaw-ghsa-maintainer/**
!.agents/skills/openclaw-parallels-smoke/
!.agents/skills/openclaw-parallels-smoke/**
!.agents/skills/openclaw-pr-maintainer/
!.agents/skills/openclaw-pr-maintainer/**
!.agents/skills/openclaw-qa-testing/
!.agents/skills/openclaw-qa-testing/**
!.agents/skills/openclaw-release-maintainer/
!.agents/skills/openclaw-release-maintainer/**
!.agents/skills/openclaw-secret-scanning-maintainer/
!.agents/skills/openclaw-secret-scanning-maintainer/**
!.agents/skills/openclaw-test-heap-leaks/
!.agents/skills/openclaw-test-heap-leaks/**
!.agents/skills/openclaw-test-performance/
!.agents/skills/openclaw-test-performance/**
!.agents/skills/openclaw-testing/
!.agents/skills/openclaw-testing/**
!.agents/skills/optimizetests/
!.agents/skills/optimizetests/**
!.agents/skills/parallels-discord-roundtrip/
!.agents/skills/parallels-discord-roundtrip/**
!.agents/skills/security-triage/
!.agents/skills/security-triage/**
!.agents/skills/tag-duplicate-prs-issues/
!.agents/skills/tag-duplicate-prs-issues/**
# Agent credentials and memory (NEVER COMMIT)
/memory/
.agent/*.json
@@ -128,15 +160,14 @@ dist/protocol.schema.json
# Synthing
**/.stfolder/
.dev-state
docs/superpowers/plans/2026-03-10-collapsed-side-nav.md
docs/superpowers/specs/2026-03-10-collapsed-side-nav-design.md
docs/superpowers
.superpowers/
.gitignore
test/config-form.analyze.telegram.test.ts
ui/src/ui/theme-variants.browser.test.ts
ui/src/ui/__screenshots__
ui/src/ui/views/__screenshots__
ui/.vitest-attachments
docs/superpowers
# Generated docs baseline artifacts (locally generated, only hashes tracked)
docs/.generated/*.json
@@ -147,6 +178,7 @@ changelog/fragments/
# Local scratch workspace
.tmp/
.vmux*
.artifacts/
test/fixtures/openclaw-vitest-unit-report.json
analysis/

View File

@@ -9,6 +9,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Run docs list first: `pnpm docs:list` if available; read relevant docs only.
- High-confidence answers only when fixing/triaging: verify source, tests, shipped/current behavior, and dependency contracts before deciding.
- Dependency-backed behavior: read upstream dependency docs/source/types first. Do not assume APIs, defaults, errors, timing, or runtime behavior.
- Live-verify when feasible. Check env/`~/.profile` for keys before assuming live tests are blocked; keep secret output redacted.
- Missing deps: `pnpm install`, retry once, then report first actionable error.
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
- Wording: product/docs/UI/changelog say "plugin/plugins"; `extensions/` is internal.
@@ -28,6 +29,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Extension prod code: no core `src/**`, `src/plugin-sdk-internal/**`, other extension `src/**`, or relative outside package.
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use `api.ts`, SDK facade, generic contracts.
- Extension-owned behavior stays extension-owned: repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
@@ -44,21 +46,27 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Smart gate: `pnpm check:changed`; explain `pnpm changed:lanes --json`; staged preview `pnpm check:changed --staged`.
- Sparse worktrees: `pnpm check:changed` is sparse-safe and may skip sparse-missing typecheck projects; do not expand sparse checkout just to satisfy changed-gate tsgo. Direct `pnpm tsgo*` remains strict; use a fuller worktree when you need direct typecheck proof.
- Prod sweep: `pnpm check`; tests: `pnpm test`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; never raw `vitest`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); do not add `tsc --noEmit`, `typecheck`, `check:types`.
- Format/lint: `pnpm format:check`/`pnpm format`; `pnpm lint*` lanes.
- Formatting: use `oxfmt`, not Prettier. Prefer `pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
## GitHub / 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.
- PR scan/triage: no unsolicited PR comments/reviews. Report in chat only unless explicitly asked, or a close/duplicate action needs a reason comment.
- 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`.
- PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
- PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
- CI polling: exact SHA, needed fields only. Example: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
- Post-land wait: minimal. Exact landed SHA only. If superseded on `main`, same-branch `cancel-in-progress` cancellations are expected; stop once local touched-surface proof exists. Never wait for newer unrelated `main` unless asked.
@@ -80,7 +88,15 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- extension tests: extension test typecheck/tests
- 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`.
- Before handoff/push for code/test/runtime/config changes: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- 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.
@@ -104,6 +120,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## Tests
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.4`.
- Avoid brittle tests that grep workflow/docs strings for operator policy. Prefer executable behavior, parsed config/schema checks, or live run proof; put release/CI policy reminders in AGENTS/docs instead.
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
- Hot tests: avoid per-test `vi.resetModules()` + heavy imports. Measure with `pnpm test:perf:imports <file>` / `pnpm test:perf:hotspots --limit N`.
- Seam depth: pure helper/contract unit tests; one integration smoke per boundary.
@@ -119,14 +136,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`; at most one contributor mention, prefer `Thanks @user`.
- 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` or `Thanks @codex`.
- 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.

File diff suppressed because it is too large Load Diff

View File

@@ -9,22 +9,19 @@
# bundled plugin workspace tree, so the main build layer is not invalidated by
# unrelated plugin source changes.
#
# Two runtime variants:
# Default (bookworm): docker build .
# Slim (bookworm-slim): docker build --build-arg OPENCLAW_VARIANT=slim .
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_VARIANT=default
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_DOCKER_APT_UPGRADE=1
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_DIGEST="sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
# Base images are pinned to SHA256 digests for reproducible builds.
# Trade-off: digests must be updated manually when upstream tags move.
# To update, run: docker buildx imagetools inspect node:24-bookworm (or podman)
# and replace the digest below with the current multi-arch manifest list entry.
# Dependabot refreshes these blessed digests; release builds consume the
# reviewed base snapshot instead of mutating distro state on every build.
# To update, run: docker buildx imagetools inspect node:24-bookworm and
# node:24-bookworm-slim (or podman) and replace the digests below with the
# current multi-arch manifest list entries.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
ARG OPENCLAW_EXTENSIONS
@@ -125,22 +122,15 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default
ARG OPENCLAW_NODE_BOOKWORM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_DIGEST}"
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim
# ── Runtime base image ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm-slim" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST}"
# ── Stage 3: Runtime ────────────────────────────────────────────
FROM base-${OPENCLAW_VARIANT}
ARG OPENCLAW_VARIANT
FROM base-runtime
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_DOCKER_APT_UPGRADE
# OCI base-image metadata for downstream image consumers.
# If you change these annotations, also update:
@@ -155,16 +145,10 @@ LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
WORKDIR /app
# Install system utilities present in bookworm but missing in bookworm-slim.
# On the full bookworm image these are already installed (apt-get is a no-op).
# Smoke workflows can opt out of distro upgrades to cut repeated CI time while
# keeping the default runtime image behavior unchanged.
# Install runtime system utilities missing from bookworm-slim.
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update && \
if [ "${OPENCLAW_DOCKER_APT_UPGRADE}" != "0" ]; then \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends; \
fi && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
procps hostname curl git lsof openssl
@@ -173,6 +157,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

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -24,7 +24,6 @@ ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends ${PACKAGES}
RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm; fi

View File

@@ -96,7 +96,7 @@ Model note: while many providers and models are supported, prefer a current flag
## Install (recommended)
Runtime: **Node 24 (recommended) or Node 22.16+**.
Runtime: **Node 24 (recommended) or Node 22.14+**.
```bash
npm install -g openclaw@latest
@@ -109,7 +109,7 @@ OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so i
## Quick start (TL;DR)
Runtime: **Node 24 (recommended) or Node 22.16+**.
Runtime: **Node 24 (recommended) or Node 22.14+**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
@@ -119,7 +119,7 @@ openclaw onboard --install-daemon
openclaw gateway --port 18789 --verbose
# Send a message
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
openclaw message send --target +1234567890 --message "Hello from OpenClaw"
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/QQ/WebChat)
openclaw agent --message "Ship checklist" --thinking high

View File

@@ -288,7 +288,7 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for *
### Node.js Version
OpenClaw requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches:
OpenClaw requires **Node.js 22.14.0 or later** (LTS). This version includes important security patches:
- CVE-2025-59466: async_hooks DoS vulnerability
- CVE-2026-21636: Permission model bypass vulnerability
@@ -296,7 +296,7 @@ OpenClaw requires **Node.js 22.12.0 or later** (LTS). This version includes impo
Verify your Node.js version:
```bash
node --version # Should be v22.12.0 or later
node --version # Should be v22.14.0 or later
```
### Docker Security

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

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

View File

@@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
@@ -52,7 +53,7 @@
<service
android:name=".NodeForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />
android:foregroundServiceType="dataSync|microphone" />
<service
android:name=".node.DeviceNotificationListenerService"
android:label="@string/app_name"

View File

@@ -101,7 +101,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
val micEnabled: StateFlow<Boolean> = prefs.talkEnabled
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
val micCooldown: StateFlow<Boolean> = runtimeState(initial = false) { it.micCooldown }
val micStatusText: StateFlow<String> = runtimeState(initial = "Mic off") { it.micStatusText }
@@ -111,6 +112,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtimeState(initial = emptyList()) { it.micConversation }
val micInputLevel: StateFlow<Float> = runtimeState(initial = 0f) { it.micInputLevel }
val micIsSending: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsSending }
val talkModeEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeEnabled }
val talkModeListening: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeListening }
val talkModeSpeaking: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeSpeaking }
val talkModeStatusText: StateFlow<String> = runtimeState(initial = "Off") { it.talkModeStatusText }
val chatSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.chatSessionKey }
val chatSessionId: StateFlow<String?> = runtimeState(initial = null) { it.chatSessionId }
@@ -283,6 +288,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
ensureRuntime().setMicEnabled(enabled)
}
fun setTalkModeEnabled(enabled: Boolean) {
ensureRuntime().setTalkModeEnabled(enabled)
}
fun setSpeakerEnabled(enabled: Boolean) {
ensureRuntime().setSpeakerEnabled(enabled)
}

View File

@@ -3,12 +3,14 @@ package ai.openclaw.app
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -21,6 +23,7 @@ class NodeForegroundService : Service() {
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var notificationJob: Job? = null
private var didStartForeground = false
private var voiceCaptureMode = VoiceCaptureMode.Off
override fun onCreate() {
super.onCreate()
@@ -36,22 +39,51 @@ class NodeForegroundService : Service() {
notificationJob =
scope.launch {
combine(
runtime.statusText,
runtime.serverName,
runtime.isConnected,
runtime.micEnabled,
runtime.micIsListening,
) { status, server, connected, micEnabled, micListening ->
Quint(status, server, connected, micEnabled, micListening)
}.collect { (status, server, connected, micEnabled, micListening) ->
val title = if (connected) "OpenClaw Node · Connected" else "OpenClaw Node"
val micSuffix =
if (micEnabled) {
if (micListening) " · Mic: Listening" else " · Mic: Pending"
} else {
""
combine(
runtime.statusText,
runtime.serverName,
runtime.isConnected,
runtime.voiceCaptureMode,
) { status, server, connected, mode ->
VoiceNotificationBase(
status = status,
server = server,
connected = connected,
mode = mode,
)
},
combine(
runtime.micEnabled,
runtime.micIsListening,
runtime.talkModeListening,
runtime.talkModeSpeaking,
) { micEnabled, micListening, talkListening, talkSpeaking ->
VoiceNotificationCapture(
micEnabled = micEnabled,
micListening = micListening,
talkListening = talkListening,
talkSpeaking = talkSpeaking,
)
},
) { base, capture ->
VoiceNotificationState(base = base, capture = capture)
}.collect { state ->
voiceCaptureMode = state.mode
val title =
when {
state.connected && state.mode == VoiceCaptureMode.TalkMode -> "OpenClaw Node · Talk"
state.connected -> "OpenClaw Node · Connected"
else -> "OpenClaw Node"
}
val text = (server?.let { "$status · $it" } ?: status) + micSuffix
val text =
(state.server?.let { "${state.status} · $it" } ?: state.status) +
voiceNotificationSuffix(
mode = state.mode,
manualMicEnabled = state.capture.micEnabled,
manualMicListening = state.capture.micListening,
talkListening = state.capture.talkListening,
talkSpeaking = state.capture.talkSpeaking,
)
startForegroundWithTypes(
notification = buildNotification(title = title, text = text),
@@ -60,13 +92,27 @@ class NodeForegroundService : Service() {
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
when (intent?.action) {
ACTION_STOP -> {
(application as NodeApp).peekRuntime()?.disconnect()
stopSelf()
return START_NOT_STICKY
}
ACTION_SET_VOICE_CAPTURE_MODE -> {
voiceCaptureMode = intent.getStringExtra(EXTRA_VOICE_CAPTURE_MODE).toVoiceCaptureMode()
startForegroundWithTypes(
notification =
buildNotification(
title = "OpenClaw Node",
text = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) "Talk mode active" else "Connected",
),
)
}
}
// Keep running; connection is managed by NodeRuntime (auto-reconnect + manual).
return START_STICKY
@@ -127,17 +173,13 @@ class NodeForegroundService : Service() {
.build()
}
private fun updateNotification(notification: Notification) {
val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mgr.notify(NOTIFICATION_ID, notification)
}
private fun startForegroundWithTypes(notification: Notification) {
val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
if (didStartForeground) {
updateNotification(notification)
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
return
}
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
didStartForeground = true
}
@@ -146,6 +188,8 @@ class NodeForegroundService : Service() {
private const val NOTIFICATION_ID = 1
private const val ACTION_STOP = "ai.openclaw.app.action.STOP"
private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)
@@ -156,7 +200,85 @@ class NodeForegroundService : Service() {
val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
context.startService(intent)
}
fun setVoiceCaptureMode(
context: Context,
mode: VoiceCaptureMode,
) {
val intent =
Intent(context, NodeForegroundService::class.java)
.setAction(ACTION_SET_VOICE_CAPTURE_MODE)
.putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
if (mode == VoiceCaptureMode.TalkMode) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
}
}
}
private data class Quint<A, B, C, D, E>(val first: A, val second: B, val third: C, val fourth: D, val fifth: E)
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
return if (mode == VoiceCaptureMode.TalkMode) {
base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
base
}
}
internal fun voiceNotificationSuffix(
mode: VoiceCaptureMode,
manualMicEnabled: Boolean,
manualMicListening: Boolean,
talkListening: Boolean,
talkSpeaking: Boolean,
): String {
return when (mode) {
VoiceCaptureMode.TalkMode ->
when {
talkSpeaking -> " · Talk: Speaking"
talkListening -> " · Talk: Listening"
else -> " · Talk: On"
}
VoiceCaptureMode.ManualMic ->
if (manualMicEnabled) {
if (manualMicListening) " · Mic: Listening" else " · Mic: Pending"
} else {
""
}
VoiceCaptureMode.Off -> ""
}
}
private fun String?.toVoiceCaptureMode(): VoiceCaptureMode {
return VoiceCaptureMode.entries.firstOrNull { it.name == this } ?: VoiceCaptureMode.Off
}
private data class VoiceNotificationBase(
val status: String,
val server: String?,
val connected: Boolean,
val mode: VoiceCaptureMode,
)
private data class VoiceNotificationCapture(
val micEnabled: Boolean,
val micListening: Boolean,
val talkListening: Boolean,
val talkSpeaking: Boolean,
)
private data class VoiceNotificationState(
val base: VoiceNotificationBase,
val capture: VoiceNotificationCapture,
) {
val status: String
get() = base.status
val server: String?
get() = base.server
val connected: Boolean
get() = base.connected
val mode: VoiceCaptureMode
get() = base.mode
}

View File

@@ -64,6 +64,8 @@ class NodeRuntime(
private val json = Json { ignoreUnknownKeys = true }
private val externalAudioCaptureActive = MutableStateFlow(false)
private val _voiceCaptureMode = MutableStateFlow(VoiceCaptureMode.Off)
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = _voiceCaptureMode.asStateFlow()
private val discovery = GatewayDiscovery(appContext, scope = scope)
val gateways: StateFlow<List<GatewayEndpoint>> = discovery.gateways
@@ -428,6 +430,18 @@ class NodeRuntime(
)
}
val talkModeEnabled: StateFlow<Boolean>
get() = talkMode.isEnabled
val talkModeListening: StateFlow<Boolean>
get() = talkMode.isListening
val talkModeSpeaking: StateFlow<Boolean>
get() = talkMode.isSpeaking
val talkModeStatusText: StateFlow<String>
get() = talkMode.statusText
private fun syncMainSessionKey(agentId: String?) {
val resolvedKey = resolveNodeMainSessionKey(agentId)
// Always push the resolved session key into TalkMode, even when the
@@ -599,17 +613,8 @@ class NodeRuntime(
prefs.loadGatewayToken()
}
scope.launch {
prefs.talkEnabled.collect { enabled ->
// MicCaptureManager handles STT + send to gateway, while the dedicated
// reply speaker handles TTS for assistant replies in the voice tab.
micCapture.setMicEnabled(enabled)
if (enabled) {
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
externalAudioCaptureActive.value = enabled
}
if (prefs.voiceMicEnabled.value) {
setVoiceCaptureMode(VoiceCaptureMode.ManualMic, persistManualMic = false)
}
scope.launch(Dispatchers.Default) {
@@ -643,7 +648,7 @@ class NodeRuntime(
if (value) {
reconnectPreferredGatewayOnForeground()
} else {
stopActiveVoiceSession()
stopManualVoiceSession()
}
}
@@ -757,21 +762,17 @@ class NodeRuntime(
fun setVoiceScreenActive(active: Boolean) {
if (!active) {
stopActiveVoiceSession()
stopManualVoiceSession()
}
// Don't re-enable on active=true; mic toggle drives that
}
fun setMicEnabled(value: Boolean) {
prefs.setTalkEnabled(value)
if (value) {
// Tapping mic on interrupts any active TTS (barge-in)
stopVoicePlayback()
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
micCapture.setMicEnabled(value)
externalAudioCaptureActive.value = value
setVoiceCaptureMode(if (value) VoiceCaptureMode.ManualMic else VoiceCaptureMode.Off)
}
fun setTalkModeEnabled(value: Boolean) {
setVoiceCaptureMode(if (value) VoiceCaptureMode.TalkMode else VoiceCaptureMode.Off)
}
val speakerEnabled: StateFlow<Boolean>
@@ -786,11 +787,72 @@ class NodeRuntime(
talkMode.setPlaybackEnabled(value)
}
private fun setVoiceCaptureMode(
mode: VoiceCaptureMode,
persistManualMic: Boolean = true,
) {
if (mode == VoiceCaptureMode.TalkMode && !hasRecordAudioPermission()) {
_voiceCaptureMode.value = VoiceCaptureMode.Off
externalAudioCaptureActive.value = false
return
}
if (_voiceCaptureMode.value == mode) return
_voiceCaptureMode.value = mode
when (mode) {
VoiceCaptureMode.Off -> {
talkMode.ttsOnAllResponses = false
talkMode.setEnabled(false)
stopVoicePlayback()
micCapture.setMicEnabled(false)
if (persistManualMic) {
prefs.setVoiceMicEnabled(false)
}
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
externalAudioCaptureActive.value = false
}
VoiceCaptureMode.ManualMic -> {
talkMode.ttsOnAllResponses = false
talkMode.setEnabled(false)
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.ManualMic)
if (persistManualMic) {
prefs.setVoiceMicEnabled(true)
}
// Tapping mic on interrupts any active TTS (barge-in).
stopVoicePlayback()
scope.launch { talkMode.ensureChatSubscribed() }
micCapture.setMicEnabled(true)
externalAudioCaptureActive.value = true
}
VoiceCaptureMode.TalkMode -> {
if (persistManualMic) {
prefs.setVoiceMicEnabled(false)
}
micCapture.setMicEnabled(false)
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.TalkMode)
talkMode.ttsOnAllResponses = true
talkMode.setPlaybackEnabled(speakerEnabled.value)
scope.launch { talkMode.ensureChatSubscribed() }
talkMode.setEnabled(true)
externalAudioCaptureActive.value = true
}
}
}
private fun stopManualVoiceSession() {
if (_voiceCaptureMode.value != VoiceCaptureMode.ManualMic) return
setVoiceCaptureMode(VoiceCaptureMode.Off)
}
private fun stopActiveVoiceSession() {
talkMode.ttsOnAllResponses = false
talkMode.setEnabled(false)
stopVoicePlayback()
micCapture.setMicEnabled(false)
prefs.setTalkEnabled(false)
prefs.setVoiceMicEnabled(false)
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
_voiceCaptureMode.value = VoiceCaptureMode.Off
externalAudioCaptureActive.value = false
}
@@ -970,6 +1032,7 @@ class NodeRuntime(
}
fun disconnect() {
stopActiveVoiceSession()
connectedEndpoint = null
activeGatewayAuth = null
_pendingGatewayTrust.value = null

View File

@@ -37,6 +37,7 @@ class SecurePrefs(
private const val notificationsForwardingMaxEventsPerMinuteKey =
"notifications.forwarding.maxEventsPerMinute"
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
private const val voiceMicEnabledKey = "voice.micEnabled"
}
private val appContext = context.applicationContext
@@ -162,8 +163,8 @@ class SecurePrefs(
private val _voiceWakeMode = MutableStateFlow(loadVoiceWakeMode())
val voiceWakeMode: StateFlow<VoiceWakeMode> = _voiceWakeMode
private val _talkEnabled = MutableStateFlow(plainPrefs.getBoolean("talk.enabled", false))
val talkEnabled: StateFlow<Boolean> = _talkEnabled
private val _voiceMicEnabled = MutableStateFlow(plainPrefs.getBoolean(voiceMicEnabledKey, false))
val voiceMicEnabled: StateFlow<Boolean> = _voiceMicEnabled
private val _speakerEnabled = MutableStateFlow(plainPrefs.getBoolean("voice.speakerEnabled", true))
val speakerEnabled: StateFlow<Boolean> = _speakerEnabled
@@ -478,9 +479,9 @@ class SecurePrefs(
_voiceWakeMode.value = mode
}
fun setTalkEnabled(value: Boolean) {
plainPrefs.edit { putBoolean("talk.enabled", value) }
_talkEnabled.value = value
fun setVoiceMicEnabled(value: Boolean) {
plainPrefs.edit { putBoolean(voiceMicEnabledKey, value) }
_voiceMicEnabled.value = value
}
fun setSpeakerEnabled(value: Boolean) {

View File

@@ -0,0 +1,7 @@
package ai.openclaw.app
enum class VoiceCaptureMode {
Off,
ManualMic,
TalkMode,
}

View File

@@ -35,10 +35,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MicOff
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MicOff
import androidx.compose.material.icons.filled.RecordVoiceOver
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
@@ -69,6 +70,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.VoiceCaptureMode
import ai.openclaw.app.voice.VoiceConversationEntry
import ai.openclaw.app.voice.VoiceConversationRole
import kotlin.math.max
@@ -81,6 +83,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
val listState = rememberLazyListState()
val gatewayStatus by viewModel.statusText.collectAsState()
val voiceCaptureMode by viewModel.voiceCaptureMode.collectAsState()
val micEnabled by viewModel.micEnabled.collectAsState()
val micCooldown by viewModel.micCooldown.collectAsState()
val speakerEnabled by viewModel.speakerEnabled.collectAsState()
@@ -90,12 +93,15 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
val micConversation by viewModel.micConversation.collectAsState()
val micInputLevel by viewModel.micInputLevel.collectAsState()
val micIsSending by viewModel.micIsSending.collectAsState()
val talkModeEnabled by viewModel.talkModeEnabled.collectAsState()
val talkModeListening by viewModel.talkModeListening.collectAsState()
val talkModeSpeaking by viewModel.talkModeSpeaking.collectAsState()
val hasStreamingAssistant = micConversation.any { it.role == VoiceConversationRole.Assistant && it.isStreaming }
val showThinkingBubble = micIsSending && !hasStreamingAssistant
var hasMicPermission by remember { mutableStateOf(context.hasRecordAudioPermission()) }
var pendingMicEnable by remember { mutableStateOf(false) }
var pendingVoicePermissionAction by remember { mutableStateOf<PendingVoicePermissionAction?>(null) }
DisposableEffect(lifecycleOwner, context) {
val observer =
@@ -107,7 +113,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
// Stop TTS when leaving the voice screen
// Manual mic is tied to the Voice tab; Talk Mode is explicit and can continue.
viewModel.setVoiceScreenActive(false)
}
}
@@ -115,10 +121,14 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
val requestMicPermission =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
hasMicPermission = granted
if (granted && pendingMicEnable) {
viewModel.setMicEnabled(true)
if (granted) {
when (pendingVoicePermissionAction) {
PendingVoicePermissionAction.ManualMic -> viewModel.setMicEnabled(true)
PendingVoicePermissionAction.TalkMode -> viewModel.setTalkModeEnabled(true)
null -> Unit
}
}
pendingMicEnable = false
pendingVoicePermissionAction = null
}
LaunchedEffect(micConversation.size, showThinkingBubble) {
@@ -161,12 +171,12 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
tint = mobileTextTertiary,
)
Text(
"Tap the mic to start",
"Tap mic or Talk",
style = mobileHeadline,
color = mobileTextSecondary,
)
Text(
"Each pause sends a turn automatically.",
"Mic sends turns; Talk keeps the conversation open.",
style = mobileCallout,
color = mobileTextTertiary,
)
@@ -263,7 +273,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
if (hasMicPermission) {
viewModel.setMicEnabled(true)
} else {
pendingMicEnable = true
pendingVoicePermissionAction = PendingVoicePermissionAction.ManualMic
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
}
},
@@ -287,11 +297,39 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
}
}
// Invisible spacer to balance the row (matches speaker column width)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(48.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
IconButton(
onClick = {
if (talkModeEnabled) {
viewModel.setTalkModeEnabled(false)
return@IconButton
}
if (hasMicPermission) {
viewModel.setTalkModeEnabled(true)
} else {
pendingVoicePermissionAction = PendingVoicePermissionAction.TalkMode
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
}
},
modifier = Modifier.size(48.dp),
colors =
IconButtonDefaults.iconButtonColors(
containerColor = if (talkModeEnabled) mobileSuccessSoft else mobileSurface,
),
) {
Icon(
imageVector = Icons.Default.RecordVoiceOver,
contentDescription = if (talkModeEnabled) "Turn Talk Mode off" else "Turn Talk Mode on",
modifier = Modifier.size(22.dp),
tint = if (talkModeEnabled) mobileSuccess else mobileTextSecondary,
)
}
Spacer(modifier = Modifier.height(4.dp))
Text("", style = mobileCaption2)
Text(
if (talkModeEnabled) "Talk on" else "Talk",
style = mobileCaption2,
color = if (talkModeEnabled) mobileSuccess else mobileTextTertiary,
)
}
}
@@ -299,6 +337,9 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
val queueCount = micQueuedMessages.size
val stateText =
when {
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeSpeaking -> "Talk speaking"
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeListening -> "Talk listening"
voiceCaptureMode == VoiceCaptureMode.TalkMode -> "Talk on"
queueCount > 0 -> "$queueCount queued"
micIsSending -> "Sending"
micCooldown -> "Cooldown"
@@ -307,14 +348,15 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
}
val stateColor =
when {
voiceCaptureMode == VoiceCaptureMode.TalkMode -> mobileSuccess
micEnabled -> mobileSuccess
micIsSending -> mobileAccent
else -> mobileTextSecondary
}
Surface(
shape = RoundedCornerShape(999.dp),
color = if (micEnabled) mobileSuccessSoft else mobileSurface,
border = BorderStroke(1.dp, if (micEnabled) mobileSuccess.copy(alpha = 0.3f) else mobileBorder),
color = if (micEnabled || talkModeEnabled) mobileSuccessSoft else mobileSurface,
border = BorderStroke(1.dp, if (micEnabled || talkModeEnabled) mobileSuccess.copy(alpha = 0.3f) else mobileBorder),
) {
Text(
"$gatewayStatus · $stateText",
@@ -353,6 +395,11 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
}
}
private enum class PendingVoicePermissionAction {
ManualMic,
TalkMode,
}
@Composable
private fun VoiceTurnBubble(entry: VoiceConversationEntry) {
val isUser = entry.role == VoiceConversationRole.User

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

@@ -2,6 +2,7 @@ package ai.openclaw.app
import android.app.Notification
import android.content.Intent
import android.content.pm.ServiceInfo
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
@@ -30,6 +31,35 @@ class NodeForegroundServiceTest {
assertEquals(expectedFlags, savedIntent.flags and expectedFlags)
}
@Test
fun foregroundServiceTypesForVoiceMode_addsMicrophoneOnlyForTalkMode() {
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.Off),
)
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.ManualMic),
)
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.TalkMode),
)
}
@Test
fun voiceNotificationSuffixReflectsActiveCaptureMode() {
assertEquals("", voiceNotificationSuffix(VoiceCaptureMode.Off, false, false, false, false))
assertEquals(
" · Mic: Listening",
voiceNotificationSuffix(VoiceCaptureMode.ManualMic, true, true, false, false),
)
assertEquals(
" · Talk: Speaking",
voiceNotificationSuffix(VoiceCaptureMode.TalkMode, false, false, true, true),
)
}
private fun buildNotification(service: NodeForegroundService): Notification {
val method =
NodeForegroundService::class.java.getDeclaredMethod(

View File

@@ -2,7 +2,9 @@ package ai.openclaw.app
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -22,6 +24,32 @@ class SecurePrefsTest {
assertEquals("whileUsing", plainPrefs.getString("location.enabledMode", null))
}
@Test
fun voiceMicEnabled_ignoresOldTalkEnabledKey() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().putBoolean("talk.enabled", true).commit()
val prefs = SecurePrefs(context)
assertFalse(prefs.voiceMicEnabled.value)
assertFalse(plainPrefs.contains("voice.micEnabled"))
}
@Test
fun setVoiceMicEnabled_persistsNewKeyOnly() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().putBoolean("talk.enabled", false).commit()
val prefs = SecurePrefs(context)
prefs.setVoiceMicEnabled(true)
assertTrue(prefs.voiceMicEnabled.value)
assertTrue(plainPrefs.getBoolean("voice.micEnabled", false))
assertFalse(plainPrefs.getBoolean("talk.enabled", false))
}
@Test
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
val context = RuntimeEnvironment.getApplication()

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

@@ -1,6 +1,10 @@
# OpenClaw iOS Changelog
## 2026.4.24 - 2026-04-24
## 2026.4.26 - 2026-04-26
Maintenance update for the current OpenClaw development release.
## 2026.4.25 - 2026-04-25
Maintenance update for the current OpenClaw development release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.4.24
OPENCLAW_MARKETING_VERSION = 2026.4.24
OPENCLAW_IOS_VERSION = 2026.4.26
OPENCLAW_MARKETING_VERSION = 2026.4.26
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

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,3 +1,3 @@
{
"version": "2026.4.24"
"version": "2026.4.26"
}

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

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.24</string>
<string>2026.4.26</string>
<key>CFBundleVersion</key>
<string>2026042400</string>
<string>2026042600</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

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
}
@@ -723,17 +735,26 @@ public struct AgentIdentityResult: Codable, Sendable {
public let agentid: String
public let name: String?
public let avatar: String?
public let avatarsource: String?
public let avatarstatus: String?
public let avatarreason: String?
public let emoji: String?
public init(
agentid: String,
name: String?,
avatar: String?,
avatarsource: String?,
avatarstatus: String?,
avatarreason: String?,
emoji: String?)
{
self.agentid = agentid
self.name = name
self.avatar = avatar
self.avatarsource = avatarsource
self.avatarstatus = avatarstatus
self.avatarreason = avatarreason
self.emoji = emoji
}
@@ -741,6 +762,9 @@ public struct AgentIdentityResult: Codable, Sendable {
case agentid = "agentId"
case name
case avatar
case avatarsource = "avatarSource"
case avatarstatus = "avatarStatus"
case avatarreason = "avatarReason"
case emoji
}
}

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
}
@@ -723,17 +735,26 @@ public struct AgentIdentityResult: Codable, Sendable {
public let agentid: String
public let name: String?
public let avatar: String?
public let avatarsource: String?
public let avatarstatus: String?
public let avatarreason: String?
public let emoji: String?
public init(
agentid: String,
name: String?,
avatar: String?,
avatarsource: String?,
avatarstatus: String?,
avatarreason: String?,
emoji: String?)
{
self.agentid = agentid
self.name = name
self.avatar = avatar
self.avatarsource = avatarsource
self.avatarstatus = avatarstatus
self.avatarreason = avatarreason
self.emoji = emoji
}
@@ -741,6 +762,9 @@ public struct AgentIdentityResult: Codable, Sendable {
case agentid = "agentId"
case name
case avatar
case avatarsource = "avatarSource"
case avatarstatus = "avatarStatus"
case avatarreason = "avatarReason"
case emoji
}
}

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,19 @@ services:
TERM: xterm-256color
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}
# Empty means auto: Bonjour disables itself in detected containers.
# Set 0 only on host/macvlan/mDNS-capable networks; set 1 to force off.
OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-}
# OpenTelemetry export is outbound OTLP/HTTP from the Gateway. Prometheus
# uses the existing authenticated Gateway route; it does not need a port.
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-}
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: ${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}
OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-http/protobuf}
OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-}
OTEL_SEMCONV_STABILITY_OPT_IN: ${OTEL_SEMCONV_STABILITY_OPT_IN:-}
OPENCLAW_OTEL_PRELOADED: ${OPENCLAW_OTEL_PRELOADED:-}
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 @@
f1fd4557473391980caf6d6b32f78e4de25f8504b29dfe083f7f9e325d0b204c config-baseline.json
68e0784ca0f9279d49b40ce4493e1cb2c416e1fb70a137a853a10a8c078c97ca config-baseline.core.json
d72032762ab46b99480b57deb81130a0ab5b1401189cfbaf4f7fef4a063a7f6c config-baseline.channel.json
0504c4f38d4c753fffeb465c93540d829df6b0fcef921eb0e2226ac16bdbbe07 config-baseline.plugin.json
3e6dd8292d9350b0ccc243f81f7b6e95494fc769c01c084d8d6d6e9e1f668a14 config-baseline.json
e040e5818afe66d71fc8a7ae1653f1e8c252cc5b51480ef3b4ae1269682b9ade config-baseline.core.json
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
74b74cb18ac37c0acaa765f398f1f9edbcee4c43567f02d45c89598a1e13afb4 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
56ccee3ef8ff3b0ba7e2e765ae631b59254464585d5fef9db7e905f2c4c34ded plugin-sdk-api-baseline.json
39184cf8afaec691f0352d1a113e30a7099b87c0748237a3c7307e903ba24eee plugin-sdk-api-baseline.jsonl
21914ef8c5840e0defc36d571834dc28a92d6d5ca2d42a088c33b4de681e836a plugin-sdk-api-baseline.json
3f22e6af0dad3433d25d996802d7436a3cc0e68bc86ecaf813a22e2b4e5333eb 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 first, durable-history-backed second: 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 checks persisted run logs and job state for the matching `cron:<jobId>:<startedAt>` run. If that durable history shows a terminal result, the task ledger is finalized from it; otherwise Gateway-owned maintenance can mark the task `lost`. Offline CLI audit can recover from durable history, but it does not treat its own empty in-process active-job set as proof that a Gateway-owned cron run is gone.
</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:
@@ -168,7 +173,7 @@ openclaw hooks enable <hook-name>
### session-memory details
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured.
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md` using the host local date. Requires `workspace.dir` to be configured.
<a id="bootstrap-extra-files"></a>

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.
@@ -23,49 +25,68 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
- Tasks are **records**, not schedulers — cron and heartbeat decide _when_ work runs, tasks track _what happened_.
- 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.
- Cron tasks stay live while the cron runtime still owns the job; if the
in-memory runtime state is gone, task maintenance first checks durable cron
run history before marking a task lost.
- 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 +98,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 +141,20 @@ 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.
- Cron tasks: the cron runtime no longer tracks the job as active and durable
cron run history does not show a terminal result for that run. Offline CLI
audit does not treat its own empty in-process cron runtime state as authority.
- 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 +168,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 +188,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, then recover terminal status from persisted cron run logs/job state before falling back to `lost`. Only the Gateway process is authoritative for the in-memory cron active-job set; offline CLI audit uses durable history but does not mark a cron task lost solely because that local Set is empty.
- 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 +292,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 +310,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

@@ -5,14 +5,14 @@ read_when:
- Troubleshooting webhook pairing
- Configuring iMessage on macOS
title: "BlueBubbles"
sidebarTitle: "BlueBubbles"
---
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
## Bundled plugin
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not
need a separate `openclaw plugins install` step.
<Note>
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not need a separate `openclaw plugins install` step.
</Note>
## Overview
@@ -21,111 +21,119 @@ 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.
## Quick start
1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
2. In the BlueBubbles config, enable the web API and set a password.
3. Run `openclaw onboard` and select BlueBubbles, or configure manually:
<Steps>
<Step title="Install BlueBubbles">
Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
</Step>
<Step title="Enable the web API">
In the BlueBubbles config, enable the web API and set a password.
</Step>
<Step title="Configure OpenClaw">
Run `openclaw onboard` and select BlueBubbles, or configure manually:
```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```
```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```
4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
5. Start the gateway; it will register the webhook handler and start pairing.
</Step>
<Step title="Point webhooks at the gateway">
Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
</Step>
<Step title="Start the gateway">
Start the gateway; it will register the webhook handler and start pairing.
</Step>
</Steps>
Security note:
<Warning>
**Security**
- Always set a webhook password.
- Webhook authentication is always required. OpenClaw rejects BlueBubbles webhook requests unless they include a password/guid that matches `channels.bluebubbles.password` (for example `?password=<password>` or `x-password`), regardless of loopback/proxy topology.
- Password authentication is checked before reading/parsing full webhook bodies.
</Warning>
## Keeping Messages.app alive (VM / headless setups)
Some macOS VM / always-on setups can end up with Messages.app going idle (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
Some macOS VM / always-on setups can end up with Messages.app going "idle" (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
### 1) Save the AppleScript
<Steps>
<Step title="Save the AppleScript">
Save this as `~/Scripts/poke-messages.scpt`:
Save this as:
```applescript
try
tell application "Messages"
if not running then
launch
end if
- `~/Scripts/poke-messages.scpt`
-- Touch the scripting interface to keep the process responsive.
set _chatCount to (count of chats)
end tell
on error
-- Ignore transient failures (first-run prompts, locked session, etc).
end try
```
Example script (non-interactive; does not steal focus):
</Step>
<Step title="Install a LaunchAgent">
Save this as `~/Library/LaunchAgents/com.user.poke-messages.plist`:
```applescript
try
tell application "Messages"
if not running then
launch
end if
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.poke-messages</string>
-- Touch the scripting interface to keep the process responsive.
set _chatCount to (count of chats)
end tell
on error
-- Ignore transient failures (first-run prompts, locked session, etc).
end try
```
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>/usr/bin/osascript &quot;$HOME/Scripts/poke-messages.scpt&quot;</string>
</array>
### 2) Install a LaunchAgent
<key>RunAtLoad</key>
<true/>
Save this as:
<key>StartInterval</key>
<integer>300</integer>
- `~/Library/LaunchAgents/com.user.poke-messages.plist`
<key>StandardOutPath</key>
<string>/tmp/poke-messages.log</string>
<key>StandardErrorPath</key>
<string>/tmp/poke-messages.err</string>
</dict>
</plist>
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.poke-messages</string>
This runs **every 300 seconds** and **on login**. The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>/usr/bin/osascript &quot;$HOME/Scripts/poke-messages.scpt&quot;</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>StandardOutPath</key>
<string>/tmp/poke-messages.log</string>
<key>StandardErrorPath</key>
<string>/tmp/poke-messages.err</string>
</dict>
</plist>
```
Notes:
- This runs **every 300 seconds** and **on login**.
- The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
Load it:
```bash
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
```
</Step>
<Step title="Load it">
```bash
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
```
</Step>
</Steps>
## Onboarding
@@ -137,11 +145,21 @@ openclaw onboard
The wizard prompts for:
- **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`)
- **Password** (required): API password from BlueBubbles Server settings
- **Webhook path** (optional): Defaults to `/bluebubbles-webhook`
- **DM policy**: pairing, allowlist, open, or disabled
- **Allow list**: Phone numbers, emails, or chat targets
<ParamField path="Server URL" type="string" required>
BlueBubbles server address (e.g., `http://192.168.1.100:1234`).
</ParamField>
<ParamField path="Password" type="string" required>
API password from BlueBubbles Server settings.
</ParamField>
<ParamField path="Webhook path" type="string" default="/bluebubbles-webhook">
Webhook endpoint path.
</ParamField>
<ParamField path="DM policy" type="string">
`pairing`, `allowlist`, `open`, or `disabled`.
</ParamField>
<ParamField path="Allow list" type="string[]">
Phone numbers, emails, or chat targets.
</ParamField>
You can also add BlueBubbles via CLI:
@@ -151,19 +169,20 @@ openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --passwor
## Access control (DMs + groups)
DMs:
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `openclaw pairing list bluebubbles`
- `openclaw pairing approve bluebubbles <CODE>`
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
Groups:
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
<Tabs>
<Tab title="DMs">
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `openclaw pairing list bluebubbles`
- `openclaw pairing approve bluebubbles <CODE>`
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
</Tab>
<Tab title="Groups">
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
</Tab>
</Tabs>
### Contact name enrichment (macOS, optional)
@@ -359,21 +378,23 @@ BlueBubbles supports advanced message actions when enabled in config:
}
```
Available actions:
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`). iMessage's native tapback set is `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. When an agent picks an emoji outside that set (for example `👀`), the reaction tool falls back to `love` so the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values.
- **edit**: Edit a sent message (`messageId`, `text`)
- **unsend**: Unsend a message (`messageId`)
- **reply**: Reply to a specific message (`messageId`, `text`, `to`)
- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`)
- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`)
- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
- **addParticipant**: Add someone to a group (`chatGuid`, `address`)
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
- **leaveGroup**: Leave a group chat (`chatGuid`)
- **upload-file**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
- Legacy alias: `sendAttachment` still works, but `upload-file` is the canonical action name.
<AccordionGroup>
<Accordion title="Available actions">
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`). iMessage's native tapback set is `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. When an agent picks an emoji outside that set (for example `👀`), the reaction tool falls back to `love` so the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values.
- **edit**: Edit a sent message (`messageId`, `text`).
- **unsend**: Unsend a message (`messageId`).
- **reply**: Reply to a specific message (`messageId`, `text`, `to`).
- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`).
- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`).
- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
- **addParticipant**: Add someone to a group (`chatGuid`, `address`).
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`).
- **leaveGroup**: Leave a group chat (`chatGuid`).
- **upload-file**: Send media/files (`to`, `buffer`, `filename`, `asVoice`).
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
- Legacy alias: `sendAttachment` still works, but `upload-file` is the canonical action name.
</Accordion>
</AccordionGroup>
### Message IDs (short vs full)
@@ -404,54 +425,56 @@ The two webhooks arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coa
`channels.bluebubbles.coalesceSameSenderDms` opts a DM into merging consecutive same-sender webhooks into a single agent turn. Group chats continue to key per-message so multi-user turn structure is preserved.
### When to enable
<Tabs>
<Tab title="When to enable">
Enable when:
Enable when:
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
Leave disabled when:
Leave disabled when:
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
### Enabling
```json5
{
channels: {
bluebubbles: {
coalesceSameSenderDms: true, // opt in (default: false)
},
},
}
```
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required — Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
// or under memory pressure (observed gap can stretch past 2 s then).
bluebubbles: 2500,
</Tab>
<Tab title="Enabling">
```json5
{
channels: {
bluebubbles: {
coalesceSameSenderDms: true, // opt in (default: false)
},
},
},
},
}
```
}
```
### Trade-offs
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required — Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
- **Added latency for DM control commands.** With the flag on, DM control-command messages (like `Dump`, `Save`, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch.
- **Merged output is bounded** — merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source `messageId` still reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate.
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
// or under memory pressure (observed gap can stretch past 2 s then).
bluebubbles: 2500,
},
},
},
}
```
</Tab>
<Tab title="Trade-offs">
- **Added latency for DM control commands.** With the flag on, DM control-command messages (like `Dump`, `Save`, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch.
- **Merged output is bounded** — merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source `messageId` still reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate.
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
</Tab>
</Tabs>
### Scenarios and what the agent sees
@@ -468,27 +491,35 @@ To tune the window yourself:
If the flag is on and split-sends still arrive as two turns, check each layer:
1. **Config actually loaded.**
<AccordionGroup>
<Accordion title="Config actually loaded">
```
grep coalesceSameSenderDms ~/.openclaw/openclaw.json
```
```
grep coalesceSameSenderDms ~/.openclaw/openclaw.json
```
Then `openclaw gateway restart` — the flag is read at debouncer-registry creation.
Then `openclaw gateway restart` — the flag is read at debouncer-registry creation.
</Accordion>
<Accordion title="Debounce window wide enough for your setup">
Look at the BlueBubbles server log under `~/Library/Logs/bluebubbles-server/main.log`:
2. **Debounce window wide enough for your setup.** Look at the BlueBubbles server log under `~/Library/Logs/bluebubbles-server/main.log`:
```
grep -E "Dispatching event to webhook" main.log | tail -20
```
```
grep -E "Dispatching event to webhook" main.log | tail -20
```
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
3. **Session JSONL timestamps ≠ webhook arrival.** Session event timestamps (`~/.openclaw/agents/<id>/sessions/*.jsonl`) reflect when the gateway hands a message to the agent, **not** when the webhook arrived. A queued-second message tagged `[Queued messages while agent was busy]` means the first turn was still running when the second webhook arrived — the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log.
4. **Memory pressure slowing reply dispatch.** On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check `memory_pressure` and `ps -o rss -p $(pgrep openclaw-gateway)`; if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host.
5. **Reply-quote sends are a different path.** If the user tapped `Dump` as a **reply** to an existing URL-balloon (iMessage shows a "1 Reply" badge on the Dump bubble), the URL lives in `replyToBody`, not in a second webhook. Coalescing does not apply — that's a skill/prompt concern, not a debouncer concern.
</Accordion>
<Accordion title="Session JSONL timestamps ≠ webhook arrival">
Session event timestamps (`~/.openclaw/agents/<id>/sessions/*.jsonl`) reflect when the gateway hands a message to the agent, **not** when the webhook arrived. A queued-second message tagged `[Queued messages while agent was busy]` means the first turn was still running when the second webhook arrived — the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log.
</Accordion>
<Accordion title="Memory pressure slowing reply dispatch">
On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check `memory_pressure` and `ps -o rss -p $(pgrep openclaw-gateway)`; if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host.
</Accordion>
<Accordion title="Reply-quote sends are a different path">
If the user tapped `Dump` as a **reply** to an existing URL-balloon (iMessage shows a "1 Reply" badge on the Dump bubble), the URL lives in `replyToBody`, not in a second webhook. Coalescing does not apply — that's a skill/prompt concern, not a debouncer concern.
</Accordion>
</AccordionGroup>
## Block streaming
@@ -514,30 +545,40 @@ Control whether responses are sent as a single message or streamed in blocks:
Full configuration: [Configuration](/gateway/configuration)
Provider options:
- `channels.bluebubbles.enabled`: Enable/disable the channel.
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
- `channels.bluebubbles.password`: API password.
- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
- `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`.
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
- `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8).
- `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`.
- `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`.
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
- `channels.bluebubbles.actions`: Enable/disable specific actions.
- `channels.bluebubbles.accounts`: Multi-account configuration.
<AccordionGroup>
<Accordion title="Connection and webhook">
- `channels.bluebubbles.enabled`: Enable/disable the channel.
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
- `channels.bluebubbles.password`: API password.
- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
</Accordion>
<Accordion title="Access policy">
- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
</Accordion>
<Accordion title="Delivery and chunking">
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
- `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`.
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
</Accordion>
<Accordion title="Media and history">
- `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8).
- `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`.
- `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`.
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
</Accordion>
<Accordion title="Actions and accounts">
- `channels.bluebubbles.actions`: Enable/disable specific actions.
- `channels.bluebubbles.accounts`: Multi-account configuration.
</Accordion>
</AccordionGroup>
Related global options:
@@ -580,8 +621,8 @@ For general channel workflow reference, see [Channels](/channels) and the [Plugi
## Related
- [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
- [Channels Overview](/channels) — all supported channels
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Security](/gateway/security) — access model and hardening

View File

@@ -5,10 +5,12 @@ read_when:
- Debugging multi-agent replies in WhatsApp
status: experimental
title: "Broadcast groups"
sidebarTitle: "Broadcast groups"
---
**Status:** Experimental
**Version:** Added in 2026.1.9
<Note>
**Status:** Experimental. Added in 2026.1.9.
</Note>
## Overview
@@ -18,55 +20,55 @@ Current scope: **WhatsApp only** (web channel).
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings).
## Use Cases
## Use cases
### 1. Specialized Agent Teams
<AccordionGroup>
<Accordion title="1. Specialized agent teams">
Deploy multiple agents with atomic, focused responsibilities:
Deploy multiple agents with atomic, focused responsibilities:
```
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
```
```
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
```
Each agent processes the same message and provides its specialized perspective.
Each agent processes the same message and provides its specialized perspective.
### 2. Multi-Language Support
```
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
```
### 3. Quality Assurance Workflows
```
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
```
### 4. Task Automation
```
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
```
</Accordion>
<Accordion title="2. Multi-language support">
```
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
```
</Accordion>
<Accordion title="3. Quality assurance workflows">
```
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
```
</Accordion>
<Accordion title="4. Task automation">
```
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
```
</Accordion>
</AccordionGroup>
## Configuration
### Basic Setup
### Basic setup
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids:
@@ -83,37 +85,40 @@ Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer
**Result:** When OpenClaw would reply in this chat, it will run all three agents.
### Processing Strategy
### Processing strategy
Control how agents process messages:
#### Parallel (Default)
<Tabs>
<Tab title="parallel (default)">
All agents process simultaneously:
All agents process simultaneously:
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
</Tab>
<Tab title="sequential">
Agents process in order (one waits for previous to finish):
#### Sequential
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
Agents process in order (one waits for previous to finish):
</Tab>
</Tabs>
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
### Complete Example
### Complete example
```json
{
@@ -148,22 +153,32 @@ Agents process in order (one waits for previous to finish):
}
```
## How It Works
## How it works
### Message Flow
### Message flow
1. **Incoming message** arrives in a WhatsApp group
2. **Broadcast check**: System checks if peer ID is in `broadcast`
3. **If in broadcast list**:
- All listed agents process the message
- Each agent has its own session key and isolated context
- Agents process in parallel (default) or sequentially
4. **If not in broadcast list**:
- Normal routing applies (first matching binding)
<Steps>
<Step title="Incoming message arrives">
A WhatsApp group or DM message arrives.
</Step>
<Step title="Broadcast check">
System checks if peer ID is in `broadcast`.
</Step>
<Step title="If in broadcast list">
- All listed agents process the message.
- Each agent has its own session key and isolated context.
- Agents process in parallel (default) or sequentially.
</Step>
<Step title="If not in broadcast list">
Normal routing applies (first matching binding).
</Step>
</Steps>
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change _which agents run_ when a message is eligible for processing.
<Note>
Broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change _which agents run_ when a message is eligible for processing.
</Note>
### Session Isolation
### Session isolation
Each agent in a broadcast group maintains completely separate:
@@ -181,92 +196,95 @@ This allows each agent to have:
- Different models (e.g., opus vs. sonnet)
- Different skills installed
### Example: Isolated Sessions
### Example: isolated sessions
In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
**Alfred's context:**
<Tabs>
<Tab title="Alfred's context">
```
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/user/openclaw-alfred/
Tools: read, write, exec
```
</Tab>
<Tab title="Bärbel's context">
```
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/user/openclaw-baerbel/
Tools: read only
```
</Tab>
</Tabs>
```
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/user/openclaw-alfred/
Tools: read, write, exec
```
## Best practices
**Bärbel's context:**
<AccordionGroup>
<Accordion title="1. Keep agents focused">
Design each agent with a single, clear responsibility:
```
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/user/openclaw-baerbel/
Tools: read only
```
## Best Practices
### 1. Keep Agents Focused
Design each agent with a single, clear responsibility:
```json
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
```
**Good:** Each agent has one job
**Bad:** One generic "dev-helper" agent
### 2. Use Descriptive Names
Make it clear what each agent does:
```json
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
```
### 3. Configure Different Tool Access
Give agents only the tools they need:
```json
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
```json
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
}
}
```
```
### 4. Monitor Performance
✅ **Good:** Each agent has one job. ❌ **Bad:** One generic "dev-helper" agent.
With many agents, consider:
</Accordion>
<Accordion title="2. Use descriptive names">
Make it clear what each agent does:
- Using `"strategy": "parallel"` (default) for speed
- Limiting broadcast groups to 5-10 agents
- Using faster models for simpler agents
```json
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
```
### 5. Handle Failures Gracefully
</Accordion>
<Accordion title="3. Configure different tool access">
Give agents only the tools they need:
Agents fail independently. One agent's error doesn't block others:
```json
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
}
}
}
```
```
Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error
```
</Accordion>
<Accordion title="4. Monitor performance">
With many agents, consider:
- Using `"strategy": "parallel"` (default) for speed
- Limiting broadcast groups to 5-10 agents
- Using faster models for simpler agents
</Accordion>
<Accordion title="5. Handle failures gracefully">
Agents fail independently. One agent's error doesn't block others:
```
Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error
```
</Accordion>
</AccordionGroup>
## Compatibility
@@ -297,108 +315,116 @@ Broadcast groups work alongside existing routing:
}
```
- `GROUP_A`: Only alfred responds (normal routing)
- `GROUP_B`: agent1 AND agent2 respond (broadcast)
- `GROUP_A`: Only alfred responds (normal routing).
- `GROUP_B`: agent1 AND agent2 respond (broadcast).
<Note>
**Precedence:** `broadcast` takes priority over `bindings`.
</Note>
## Troubleshooting
### Agents Not Responding
<AccordionGroup>
<Accordion title="Agents not responding">
**Check:**
**Check:**
1. Agent IDs exist in `agents.list`.
2. Peer ID format is correct (e.g., `120363403215116621@g.us`).
3. Agents are not in deny lists.
1. Agent IDs exist in `agents.list`
2. Peer ID format is correct (e.g., `120363403215116621@g.us`)
3. Agents are not in deny lists
**Debug:**
**Debug:**
```bash
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
```
```bash
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
```
</Accordion>
<Accordion title="Only one agent responding">
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
### Only One Agent Responding
**Fix:** Add to broadcast config or remove from bindings.
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
</Accordion>
<Accordion title="Performance issues">
If slow with many agents:
**Fix:** Add to broadcast config or remove from bindings.
- Reduce number of agents per group.
- Use lighter models (sonnet instead of opus).
- Check sandbox startup time.
### Performance Issues
**If slow with many agents:**
- Reduce number of agents per group
- Use lighter models (sonnet instead of opus)
- Check sandbox startup time
</Accordion>
</AccordionGroup>
## Examples
### Example 1: Code Review Team
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
"agents": {
"list": [
{
"id": "code-formatter",
"workspace": "~/agents/formatter",
"tools": { "allow": ["read", "write"] }
<AccordionGroup>
<Accordion title="Example 1: Code review team">
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
{
"id": "security-scanner",
"workspace": "~/agents/security",
"tools": { "allow": ["read", "exec"] }
"agents": {
"list": [
{
"id": "code-formatter",
"workspace": "~/agents/formatter",
"tools": { "allow": ["read", "write"] }
},
{
"id": "security-scanner",
"workspace": "~/agents/security",
"tools": { "allow": ["read", "exec"] }
},
{
"id": "test-coverage",
"workspace": "~/agents/testing",
"tools": { "allow": ["read", "exec"] }
},
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
```
**User sends:** Code snippet.
**Responses:**
- code-formatter: "Fixed indentation and added type hints"
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
- test-coverage: "Coverage is 45%, missing tests for error cases"
- docs-checker: "Missing docstring for function `process_data`"
</Accordion>
<Accordion title="Example 2: Multi-language support">
```json
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
{
"id": "test-coverage",
"workspace": "~/agents/testing",
"tools": { "allow": ["read", "exec"] }
},
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
```
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
```
</Accordion>
</AccordionGroup>
**User sends:** Code snippet
**Responses:**
## API reference
- code-formatter: "Fixed indentation and added type hints"
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
- test-coverage: "Coverage is 45%, missing tests for error cases"
- docs-checker: "Missing docstring for function `process_data`"
### Example 2: Multi-Language Support
```json
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
```
## API Reference
### Config Schema
### Config schema
```typescript
interface OpenClawConfig {
@@ -411,20 +437,21 @@ interface OpenClawConfig {
### Fields
- `strategy` (optional): How to process agents
- `"parallel"` (default): All agents process simultaneously
- `"sequential"`: Agents process in array order
- `[peerId]`: WhatsApp group JID, E.164 number, or other peer ID
- Value: Array of agent IDs that should process messages
<ParamField path="strategy" type='"parallel" | "sequential"' default='"parallel"'>
How to process agents. `parallel` runs all agents simultaneously; `sequential` runs them in array order.
</ParamField>
<ParamField path="[peerId]" type="string[]">
WhatsApp group JID, E.164 number, or other peer ID. Value is the array of agent IDs that should process messages.
</ParamField>
## Limitations
1. **Max agents:** No hard limit, but 10+ agents may be slow
2. **Shared context:** Agents don't see each other's responses (by design)
3. **Message ordering:** Parallel responses may arrive in any order
4. **Rate limits:** All agents count toward WhatsApp rate limits
1. **Max agents:** No hard limit, but 10+ agents may be slow.
2. **Shared context:** Agents don't see each other's responses (by design).
3. **Message ordering:** Parallel responses may arrive in any order.
4. **Rate limits:** All agents count toward WhatsApp rate limits.
## Future Enhancements
## Future enhancements
Planned features:
@@ -435,8 +462,8 @@ Planned features:
## Related
- [Groups](/channels/groups)
- [Channel routing](/channels/channel-routing)
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Multi-agent sandbox tools](/tools/multi-agent-sandbox-tools)
- [Pairing](/channels/pairing)
- [Session management](/concepts/session)

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`).
@@ -961,14 +965,23 @@ Discord has two distinct voice surfaces: realtime **voice channels** (continuous
### Voice channels
Requirements:
Setup checklist:
- Enable native commands (`commands.native` or `channels.discord.commands.native`).
- Configure `channels.discord.voice`.
- The bot needs Connect + Speak permissions in the target voice channel.
1. Enable Message Content Intent in the Discord Developer Portal.
2. Enable Server Members Intent when role/user allowlists are used.
3. Invite the bot with `bot` and `applications.commands` scopes.
4. Grant Connect, Speak, Send Messages, and Read Message History in the target voice channel.
5. Enable native commands (`commands.native` or `channels.discord.commands.native`).
6. Configure `channels.discord.voice`.
Use `/vc join|leave|status` to control sessions. The command uses the account default agent and follows the same allowlist and group policy rules as other Discord commands.
```bash
/vc join channel:<voice-channel-id>
/vc status
/vc leave
```
Auto-join example:
```json5
@@ -977,6 +990,7 @@ Auto-join example:
discord: {
voice: {
enabled: true,
model: "openai/gpt-5.4-mini",
autoJoin: [
{
guildId: "123456789012345678",
@@ -987,7 +1001,7 @@ Auto-join example:
decryptionFailureTolerance: 24,
tts: {
provider: "openai",
openai: { voice: "alloy" },
openai: { voice: "onyx" },
},
},
},
@@ -998,12 +1012,24 @@ Auto-join example:
Notes:
- `voice.tts` overrides `messages.tts` for voice playback only.
- `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model.
- STT uses `tools.media.audio`; `voice.model` does not affect transcription.
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
- Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable it.
- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options.
- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)`, this may be the upstream `@discordjs/voice` receive bug tracked in [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419).
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419.
Voice channel pipeline:
- Discord PCM capture is converted to a WAV temp file.
- `tools.media.audio` handles STT, for example `openai/gpt-4o-mini-transcribe`.
- The transcript is sent through normal Discord ingress and routing.
- `voice.model`, when set, overrides only the response LLM for this voice-channel turn.
- `voice.tts` is merged over `messages.tts`; the resulting audio is played in the joined channel.
Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, and TTS auth for `messages.tts`/`voice.tts`.
### Voice messages
@@ -1130,7 +1156,7 @@ openclaw logs --follow
- watch logs for:
- `discord voice: DAVE decrypt failures detected`
- `discord voice: repeated decrypt failures; attempting rejoin`
- if failures continue after automatic rejoin, collect logs and compare against [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419)
- if failures continue after automatic rejoin, collect logs and compare against the upstream DAVE receive history in [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419) and [discord.js #11449](https://github.com/discordjs/discord.js/pull/11449)
</Accordion>
</AccordionGroup>

View File

@@ -16,7 +16,7 @@ Feishu/Lark is an all-in-one collaboration platform where teams chat, share docu
## Quick start
> **Requires OpenClaw 2026.4.24 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`.
> **Requires OpenClaw 2026.4.25 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`.
<Steps>
<Step title="Run the channel setup wizard">
@@ -213,6 +213,11 @@ openclaw pairing list feishu
appId: "cli_xxx",
appSecret: "xxx",
name: "Primary bot",
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
backup: {
appId: "cli_yyy",
@@ -227,6 +232,10 @@ openclaw pairing list feishu
```
`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`.
`accounts.<id>.tts` uses the same shape as `messages.tts` and deep-merges over
global TTS config, so multi-bot Feishu setups can keep shared provider
credentials globally while overriding only voice, model, persona, or auto mode
per account.
### Message limits
@@ -386,6 +395,7 @@ Full configuration: [Gateway configuration](/gateway/configuration)
| `channels.feishu.accounts.<id>.appId` | App ID | — |
| `channels.feishu.accounts.<id>.appSecret` | App Secret | — |
| `channels.feishu.accounts.<id>.domain` | Per-account domain override | `feishu` |
| `channels.feishu.accounts.<id>.tts` | Per-account TTS override | `messages.tts` |
| `channels.feishu.dmPolicy` | DM policy | `allowlist` |
| `channels.feishu.allowFrom` | DM allowlist (open_id list) | [BotOwnerId] |
| `channels.feishu.groupPolicy` | Group policy | `allowlist` |
@@ -414,6 +424,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
@@ -424,6 +443,14 @@ Full configuration: [Gateway configuration](/gateway/configuration)
- ✅ Interactive cards (including streaming updates)
- ⚠️ Rich text (post-style formatting; doesn't support full Feishu/Lark authoring capabilities)
Native Feishu/Lark audio bubbles use the Feishu `audio` message type and require
Ogg/Opus upload media (`file_type: "opus"`). Existing `.opus` and `.ogg` media
is sent directly as native audio. MP3/WAV/M4A and other likely audio formats are
transcoded to 48kHz Ogg/Opus with `ffmpeg` only when the reply requests voice
delivery (`audioAsVoice` / message tool `asVoice`, including TTS voice-note
replies). Ordinary MP3 attachments stay regular files. If `ffmpeg` is missing or
conversion fails, OpenClaw falls back to a file attachment and logs the reason.
### Threads and replies
- ✅ Inline replies

View File

@@ -3,14 +3,14 @@ summary: "Group chat behavior across surfaces (Discord/iMessage/Matrix/Microsoft
read_when:
- Changing group chat behavior or mention gating
title: "Groups"
sidebarTitle: "Groups"
---
OpenClaw treats group chats consistently across surfaces: Discord, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo.
## Beginner intro (2 minutes)
OpenClaw lives on your own messaging accounts. There is no separate WhatsApp bot user.
If **you** are in a group, OpenClaw can see that group and respond there.
OpenClaw "lives" on your own messaging accounts. There is no separate WhatsApp bot user. If **you** are in a group, OpenClaw can see that group and respond there.
Default behavior:
@@ -19,11 +19,13 @@ Default behavior:
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
> TL;DR
>
> - **DM access** is controlled by `*.allowFrom`.
> - **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`).
> - **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`).
<Note>
**TL;DR**
- **DM access** is controlled by `*.allowFrom`.
- **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`).
- **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`).
</Note>
Quick flow (what happens to a group message):
@@ -43,18 +45,20 @@ Two different controls are involved in group safety:
By default, OpenClaw prioritizes normal chat behavior and keeps context mostly as received. This means allowlists primarily decide who can trigger actions, not a universal redaction boundary for every quoted or historical snippet.
Current behavior is channel-specific:
<AccordionGroup>
<Accordion title="Current behavior is channel-specific">
- Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups).
- Other channels still pass quote/reply/forward context through as received.
</Accordion>
<Accordion title="Hardening direction (planned)">
- `contextVisibility: "all"` (default) keeps current as-received behavior.
- `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders.
- `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception.
- Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups).
- Other channels still pass quote/reply/forward context through as received.
Until this hardening model is implemented consistently across channels, expect differences by surface.
Hardening direction (planned):
- `contextVisibility: "all"` (default) keeps current as-received behavior.
- `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders.
- `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception.
Until this hardening model is implemented consistently across channels, expect differences by surface.
</Accordion>
</AccordionGroup>
![Group message flow](/images/groups-flow.svg)
@@ -78,63 +82,69 @@ If you want...
## Pattern: personal DMs + public groups (single agent)
Yes — this works well if your personal traffic is **DMs** and your public traffic is **groups**.
Yes — this works well if your "personal" traffic is **DMs** and your "public" traffic is **groups**.
Why: in single-agent mode, DMs typically land in the **main** session key (`agent:main:main`), while groups always use **non-main** session keys (`agent:main:<channel>:group:<id>`). If you enable sandboxing with `mode: "non-main"`, those group sessions run in the configured sandbox backend while your main DM session stays on-host. Docker is the default backend if you do not choose one.
This gives you one agent brain (shared workspace + memory), but two execution postures:
This gives you one agent "brain" (shared workspace + memory), but two execution postures:
- **DMs**: full tools (host)
- **Groups**: sandbox + restricted tools
> If you need truly separate workspaces/personas (“personal” and “public” must never mix), use a second agent + bindings. See [Multi-Agent Routing](/concepts/multi-agent).
<Note>
If you need truly separate workspaces/personas ("personal" and "public" must never mix), use a second agent + bindings. See [Multi-Agent Routing](/concepts/multi-agent).
</Note>
Example (DMs on host, groups sandboxed + messaging-only tools):
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none",
},
},
},
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
```
Want “groups can only see folder X” instead of “no host access”? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"/home/user/FriendsShared:/data:ro",
],
<Tabs>
<Tab title="DMs on host, groups sandboxed">
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none",
},
},
},
},
},
}
```
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
```
</Tab>
<Tab title="Groups see only an allowlisted folder">
Want "groups can only see folder X" instead of "no host access"? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"/home/user/FriendsShared:/data:ro",
],
},
},
},
},
}
```
</Tab>
</Tabs>
Related:
@@ -202,33 +212,40 @@ Control how group/room messages are handled per channel:
| `"disabled"` | Block all group messages entirely. |
| `"allowlist"` | Only allow groups/rooms that match the configured allowlist. |
Notes:
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists.
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Prefer room IDs or aliases; joined-room name lookup is best-effort, and unresolved names are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
<AccordionGroup>
<Accordion title="Per-channel notes">
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists.
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Prefer room IDs or aliases; joined-room name lookup is best-effort, and unresolved names are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
</Accordion>
</AccordionGroup>
Quick mental model (evaluation order for group messages):
1. `groupPolicy` (open/disabled/allowlist)
2. group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist)
3. mention gating (`requireMention`, `/activation`)
<Steps>
<Step title="groupPolicy">
`groupPolicy` (open/disabled/allowlist).
</Step>
<Step title="Group allowlists">
Group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist).
</Step>
<Step title="Mention gating">
Mention gating (`requireMention`, `/activation`).
</Step>
</Steps>
## Mention gating (default)
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
Replying to a bot message counts as an implicit mention when the channel
supports reply metadata. Quoting a bot message can also count as an implicit
mention on channels that expose quote metadata. Current built-in cases include
Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
Replying to a bot message counts as an implicit mention when the channel supports reply metadata. Quoting a bot message can also count as an implicit mention on channels that expose quote metadata. Current built-in cases include Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
```json5
{
@@ -266,31 +283,41 @@ Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
}
```
Notes:
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
- 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).
- 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.
<AccordionGroup>
<Accordion title="Mention gating notes">
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
- 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.
</Accordion>
</AccordionGroup>
## Group/channel tool restrictions (optional)
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
- `tools`: allow/deny tools for the whole group.
- `toolsBySender`: per-sender overrides within the group.
Use explicit key prefixes:
`id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard.
Legacy unprefixed keys are still accepted and matched as `id:` only.
- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard. Legacy unprefixed keys are still accepted and matched as `id:` only.
Resolution order (most specific wins):
1. group/channel `toolsBySender` match
2. group/channel `tools`
3. default (`"*"`) `toolsBySender` match
4. default (`"*"`) `tools`
<Steps>
<Step title="Group toolsBySender">
Group/channel `toolsBySender` match.
</Step>
<Step title="Group tools">
Group/channel `tools`.
</Step>
<Step title="Default toolsBySender">
Default (`"*"`) `toolsBySender` match.
</Step>
<Step title="Default tools">
Default (`"*"`) `tools`.
</Step>
</Steps>
Example (Telegram):
@@ -312,68 +339,67 @@ Example (Telegram):
}
```
Notes:
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, Microsoft Teams `teams.*.channels.*`).
<Note>
Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins). Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, Microsoft Teams `teams.*.channels.*`).
</Note>
## Group allowlists
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
Common confusion: DM pairing approval is not the same as group authorization.
For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as `groupAllowFrom` or the documented config fallback for that channel.
<Warning>
Common confusion: DM pairing approval is not the same as group authorization. For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as `groupAllowFrom` or the documented config fallback for that channel.
</Warning>
Common intents (copy/paste):
1. Disable all group replies
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
2. Allow only specific groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
<Tabs>
<Tab title="Disable all group replies">
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
</Tab>
<Tab title="Allow only specific groups (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
},
},
},
},
},
}
```
3. Allow all groups but require mention (explicit)
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
```
4. Only the owner can trigger in groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
```
}
```
</Tab>
<Tab title="Allow all groups but require mention">
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
<Tab title="Owner-only triggers (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
</Tabs>
## Activation (owner-only)
@@ -382,7 +408,7 @@ Group owners can toggle per-group activation:
- `/activation mention`
- `/activation always`
Owner is determined by `channels.whatsapp.allowFrom` (or the bots self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
Owner is determined by `channels.whatsapp.allowFrom` (or the bot's self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
## Context fields
@@ -394,7 +420,7 @@ Group inbound payloads set:
- `WasMentioned` (mention gating result)
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
Channel specific notes:
Channel-specific notes:
- BlueBubbles can optionally enrich unnamed macOS group participants from the local Contacts database before populating `GroupMembers`. This is off by default and only runs after normal group gating passes.
@@ -416,7 +442,7 @@ See [Group messages](/channels/group-messages) for WhatsApp-only behavior (histo
## Related
- [Group messages](/channels/group-messages)
- [Broadcast groups](/channels/broadcast-groups)
- [Channel routing](/channels/channel-routing)
- [Group messages](/channels/group-messages)
- [Pairing](/channels/pairing)

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