Compare commits

..

693 Commits

Author SHA1 Message Date
Peter Steinberger
8982be10ec fix(lint): satisfy shell inline option checks 2026-05-08 06:04:48 +01:00
Peter Steinberger
5ed1cfc15c docs: keep qa broker notes internal 2026-05-08 06:01:23 +01:00
Peter Steinberger
c659590d22 fix: restore external file write helper 2026-05-08 06:01:23 +01:00
sallyom
397cf2b9ff fix: clarify gateway version mismatch warnings 2026-05-08 00:59:42 -04:00
Peter Steinberger
9e58cc82c8 test: fix strict CI gates 2026-05-08 05:56:38 +01:00
Val Alexander
79e3142122 fix(control-ui): clarify login failure guidance
Summary:
- Replace raw Control UI login failures with structured remediation guidance.
- Classify auth, pairing, insecure HTTP, origin, protocol mismatch, and transport failures without changing Gateway protocol/auth contracts.
- Localize the new login failure copy across shipped Control UI locale bundles and add regression coverage.

Verification:
- pnpm ui:i18n:sync
- pnpm ui:i18n:check
- pnpm exec vitest run --config test/vitest/vitest.ui.config.ts ui/src/i18n/test/translate.test.ts
- pnpm test ui/src/ui/views/login-gate.test.ts ui/src/ui/views/overview.node.test.ts ui/src/ui/app-gateway.node.test.ts
- pnpm tsgo:test:ui
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md ui/src/i18n/locales/*.ts ui/src/i18n/test/translate.test.ts ui/src/styles/components.css ui/src/ui/views/login-gate.ts ui/src/ui/views/login-gate.test.ts
- git diff --check origin/main..HEAD
- Testbox: pnpm check:changed, https://github.com/openclaw/openclaw/actions/runs/25536382431

Notes:
- Current broad CI has unrelated failures in files outside this PR diff; the PR-specific changed gate and touched UI/i18n checks passed.
- Closes none.
2026-05-07 23:52:48 -05:00
Pavan Kumar Gondhi
fc065b2693 Harden macOS shell wrapper allowlist parsing [AI] (#78518)
* fix: harden shell wrapper allowlist parsing

* fix: harden shell wrapper approval binding

* docs: add changelog entry for PR merge

---------

Co-authored-by: Ishaan <ishaan@Ishaans-Mac-mini.local>
2026-05-08 10:18:41 +05:30
Peter Steinberger
eabae023eb perf: lazy load memory embedding runtime 2026-05-08 05:39:13 +01:00
Peter Steinberger
8dcc2ff1d2 fix(discord): prefer latest voice auto-join channel 2026-05-08 05:35:51 +01:00
Peter Steinberger
1f88cb2ce5 fix(gateway): persist macOS stop disable after bootout
Summary:
- carry forward #78412's macOS LaunchAgent bootout-by-default stop behavior and repair guard
- fix the remaining `gateway stop --disable` tail when the service is already not loaded after bootout
- add lifecycle regressions, docs, and changelog

Verification:
- pnpm install
- pnpm test src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/lifecycle.test.ts src/daemon/launchd.test.ts
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/cli/daemon-cli/lifecycle-core.ts src/cli/daemon-cli/lifecycle.ts src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/lifecycle.test.ts docs/cli/gateway.md docs/gateway/index.md src/daemon/launchd.ts src/daemon/launchd.test.ts src/cli/daemon-cli/register-service-commands.ts src/cli/daemon-cli/types.ts src/daemon/service-types.ts
- git diff --check origin/main...HEAD
- pnpm build
- Parallels macOS Tahoe VM reproduce/fix proof in PR body
- PR checks green: Real behavior proof, auto-response, dispatch, label, label-issues

Co-authored-by: wdeveloper16 <25180374+wdeveloper16@users.noreply.github.com>
2026-05-08 05:35:21 +01:00
Peter Steinberger
fe79d85ae0 feat(imessage): add native imsg message actions
Adds native iMessage private-API message actions, lightweight message-tool discovery, bridge capability cache sharing, execution-time action gates, target alias coverage, and regression tests.
2026-05-08 05:34:22 +01:00
samzong
1819e41d26 fix(gateway): preserve node reconnect state (#78351)
Preserve node registry ownership across same-node WebSocket reconnect races so stale old-socket closes cannot clear the replacement session or complete the wrong pending invoke.

Thanks @samzong.
2026-05-08 12:32:18 +08:00
Peter Steinberger
9ef37d1907 test: tighten assertions and harness coverage 2026-05-08 05:28:12 +01:00
Kevin Lin
f62618f805 fix: respect Codex requirements for app-server defaults (#79151)
* fix(codex): honor requirements for app-server defaults

* test(codex): harden requirements policy coverage

* fix(codex): match requirements sandbox constraints

* fix(codex): honor approval requirements in defaults

* fix(codex): honor reviewer requirements in defaults

* fix(codex): honor remote sandbox requirements
2026-05-07 21:16:08 -07:00
NVIDIAN
36f847a60e fix(whatsapp): ignore outbound echoes for inbound activity (#79057)
Merged via squash.

Prepared head SHA: 3b1f38a2bd
Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-05-08 01:02:19 -03:00
Val Alexander
30214a40cb fix(ui): read exec policy from tools config (#79119) thanks @BunsDev
Co-authored-by: Nova <nova@openclaw.local>
2026-05-07 22:58:47 -05:00
Alex Knight
b1eedb2fc8 Add ACP session load event ledger (#79093)
* Add ACP session load event ledger

* Record ACP prompts after send acceptance

* Support ACP ledger replay by session key

* Harden ACP ledger replay completeness

* Harden ACP ledger review gaps

* Fix ACP canonical session key handling

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-08 13:54:08 +10:00
Ayaan Zaidi
252456e2f6 fix(telegram): recover sticky fallback transport 2026-05-08 09:15:31 +05:30
github-actions[bot]
4aaf934c88 chore(ui): refresh fa control ui locale 2026-05-08 03:44:43 +00:00
github-actions[bot]
114d109df2 chore(ui): refresh nl control ui locale 2026-05-08 03:44:23 +00:00
github-actions[bot]
3b6886bdd5 chore(ui): refresh vi control ui locale 2026-05-08 03:44:20 +00:00
github-actions[bot]
034122bdc6 chore(ui): refresh th control ui locale 2026-05-08 03:43:44 +00:00
github-actions[bot]
071c1c0bfe chore(ui): refresh pl control ui locale 2026-05-08 03:43:23 +00:00
github-actions[bot]
0af2441c31 chore(ui): refresh id control ui locale 2026-05-08 03:43:15 +00:00
github-actions[bot]
881e8cfff3 chore(ui): refresh uk control ui locale 2026-05-08 03:43:10 +00:00
github-actions[bot]
d8a5ef1702 chore(ui): refresh it control ui locale 2026-05-08 03:42:33 +00:00
github-actions[bot]
4e848ada7d chore(ui): refresh ar control ui locale 2026-05-08 03:42:21 +00:00
github-actions[bot]
6ce9680932 chore(ui): refresh tr control ui locale 2026-05-08 03:42:17 +00:00
github-actions[bot]
64cc58c8c8 chore(ui): refresh fr control ui locale 2026-05-08 03:41:55 +00:00
github-actions[bot]
89a0e75772 chore(ui): refresh ko control ui locale 2026-05-08 03:41:22 +00:00
github-actions[bot]
1b3bbed785 chore(ui): refresh ja-JP control ui locale 2026-05-08 03:41:20 +00:00
github-actions[bot]
7b2255ecb7 chore(ui): refresh es control ui locale 2026-05-08 03:41:10 +00:00
github-actions[bot]
abf6b6619d chore(ui): refresh pt-BR control ui locale 2026-05-08 03:40:48 +00:00
github-actions[bot]
448f62f53a chore(ui): refresh de control ui locale 2026-05-08 03:40:16 +00:00
github-actions[bot]
858b6bf3ae chore(ui): refresh zh-CN control ui locale 2026-05-08 03:40:14 +00:00
github-actions[bot]
9fc08fbf42 chore(ui): refresh zh-TW control ui locale 2026-05-08 03:40:07 +00:00
Val Alexander
d12c92c216 fix(usage): roll up session lineage history
Summary:
- Roll up transcript-backed session usage across logical session lineage.
- Preserve lineage through /new and /reset rotations.
- Add Control UI usage scope controls with legacy gateway fallback.
- Refresh generated protocol and Control UI locale fallback surfaces.

Verification:
- pnpm test src/auto-reply/reply/session.test.ts ui/src/ui/controllers/usage.node.test.ts src/gateway/server-methods/usage.sessions-usage.test.ts
- pnpm protocol:check
- pnpm ui:i18n:check
- pnpm ui:build
- git diff --check
- PR CI green on 10f10850ee

Closes #50701.
2026-05-07 22:38:11 -05:00
Bek
737e5707f1 docs: clarify Slack thread sessions (#79221) 2026-05-07 23:36:37 -04:00
Momo
681042a897 Add Maurice Niu as maintainer (#79219) 2026-05-08 11:33:58 +08:00
sallyom
79853b2fe8 docs: align digitalocean root setup 2026-05-07 23:31:03 -04:00
Jerry-Xin
6ec4e5cf4a fix: check effective UID (geteuid) in root guard
assertNotRoot only checked process.getuid(), so the guard was bypassed
when the CLI was launched with a non-root real UID but an effective UID
of 0 (e.g. via a setuid-root wrapper). In that context the process still
has root write privileges and can cause the same state/config corruption
the guard was added to prevent.

Now checks both getuid() and geteuid() — either being 0 triggers the
guard. Added three tests covering setuid-root scenarios.
2026-05-07 23:31:03 -04:00
Jerry-Xin
ad461c74cc fix: narrow container bypass to require container hint
OPENCLAW_CLI_CONTAINER_BYPASS alone is an internal recursion sentinel,
not a user-facing opt-in. Require OPENCLAW_CONTAINER_HINT to also be
present — this combination only occurs in the container-forwarding flow
(container-target.ts), so inherited or accidental env vars can no
longer silently skip the root guard.
2026-05-07 23:31:03 -04:00
Jerry-Xin
5986c2d013 fix: enforce root guard unconditionally on legacy entrypoint
Remove the --help/--version exemption from the legacy entrypoint
(src/index.ts). Unlike src/entry.ts which has fast-path exits before
startup work, the legacy path always calls runCli() which runs dotenv
loading and debug capture initialization before rendering output. The
assertNotRoot() error message already shows the OPENCLAW_ALLOW_ROOT=1
escape hatch, so users can still discover the override.
2026-05-07 23:31:03 -04:00
Jerry-Xin
690c7aa263 fix: apply root guard to legacy CLI entrypoint in src/index.ts
runLegacyCliEntry now calls assertNotRoot() before runCli, matching
the protection already present in src/entry.ts. Help and version
invocations are exempted so users can still discover OPENCLAW_ALLOW_ROOT.
2026-05-07 23:31:03 -04:00
Jerry-Xin
a5f6668a5c fix: skip root guard for container-forwarded CLI
The container forwarder sets OPENCLAW_CLI_CONTAINER_BYPASS=1 but not
OPENCLAW_ALLOW_ROOT. When the child CLI inside a root-based container
hits assertNotRoot(), it would exit before command handling. Exempt
container-forwarded invocations from the root guard.
2026-05-07 23:31:03 -04:00
Jerry-Xin
225339abc8 test: use static import in root-guard tests
Replace dynamic import helper with a static import since root-guard.ts
has no module-level mutable state and vi.resetModules() is not used,
making the dynamic import unnecessary.
2026-05-07 23:31:03 -04:00
忻役
ca8121d22b fix: add root guard to prevent CLI execution as root (#67478)
Block openclaw CLI from running as root (uid 0) to prevent:
- Separate state directory at /root/.openclaw/
- Conflicting systemd user services racing on port 18789
- Root-owned files in the service user state dir (EACCES)

The guard runs early in src/entry.ts before any state/config operations.
Root-level --help and --version bypass the guard so users can discover
the OPENCLAW_ALLOW_ROOT=1 override. Subcommand help paths still enforce
the guard since they enter runCli() and resolve state directories.

Closes #67478
2026-05-07 23:31:03 -04:00
Zeroth
741315e657 fix(slack): seed thread routing for implicit-conversation channels (#78522)
When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `🧵<root_ts>` session, breaking conversational continuity.

Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session.

Fixes #78505
2026-05-07 23:30:10 -04:00
Val Alexander
4e983aa57b fix: hide retired google gemini chat models
Summary:
- Hide retired and non-public Google Gemini model IDs from Control UI/chat model catalogs.
- Route the bare gemini-3-pro alias to gemini-3.1-pro-preview.
- Keep models.list fallback rows filtered by manifest suppressions and update stale pricing-cache expectations.

Verification:
- pnpm test src/commands/models/list.list-command.forward-compat.test.ts src/commands/models/list.rows.test.ts extensions/google/manifest.test.ts extensions/google/model-id.test.ts extensions/google/provider-models.test.ts extensions/google/provider-policy-api.test.ts extensions/google/media-understanding-provider.video.test.ts src/plugin-sdk/provider-model-id-normalize.test.ts src/plugins/manifest-model-suppression.test.ts src/gateway/server-methods/models.test.ts ui/src/ui/chat-model-select-state.test.ts ui/src/ui/chat-model-ref.test.ts
- pnpm test src/gateway/model-pricing-cache.test.ts
- pnpm --silent openclaw models list --all --json --provider google / google-vertex hidden-row probe
- Testbox pnpm check:changed: https://github.com/openclaw/openclaw/actions/runs/25534551033
2026-05-07 22:20:07 -05:00
pashpashpash
fb106fb9ae fix codex harness service tiers (#79152) 2026-05-08 12:10:41 +09:00
Omar Shahine
df069f7b02 fix(imessage): surface silent group-allowlist drops at default log level (#79190)
Merged via squash.

Prepared head SHA: 6454366a62
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-05-07 20:09:05 -07:00
Val Alexander
5ae385b2f0 fix(ui): keep control ui refresh responsive
Summary:
- Keep Control UI chat refresh usable while history and secondary metadata refreshes are slow, with an explicit history-await path for manual refresh.
- Let config and channel tabs render cheap/stale snapshots before slow schema or probe work finishes, then request updates when background refreshes settle.
- Bound large chat render pressure to the last 100 history messages and preserve slow-render/long-frame instrumentation for follow-up tuning.
- Add regression coverage for non-blocking refreshes, manual refresh completion, background update callbacks, and the 100-message render cap.

Verification:
- pnpm test ui/src/ui/app-chat.test.ts ui/src/ui/app-render.helpers.node.test.ts ui/src/ui/app-settings.refresh-active-tab.node.test.ts ui/src/ui/control-ui-performance.test.ts ui/src/ui/controllers/chat.test.ts ui/src/ui/chat/build-chat-items.test.ts
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md ui/src/ui/app-chat.ts ui/src/ui/app-chat.test.ts ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.helpers.node.test.ts ui/src/ui/app-render.ts ui/src/ui/app-settings.ts ui/src/ui/app-settings.refresh-active-tab.node.test.ts ui/src/ui/chat/build-chat-items.ts ui/src/ui/chat/build-chat-items.test.ts ui/src/ui/chat/history-limits.ts
- git diff --check origin/main..HEAD && git diff --check
- GitHub CI on exact head 53295aeb4f: all required checks passed
2026-05-07 22:02:35 -05:00
sallyom
cbc69d9a96 fix: surface gateway version skew
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-07 22:58:14 -04:00
Patrick Erichsen
cde99c3349 docs: add ClawHub publishing page 2026-05-07 19:43:06 -07:00
Daev Mithran
9e1e59717f feat(plugin-sdk): add LLM completion API to plugin (#64294) 2026-05-07 19:27:48 -07:00
Omar Shahine
e259751ec9 feat(imessage): private-API support via imsg JSON-RPC [AI-assisted] (#78317)
Merged via squash.

Prepared head SHA: b7d336b296
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-05-07 19:20:18 -07:00
Peter Steinberger
6cfb08680e fix(codex): close app-server stdio gracefully 2026-05-08 02:58:27 +01:00
Peter Steinberger
bee3a7372e fix(cli): dispose agent harnesses on exit 2026-05-08 02:58:12 +01:00
Peter Steinberger
22657861c8 fix(agents): enable codex for openai overrides 2026-05-08 02:57:55 +01:00
Peter Steinberger
f463d471d3 fix(gateway): scope explicit live model registry 2026-05-08 02:57:30 +01:00
Patrick Erichsen
10f9a758b6 docs: add dedicated ClawHub docs tab (#79159)
* docs: add clawhub docs tab

* fix: satisfy docs sync lint

* docs: prune internal clawhub nav pages

* docs: include ClawHub publishing page in nav

* docs: use clawhub how-it-works route
2026-05-07 18:55:08 -07:00
Peter Steinberger
478996231a test: stabilize Codex searchable dynamic tools test 2026-05-08 02:42:38 +01:00
Val Alexander
af0c273d28 ui: gate WhatsApp QR actions by link state
Summary:
- Gate WhatsApp Control UI QR actions by link state so unlinked accounts show Show QR, linked accounts show Relink, and Wait for scan appears only while a QR is active.
- Preserve the existing web.login.start/web.login.wait controller flow while removing misleading simultaneous actions.
- Add focused Lit render tests and a user-facing changelog entry.

Verification:
- pnpm docs:list
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md ui/src/ui/views/channels.whatsapp.ts ui/src/ui/views/channels.test.ts
- pnpm test ui/src/ui/views/channels.test.ts ui/src/ui/controllers/channels.test.ts
- git diff --check
- pnpm changed:lanes --json
- pnpm check:changed
- GitHub CI passed on b5f4433f89
2026-05-07 20:21:47 -05:00
Josh Avant
83aad863fd Clarify exec filesystem policy drift (#79153)
* docs: clarify exec filesystem policy

* fix: warn on exec filesystem policy drift

* docs: clarify exec filesystem mutation surface
2026-05-07 20:05:19 -05:00
Peter Steinberger
e0cc5c0eee fix: preserve progress draft tool titles 2026-05-08 02:03:12 +01:00
Peter Steinberger
164ecfd7c8 fix: show web search queries in progress drafts 2026-05-08 02:03:12 +01:00
Peter Steinberger
accf774591 fix: make channel progress labels rolling 2026-05-08 02:03:12 +01:00
Peter Steinberger
ef29c85a48 fix: improve Discord progress draft rendering 2026-05-08 02:03:12 +01:00
Patrick Erichsen
31a710c5a6 Fix Telegram model status Codex auth label (#79135) 2026-05-07 20:43:31 -04:00
Kevin Lin
a1ac559ed7 feat(codex): enable native plugin app support (#78733)
* feat(codex): add native plugin config schema

* feat(codex): add native plugin inventory activation

* feat(codex): configure native plugin apps for threads

* feat(codex): enforce plugin elicitation policy

* feat(codex): migrate native plugins

* docs(codex): document native plugin support

* fix(codex): harden plugin migration refresh

* fix(codex): satisfy plugin activation lint

* fix: stabilize codex plugin app config

* fix: address codex plugin review feedback

* fix: key codex plugin app cache by websocket credentials

* fix: keep codex plugin app fingerprints stable

* fix: refresh codex plugin cache test fixtures

* fix: refresh plugin app readiness after activation

* fix: support remote codex plugin activation

* fix: recover plugin app bindings after cache refresh

* fix: force codex app refresh after plugin activation

* fix: recover partial codex plugin app bindings

* fix: sync codex plugin selection config

* fix: keep codex plugin activation fail closed

* fix: align codex plugin protocol types with main

* fix: refresh partial codex plugin app bindings

* fix: key codex app cache by env api key

* fix: skip failed codex plugin migration config

* test: update codex prompt snapshots

* fix: fail closed on missing codex app inventory entries

* fix(codex): enforce native plugin policy gates

* fix(codex): normalize native plugin policy types

* fix(codex): fail closed on plugin refresh errors

* fix(codex): use native plugin destructive policy

* fix(codex): key plugin cache by api-key profiles

* fix(codex): drop unshipped plugin fingerprint compat

* fix(codex): let native app policy gate plugin tools

* fix(codex): allow open-world plugin app tools

* fix(codex): revalidate native plugin app bindings

* fix(codex): preserve plugin binding on recheck failure

* docs(codex): clarify plugin harness scope

* fix(codex): return activation report state exhaustively

* test(codex): refresh prompt snapshots after rebase

* fix(codex): match namespaced plugin ids
2026-05-07 17:20:28 -07:00
Peter Steinberger
b75e5c50bf docs: document OpenAI realtime voices 2026-05-08 01:07:46 +01:00
Peter Steinberger
63ec912786 fix(openai): use GA realtime bridge for gpt-realtime-2
Summary:
- switch OpenAI realtime voice default to gpt-realtime-2
- migrate backend OpenAI WebSocket bridge to the GA session shape and drop the beta header
- keep Azure deployment realtime bridges on the deployment-compatible shape
- extend live Talk smoke coverage and align npm Telegram package-runtime assertions

Verification:
- pnpm test extensions/openai/realtime-voice-provider.test.ts src/gateway/protocol/index.test.ts src/gateway/talk-handoff.test.ts extensions/google-meet/index.test.ts -- --reporter=dot
- pnpm test test/scripts/npm-telegram-live.test.ts -- --reporter=dot
- pnpm check:docs
- env OPENCLAW_TESTBOX=0 pnpm check:changed
- OPENCLAW_REALTIME_OPENAI_MODEL=gpt-realtime-2 node --import tsx scripts/dev/realtime-talk-live-smoke.ts
2026-05-08 00:47:25 +01:00
pashpashpash
3f217964d1 Defer Codex dynamic tools behind search
Defer OpenClaw Codex dynamic tools behind Codex tool_search, keep required turn-control tools direct, pin the managed Codex harness to 0.129.0-alpha.15, and document the real behavior/token impact from the live dev-agent watch.
2026-05-07 16:40:37 -07:00
Kevin Lin
e984a99c7e fix: keep gateway watch sync tracing opt-in (#79110) 2026-05-07 16:40:35 -07:00
Mert Başar
029ca8c268 feat(agents): implement state-aware failover and lane suspension
Summary:
- Persist quota-suspension state transitions and reload fresh suspension state before failover handoff injection.
- Restore suspended lanes to configured concurrency and share failover-to-suspension reason mapping across fallback and embedded runner paths.
- Export model.failover diagnostics via OTLP and cover queueing/resume behavior with regressions.

Verification:
- pnpm test src/config/sessions/store.pruning.integration.test.ts src/process/command-queue.test.ts src/agents/session-suspension.test.ts src/agents/model-fallback.test.ts extensions/diagnostics-otel/src/service.test.ts
- git diff --check
- pnpm exec oxfmt --check --threads=1 on changed TypeScript files
- GitHub checks: 92 successful, 0 pending, 0 failed on head 962146be88
- Review threads: none unresolved
2026-05-07 18:34:05 -05:00
Peter Steinberger
e29f4ff6b8 fix: keep npm telegram e2e on package runtime 2026-05-08 00:29:20 +01:00
Peter Steinberger
6a4069dead fix: share plugin runtime helpers
Consolidate shared plugin runtime MIME/schema helpers, preserve canonical runtime behavior, and guard QQBot STT fetches.
2026-05-08 00:28:43 +01:00
Peter Steinberger
f3c9203631 fix(mistral): normalize structured completion content 2026-05-08 00:21:55 +01:00
Peter Steinberger
9e4da8c7b3 fix(active-memory): honor agent allowlist in status 2026-05-08 00:21:44 +01:00
Peter Steinberger
b00c9943bd fix(active-memory): avoid google chat space ids as channels 2026-05-08 00:21:26 +01:00
Peter Steinberger
84dd9c7395 fix(gateway): fail closed for trusted-proxy auth 2026-05-08 00:21:08 +01:00
Peter Steinberger
97d2d40fb7 fix: allow safe exec secret passEnv inheritance 2026-05-08 00:00:40 +01:00
Peter Steinberger
3adce8fac1 fix: show active model in session status 2026-05-08 00:00:40 +01:00
Peter Steinberger
3a452a029c ci(release): automate stable appcast handoff 2026-05-07 23:46:33 +01:00
sallyom
244c2b5b23 fix: bound skills watcher traversal
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-07 18:12:08 -04:00
Marcus Castro
5df08201ff refactor(runtime): add prepared runtime foundation (#78248)
* docs(runtime): document prepared runtime guidance

* refactor(provider-runtime): thread prepared provider handles

* refactor(runtime-plan): add prepared runtime foundation

* refactor(outbound): add prepared channel runtime facts

* refactor(models): add scoped model reference helpers

* refactor(plugin-sdk): expose prepared runtime helper surfaces
2026-05-07 18:49:42 -03:00
Shakker
70eabd3b08 fix: satisfy cron model selection checks 2026-05-07 22:47:04 +01:00
Shakker
ab3a3d14f0 fix: satisfy chat pending switch lint 2026-05-07 22:40:31 +01:00
Peter Steinberger
6f4272bd04 fix(providers): preserve streaming error bodies 2026-05-07 22:36:31 +01:00
Peter Steinberger
830a72d2ee fix(chat): reset model override with default 2026-05-07 22:36:31 +01:00
Peter Steinberger
139122f655 fix(cron): show rejected model allowlist 2026-05-07 22:36:31 +01:00
Peter Steinberger
e74347bbe7 fix(agents): retry overloaded subagent announces 2026-05-07 22:36:31 +01:00
Peter Steinberger
a95d7ab1c8 fix(providers): honor cidr no_proxy entries 2026-05-07 22:36:31 +01:00
Peter Steinberger
36835592df feat: log discord voice transcripts 2026-05-07 22:17:42 +01:00
Peter Steinberger
6785633d13 fix(ui): wait for pending model switches before send 2026-05-07 22:05:46 +01:00
Peter Steinberger
70717c50fc fix(agents): clamp compaction reserve tokens 2026-05-07 22:05:45 +01:00
Peter Steinberger
5a4676bd64 fix(byteplus): align Kimi catalog metadata 2026-05-07 22:05:45 +01:00
Peter Steinberger
fa8a85586c ci(release): create GitHub release during publish 2026-05-07 22:03:46 +01:00
sallyom
56fe64e8e3 fix: print resolved installer follow-up command
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-07 16:31:21 -04:00
Alex Knight
6a8b4e422e Implement ACP bridge lifecycle handlers (#78880)
* Implement ACP bridge lifecycle handlers

* docs: add acp smoke evidence example

* docs: trim acp smoke example

* docs: remove acp pr plan file

* fix: tighten acp session list filters

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-08 06:26:36 +10:00
Vincent Koc
0fca665497 docs(imessage): document bluebubbles deprecation 2026-05-07 13:12:00 -07:00
Vincent Koc
2597723dfc fix(test): align main channel assumptions 2026-05-07 13:03:49 -07:00
Vincent Koc
7f4c0b3192 chore(config): refresh bundled channel metadata 2026-05-07 12:53:02 -07:00
Vincent Koc
91ed1604b0 docs(imessage): make imsg the supported setup path 2026-05-07 12:53:01 -07:00
Vincent Koc
84638bfbb0 fix(imessage): report non-mac default imsg hosts 2026-05-07 12:53:01 -07:00
Vincent Koc
4ad4be9aff test(channels): drop bluebubbles fixture assumptions 2026-05-07 12:53:01 -07:00
Vincent Koc
07bf572f35 chore(channels): delete bluebubbles plugin package 2026-05-07 12:53:00 -07:00
Vincent Koc
c97998ce21 chore(channels): remove bluebubbles bundled surface 2026-05-07 12:52:48 -07:00
Vincent Koc
f482e4d335 fix(channels): surface missing external plugin repairs
## Summary
- Add catalog-backed repair hints for official external channel plugins.
- Show configured Feishu/WhatsApp-style external channels as missing-plugin warning rows in status surfaces.
- Keep installed-but-unconfigured, disabled, allowlist-denied, and untrusted plugins on their real activation/configuration error paths.

Fixes #78702
Fixes #78593
2026-05-07 12:49:17 -07:00
sallyom
484a289be3 docs: document docker cli dns override
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-07 15:18:25 -04:00
jesse-merhi
95a1c91531 ci: make network CodeQL shard additive 2026-05-08 01:18:04 +10:00
jesse-merhi
b6c9ed66c3 chore: preserve opengrep generated timestamp 2026-05-08 01:18:04 +10:00
jesse-merhi
cf9e9cd119 lint: move managed proxy guard to codeql 2026-05-08 01:18:04 +10:00
jesse-merhi
dd0a9bf869 lint: replace raw socket guard with codeql 2026-05-08 01:18:04 +10:00
jesse-merhi
9cc5e49e65 lint: replace proxy mutation guard with opengrep 2026-05-08 01:18:04 +10:00
jesse-merhi
f05e2222f3 lint: allow managed proxy mutation scopes 2026-05-08 01:18:04 +10:00
jesse-merhi
9eaadcdf29 chore: add proxy guardrail changelog 2026-05-08 01:18:04 +10:00
jesse-merhi
f4797921ac lint: classify raw socket callsites 2026-05-08 01:18:04 +10:00
Vincent Koc
8e88c7b297 test(plugins): align canvas startup metadata 2026-05-07 07:16:21 -07:00
Vincent Koc
fcb9dcc886 test(openai): align codex default auth contract 2026-05-07 07:16:20 -07:00
Vincent Koc
237fcbcbf1 test(llm-task): use real typebox schemas 2026-05-07 07:16:20 -07:00
Vincent Koc
9b279ef173 fix(agents): reclaim reported stale session locks 2026-05-07 07:16:20 -07:00
Vincent Koc
11a038207b fix(infra): support non-durable text writes 2026-05-07 07:16:20 -07:00
Vincent Koc
3a89e20b7b fix(infra): support hardlink-safe package moves 2026-05-07 07:16:20 -07:00
Peter Steinberger
a68ad39877 ci(release): speed up beta publish path 2026-05-07 15:02:24 +01:00
Vincent Koc
c41a73b828 docs(providers/arcee): note Trinity Large Thinking has tools disabled 2026-05-07 06:49:14 -07:00
Ayaan Zaidi
238e72d74d docs(changelog): note telegram poll cap (#78762) (thanks @obviyus) 2026-05-07 19:08:43 +05:30
Ayaan Zaidi
11d6a3f892 fix(telegram): keep dm allow separate from group auth 2026-05-07 19:08:43 +05:30
Ayaan Zaidi
c967628816 fix(telegram): restore outbound poll cap 2026-05-07 19:08:43 +05:30
Ayaan Zaidi
923ea990fd refactor(telegram): use grammY native helpers 2026-05-07 19:08:43 +05:30
Ayaan Zaidi
53efb6747d refactor(telegram): centralize access authorization 2026-05-07 19:08:43 +05:30
Ayaan Zaidi
6554e85ad6 refactor(telegram): unify outbound delivery adapter 2026-05-07 19:08:43 +05:30
Peter Steinberger
a966303216 build(canvas): refresh A2UI bundle hash 2026-05-07 14:28:38 +01:00
Peter Steinberger
dd09e6fe40 fix(arcee): disable tools for Trinity thinking 2026-05-07 14:28:33 +01:00
Peter Steinberger
a85261932e fix(cli): fall back to sips for HEIC infer inputs 2026-05-07 14:28:27 +01:00
Peter Steinberger
6ce1c98b61 fix: normalize auth profile inline secrets 2026-05-07 13:46:46 +01:00
Peter Steinberger
347b51be4b fix: sanitize existing prompt images 2026-05-07 13:46:46 +01:00
Peter Steinberger
548b55676f fix: strip unsupported Fireworks tool schema keywords 2026-05-07 13:46:46 +01:00
Peter Steinberger
772034d741 fix: strip tools for no-tool completions models 2026-05-07 13:46:46 +01:00
Pavan Kumar Gondhi
c65f3bc70e Compute plugin callback authorization dynamically [AI] (#78866)
* fix: compute plugin callback command authorization

* addressing codex review

* addressing ci

* addressing ci

* docs: add changelog entry for PR merge
2026-05-07 18:05:21 +05:30
Peter Steinberger
be33b68fd4 test: expand native sqlite Kysely coverage
Expand the native node:sqlite Kysely dialect tests for connection setup, insert metadata, transaction/savepoint behavior, and streaming.
2026-05-07 13:18:29 +01:00
Peter Steinberger
955b025697 feat: add native sqlite Kysely dialect
Add an owned Kysely dialect for native node:sqlite, raise the Node 22 floor to 22.16+ for StatementSync.columns(), and cover select/returning/stale insert id behavior.
2026-05-07 13:07:03 +01:00
Peter Steinberger
037174141e docs(changelog): clarify session-store fsync scope 2026-05-07 12:32:02 +01:00
Peter Steinberger
897bac5b8c fix(sessions): skip durable fsync for session store 2026-05-07 12:31:18 +01:00
Shakker
01dd593cfd test: stabilize prompt snapshot plugin tools 2026-05-07 12:25:08 +01:00
Peter Steinberger
64514a6548 test: remove unused canvas temp helper 2026-05-07 12:15:51 +01:00
Peter Steinberger
e867ab7e16 test: import os in canvas a2ui test 2026-05-07 12:15:51 +01:00
Peter Steinberger
f2bf925a38 fix: guard sandbox move cleanup identity 2026-05-07 12:15:51 +01:00
Peter Steinberger
530e4f93de refactor: use fs-safe for staged package swaps 2026-05-07 12:15:51 +01:00
Peter Steinberger
113761ab57 build: update fs-safe dependency 2026-05-07 12:15:51 +01:00
Peter Steinberger
2f69c40a62 fix: preserve late sandbox rename writes 2026-05-07 12:15:51 +01:00
Peter Steinberger
55a8f56a15 fix: harden sandbox runtime cleanup 2026-05-07 12:15:51 +01:00
Peter Steinberger
56636dfe57 fix(ci): restore main validation 2026-05-07 12:00:29 +01:00
Shakker
6ef7fa08af test: keep bluebubbles schema tests extension-local 2026-05-07 12:00:18 +01:00
Shakker
2c0f8a0beb fix: restore canvas ci checks 2026-05-07 11:55:52 +01:00
Shakker
0fd6607d56 fix: await control ui chat startup refresh 2026-05-07 11:50:48 +01:00
Vincent Koc
7ad53cefee fix(ci): account for canvas a2ui deps 2026-05-07 03:47:13 -07:00
pashpashpash
1c33990108 Route OpenAI agents through Codex by default (#78899)
* route openai agent runs through codex

* fix: load codex plugin for implicit openai runtime

* fix: preserve explicit OpenAI PI Codex auth routing

* fix: show codex auth for openai model listing

* fix: map codex auth into configured openai list rows

* fix: preserve explicit openai pi auth routes

* docs: keep openai model route examples canonical

* fix: clean openai codex test fixtures

* fix: scope codex auth status fallback

* fix: repair current ci boundary drift
2026-05-07 19:46:49 +09:00
Peter Steinberger
8b701ce1c7 fix: repair ci regressions 2026-05-07 11:46:21 +01:00
Vincent Koc
a6159bb60d docs(skills): clarify Crabbox broker auth 2026-05-07 03:43:49 -07:00
Vincent Koc
b165c0d10a fix(ci): restore main validation 2026-05-07 03:39:26 -07:00
Vincent Koc
c676cd4dcf docs(skills): keep broad OpenClaw gates remote 2026-05-07 03:35:12 -07:00
CaptainTimon
e1fec3c892 fix(config): remove core BlueBubbles schema (#78612)
* fix(config): remove core BlueBubbles schema

* fix(config): preserve BlueBubbles dmPolicy validation

* fix(config): type BlueBubbles account refinement

* chore(plugin-sdk): refresh API baseline

* chore(plugin-sdk): refresh API baseline

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-07 03:16:42 -07:00
Alex Knight
bf3b994378 fix(compaction): ignore metadata bytes in preflight pressure
Fix stale preflight compaction pressure estimation so metadata bytes before the latest usage record do not count as model-context tokens, while preserving post-usage tail pressure and the active transcript byte guard.

Fixes #78604.
2026-05-07 20:10:20 +10:00
Vincent Koc
f2b01bb7b1 feat(openai): add chat-latest model override
Add openai/chat-latest as an explicit direct API-key OpenAI model override, document the moving alias, and normalize unsupported Responses text verbosity for that model.
2026-05-07 03:09:16 -07:00
Pavan Kumar Gondhi
5852f5d15c fix(active-memory): require admin scope for global toggles [AI] (#78863)
* fix: gate active-memory global writes by admin scope

* addressing claude review

* docs: add changelog entry for PR merge
2026-05-07 15:35:30 +05:30
Vincent Koc
f4b2a08c85 test(gateway): use core node command in pairing authz 2026-05-07 03:00:34 -07:00
Vincent Koc
b5d434db61 docs(providers/anthropic): correct media-understanding default model to claude-opus-4-7 2026-05-07 02:57:16 -07:00
Pavan Kumar Gondhi
758051322d Honor owner enforcement for native commands [AI] (#78864)
* fix: honor owner enforcement for native commands

* addressing codex review

* addressing codex review

* docs: add changelog entry for PR merge
2026-05-07 15:26:49 +05:30
Vincent Koc
55bff24973 fix(plugins): share npm script shell env (#78887) 2026-05-07 02:56:32 -07:00
Vincent Koc
283c957fdc changelog: credit @sliverp for channels list channel-only rework 2026-05-07 02:50:31 -07:00
VACInc
8de5a55317 Fix Tavily tool SecretRef runtime config
Resolve Tavily dedicated tool credential lookup against the active runtime config snapshot.

PR: https://github.com/openclaw/openclaw/pull/78610
2026-05-07 02:40:22 -07:00
Peter Steinberger
129b9dad9e ci: use explicit channel contract runner 2026-05-07 10:28:34 +01:00
Sliverp
9170243f92 Feat/channels list show all and drop auth (#78456)
* feat(channels list): drop auth providers, add --all, surface installed/configured/enabled

`openclaw channels list` used to conflate two very different surfaces: chat
channels and OAuth/API-key auth providers for model routing. The auth
section was the first and most visible block in the output even for
operators who only cared about chat channels, and its JSON `auth` key
leaked model-provider identities into a command whose top-level help
describes it as channel management. Worse, the command silently hid
every channel that had no configured account, so users could not tell
from `channels list` which bundled or catalog channels were even
available to configure.

Split the surface cleanly around channels only:

1. Remove the `Auth providers (OAuth + API keys)` text section and the
   `auth` field from the JSON payload. Model-provider auth profiles
   remain reachable via `openclaw models auth list`, which is where
   they conceptually belong.

2. Add a `--all` flag to surface every channel an operator could
   configure: bundled channel plugins that have no account yet and
   catalog-listed external channels whose plugin package is not even
   installed on disk. Without `--all` the output still shows only
   channels with at least one configured account, matching the
   previous default behavior so existing scripts keep working. The
   "empty" default path now prints a hint pointing at `--all`.

3. Render three explicit status tags per row — `installed` /
   `not installed`, `configured` / `not configured`, `enabled` /
   `disabled` — so bundled-but-unconfigured plugins and installable
   catalog channels both render with accurate state instead of being
   invisible. Installed state comes from the same
   `isCatalogChannelInstalled` probe the setup flow uses, so it stays
   consistent with `openclaw onboard` and `channels add`.

4. JSON payload now carries an `origin` per channel (`configured`,
   `available`, `installable`) alongside `installed: boolean`, which
   lets tooling distinguish "user has set this up" from "user could
   set this up" without second-guessing.

Register `--all` on both the Commander CLI and the fast-path route-arg
parser so the flag works in both code paths, update the one routes
test that asserted the parsed args shape, and rewrite the old auth
profiles surface test as a broader `channels list` behavior spec
covering default output, `--all` output, JSON shape (no `auth`), and
the bundled-unconfigured + catalog-not-installed cases.

Docs: call out that `channels list` is chat-channel only now, mention
`--all`, and point at `openclaw models auth list` for what used to be
the auth providers block.

* fix(channels list): surface catalog channels that are installed on disk but not yet configured

The previous `--all` path filtered catalog entries with
`!installedByChannelId.get(entry.id)` before rendering them as
catalog-only rows. That assumed "catalog entry not already rendered
as a plugin row" implied "not installed", which is wrong: an external
channel plugin package can be installed on disk (`isCatalogChannelInstalled`
returns true) while the read-only channel loader still declines to
surface a plugin object for it — the loader only activates channels
that appear in user config, so a plugin that is installed but never
configured ended up in neither bucket and silently dropped out of
`channels list --all`.

Operator-facing symptom: `pnpm openclaw channels list --all` omitted
WeCom (and any other catalog channel in the same state) even though
its npm package was present on disk and its catalog entry existed,
while rendering every other uninstalled catalog channel as expected.

Fix: drop the `installed` filter from `catalogOnlyLines` so every
catalog entry that is not already represented by a plugin row is
rendered, and let the row itself carry the real installed/not-installed
tag. Two renderings now land in the catalog-only bucket:

- Not installed — rendered as `not installed, not configured, disabled`
  (installable row).
- Installed but unconfigured — rendered as `installed, not configured,
  disabled` (ready-to-configure row). The JSON `origin` for this case
  becomes `available`, matching the existing origin for bundled
  plugins that are installed but unconfigured, so downstream tooling
  sees a consistent "you could configure this now" signal regardless
  of whether the plugin came from bundled sources or from the catalog.

Regression test added under the WeCom scenario.

* refactor(channels list): drop model-provider usage surface, make the command channel-only

`openclaw channels list` used to append a model-provider usage/quota
snapshot (Anthropic, OpenRouter, OpenAI Codex, Gemini, Zai, Minimax,
etc.) under every invocation. That was a leftover from the days when
`channels list` was the only "operator overview" command; the same
data is now owned by `openclaw status` (overview) and
`openclaw models list` (per-provider), which handle timeouts, probe
errors, and output shape consistently for that class of data. Keeping
the snapshot wired into `channels list` meant:

- Every default invocation made one blocking `loadProviderUsageSummary`
  call that fanned out to every configured provider billing/auth
  endpoint, adding seconds of latency to a command that otherwise
  just reads local config.
- `channels list --no-usage` was the escape hatch, but the flag was
  itself a self-sustaining bug: it only existed because the command
  did work that did not belong to it.
- JSON consumers had an optional `usage` key whose shape was owned by
  the provider-usage module, not by the channels module, so any
  change upstream silently reshaped `channels list --json` output.
- Failed provider fetches printed provider-side errors on a command
  that never advertised itself as a provider-health surface.

Scope this PR tightens, in one move:

1. Remove `loadProviderUsageSummary` / `formatUsageReportLines` usage
   from `src/commands/channels/list.ts`. The command now only reads
   config, the read-only channel plugin registry, and the trusted
   catalog — matching its name.
2. Drop `--no-usage` from the Commander CLI registration, from the
   fast-path route-arg parser (`parseChannelsListRouteArgs`), and
   from `ChannelsListOptions`. The flag is gone, not silently
   ignored, so anyone depending on it will get a clear
   "unknown option" from Commander and from the fast-path router.
3. Drop the `usage` key from `channels list --json` payloads. Shape
   of the `chat` record and the new `origin` / `installed` tags
   introduced earlier in this branch are unchanged.
4. Print a single-line migration pointer at the bottom of the text
   output so operators who expected usage know where it went
   (`openclaw status` / `openclaw models list`). This replaces what
   used to be a block of fetched provider data with one static line,
   so it cannot fail or add latency.
5. Update `docs/cli/channels.md` troubleshooting to remove the
   `--no-usage` mention and point at the two new entry points.
6. Update tests: drop the `loadProviderUsageSummary` mock and the
   `"keeps JSON output valid when usage loading fails"` case,
   replace it with a positive assertion that `payload.usage` is
   undefined (locking in the narrower contract), and remove `usage`
   from every `channelsListCommand(...)` call to match the narrowed
   `ChannelsListOptions` type. The route-args test is updated to
   expect `{ json, all }` without `usage`.

No other command changes. `openclaw status` and `openclaw models list`
already render usage; they are the documented replacements.

Breaking-ish surface:

- CLI: `channels list --no-usage` now fails with "unknown option".
  Tooling should drop the flag — there is nothing left to opt out of.
- JSON: `channels list --json` no longer carries a top-level `usage`
  key. Tooling that read it must migrate to
  `openclaw status --json` or `openclaw models list --json`.

* fix(channels.list.test): widen isCatalogChannelInstalled mock signature to accept entry param

CI typecheck failed because the mock was declared with a zero-arg signature while one test called mockImplementation(({ entry }) => …). Tighten the generic so vitest's mock accepts the same params the real helper does.

* changelog: record channels list channel-only rework (#78456)
2026-05-07 17:28:03 +08:00
Vincent Koc
45778c66f4 docs(cli): document cron list/show --json status field 2026-05-07 02:25:09 -07:00
Peter Steinberger
8e17910191 fix: treat aws sdk auth profiles as config metadata 2026-05-07 10:24:19 +01:00
Aaron Weiker
8974a78f47 feat(cron): add computed status field to --json output (#78701)
* feat(cron): add computed status field to --json output

`openclaw cron list --json` and `openclaw cron show <id> --json` now
include a top-level `status` field on each job object, computed from
enabled + state.runningAtMs + state.lastRunStatus.

Values: "disabled" | "running" | "ok" | "error" | "skipped" | "idle"

This matches the human-readable status column already shown by
`cron list` and `cron show` (without --json), making it easier for
external tooling (dashboards, ops gateways) to determine job state
without re-implementing the derivation logic.

The raw state object is preserved unchanged for backward compatibility.

* fix: preserve lastStatus fallback + add changelog entry

Address ClawSweeper review findings:
- P2: Fall back to deprecated state.lastStatus when lastRunStatus is
  absent, matching the existing formatStatus behavior for legacy jobs.
- P3: Add CHANGELOG.md entry under Unreleased for this user-facing
  CLI feature.

* fix: address lint errors - add braces and avoid spread-in-map

---------

Co-authored-by: Rodin <rodin@forgedthought.ai>
Co-authored-by: claw <claw@weiker.me>
2026-05-07 02:19:18 -07:00
Christof
afdf03b563 fix: clear reset skills snapshot (#78873) 2026-05-07 11:18:39 +02:00
pashpashpash
3a901b5e95 Revert "Install Codex plugin on OpenAI model selection (#78799)" (#78878)
This reverts commit c8f3fecad6.
2026-05-07 18:13:59 +09:00
Vincent Koc
61386055b1 fix(test): use current gateway protocol in docker network smoke 2026-05-07 02:11:41 -07:00
Vincent Koc
34ca9adbf5 test(status): keep pi status expectation for openai routes 2026-05-07 02:02:59 -07:00
pashpashpash
c8f3fecad6 Install Codex plugin on OpenAI model selection (#78799)
* route openai agent runs through codex

* fix: load codex plugin for implicit openai runtime

* docs: credit openai codex auth fix

* fix(agents): respect custom openai runtime routing

* fix(agents): install codex plugin on openai selection

* fix(agents): preserve OpenAI Codex auth switching

* fix(ci): restore channel contract runner expression

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-07 01:57:34 -07:00
Vincent Koc
1831e124b2 fix(lint): clean up main lint regressions 2026-05-07 01:39:46 -07:00
Rajvardhan Patil
c25f319d49 fix(btw): keep usage placeholder visible
Fixes #62877.\n\nThanks @RajvardhanPatil07.
2026-05-07 01:36:11 -07:00
Pincer
8a66694c5e docs(lobster): clarify embedded openclaw.invoke limitation 2026-05-07 01:27:29 -07:00
Val Alexander
6b4ff8be81 fix(ui): bound sessions to configured agents
Fixes #41685.\n\nSummary:\n- Adds an additive sessions.list configuredAgentsOnly option for Control UI.\n- Filters default Control UI session listing to configured agents while preserving broad Gateway discovery for explicit callers.\n- Falls back restored unconfigured agent session keys before chat refresh.\n\nValidation:\n- pnpm protocol:check\n- pnpm test ui/src/ui/controllers/sessions.test.ts ui/src/ui/app-gateway.node.test.ts src/gateway/server.sessions.store-rpc.test.ts -- --reporter=verbose\n- pnpm format:docs:check\n- pnpm lint:swift\n- pnpm check:no-conflict-markers\n- git diff --check
2026-05-07 03:26:47 -05:00
Pavan Kumar Gondhi
d5eabbd36c fix(auto-reply): gate inline skill tool dispatch [AI] (#78517)
* fix: enforce tool hooks for inline skill dispatch

* addressing claude review

* addressing codex review

* addressing codex review

* fix: complete root-cause handling

* docs: add changelog entry for PR merge
2026-05-07 13:47:18 +05:30
Harris Ali
79d9b95e67 docs(msteams): document replyStyle resolution precedence and thread context preservation (#78835)
The existing replyStyle section explains the Posts-vs-Threads tradeoff but
doesn't document how the value is actually resolved at send-time, nor what
happens to thread-root context across configurations. Operators who hit
unexpected top-level posts (e.g., requireMention=false setups, long-running
agents whose proactive sends fall outside the live Bot Framework turn) have
no docs-side anchor for understanding which knob to flip.

Add two subsections under Reply Style:

1. Resolution precedence — channel > team > global > implicit default,
   plus the requireMention-derived implicit default.
2. Thread context preservation — describes that replyStyle=thread re-attaches
   the original thread root on outbound (live and proactive paths after
   #78387), with the threadId/activityId fallback for legacy stored refs.
   Calls out the deliberate "no thread suffix" behavior for replyStyle=top-level.

Pure documentation change, no behavior or surface impact.
2026-05-07 18:09:46 +10:00
Peter Steinberger
2c33464b26 chore: refresh generated hashes after rebase 2026-05-07 09:07:18 +01:00
Peter Steinberger
66b02c91b1 fix: build canvas assets for docker package build 2026-05-07 09:07:18 +01:00
Peter Steinberger
61e534428a fix: harden agent live event regressions 2026-05-07 09:07:18 +01:00
Peter Steinberger
1dd9a15eb8 fix: preserve deferred channel setup contracts 2026-05-07 09:07:18 +01:00
Peter Steinberger
bece8dcbb8 fix: harden generated surface pruning 2026-05-07 09:07:18 +01:00
Peter Steinberger
23920f6160 fix: preserve unmatched telegram access groups 2026-05-07 09:07:18 +01:00
Peter Steinberger
d033c369c6 fix: restore telegram access group allowlists 2026-05-07 09:07:18 +01:00
Peter Steinberger
330ba1fa31 refactor: move canvas to plugin surfaces 2026-05-07 09:07:18 +01:00
Peter Steinberger
c6e6b31643 docs: clarify legacy compatibility policy 2026-05-07 09:00:59 +01:00
Pavan Kumar Gondhi
0003f3f755 feishu: honor config write policy for dynamic agents [AI] (#78520)
* fix: honor Feishu config write policy for dynamic agents

* docs: add changelog entry for PR merge
2026-05-07 13:30:05 +05:30
Val Alexander
5a90179e8f feat(ui): show persistent chat context usage
Summary:
- Show a persistent compact Control UI/WebChat context usage indicator whenever fresh session token/context data is available below the high-pressure threshold.
- Preserve stale usage snapshot hiding plus the existing high-pressure warning and compact-session action thresholds.
- Update Control UI docs and changelog attribution.

Fixes #46398.
Refs #73744, #45048, #50071, #32188, and #62167.

Verification:
- pnpm docs:list
- pnpm format:docs:check
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/web/control-ui.md ui/src/styles/chat/layout.css ui/src/ui/chat/context-notice.ts ui/src/ui/chat/run-controls.test.ts
- pnpm test ui/src/ui/chat/run-controls.test.ts
- pnpm changed:lanes --json selected core, coreTests, docs only
- Blacksmith Testbox pnpm check:changed passed on tbx_01kr0pvxy0ssp70p3qe49j5dcb: https://github.com/openclaw/openclaw/actions/runs/25483307211
- GitHub PR checks for 04b8ad2e09 were clean before merge.
2026-05-07 02:59:42 -05:00
Pavan Kumar Gondhi
2d65ead914 fix(skill-workshop): honor pending approval for tool suggestions [AI] (#78516)
* fix: honor pending skill workshop approvals

* addressing review-skill

* addressing codex review

* addressing codex review

* fix: require approval before skill workshop apply

* docs: add changelog entry for PR merge
2026-05-07 13:27:32 +05:30
Peter Steinberger
1ef85c7d4c test: make suites safe without isolation (#78834)
* test: make suites safe without isolation

* fix: narrow auth profile credential types

* test: inject channel module loader factory locally
2026-05-07 08:43:29 +01:00
Val Alexander
9ffe290a17 fix(chat): decode native thinking metadata
Decode gateway-provided thinking metadata for native iOS/macOS chat picker options, preserving extended and legacy thinking levels without leaking default-model options across sessions.\n\nVerification:\n- swift test --package-path apps/shared/OpenClawKit --filter ChatViewModelTests --no-parallel\n- swift test --package-path apps/macos --filter WebChatSwiftUISmokeTests --no-parallel\n- pnpm lint:swift\n- pnpm check:changed\n\nFollow-up maintainer fix for #40878 review feedback.
2026-05-07 02:39:01 -05:00
Val Alexander
62ccd8b644 Fix model and tool normalization regressions
Summary:
- Fix model and tool normalization regressions, including explicit tool-policy grants for messaging profile warnings.
- Keep Codex and Microsoft Foundry auth handling compatible with aws-sdk auth profile modes after rebasing onto current main.

Verification:
- pnpm test src/agents/pi-tools.policy.test.ts
- pnpm tsgo:extensions
- pnpm tsgo:extensions:test
- pnpm test extensions/codex/src/app-server/auth-bridge.test.ts extensions/microsoft-foundry/index.test.ts
- pnpm test:extensions:package-boundary
- pnpm lint --threads=8
- git diff --check
- GitHub PR checks green on 4ad136106b
2026-05-07 02:29:28 -05:00
Val Alexander
d4e04f33a6 fix(sessions): retire stale direct dm rows after dmscope changes
Summary:
- Add explicit sessions cleanup --fix-dm-scope handling for stale direct-DM rows after session.dmScope returns to main.
- Preserve removed-row transcripts as deleted archives and expose the option through CLI, Gateway RPC, protocol schema, generated Swift mirrors, docs, tests, and changelog.
- Fixes #47561 and #45554.

Verification:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/cli/sessions.md docs/concepts/session.md src/config/sessions/cleanup-service.ts src/commands/sessions-cleanup.ts src/cli/program/register.status-health-sessions.ts src/gateway/protocol/schema/sessions.ts src/gateway/server-methods/sessions.ts src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts
- git diff --check origin/main...HEAD
- pnpm protocol:check
- pnpm exec oxlint src/config/sessions/cleanup-service.ts src/commands/sessions-cleanup.ts src/cli/program/register.status-health-sessions.ts src/gateway/protocol/schema/sessions.ts src/gateway/server-methods/sessions.ts src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts
- pnpm test src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts src/gateway/server.sessions.store-rpc.test.ts
- pnpm changed:lanes --json

Security:
- No new network, credential, process execution, dependency, or permission surface. Cleanup is explicit operator-invoked local session-store repair.

CI note:
- Exact-head CI failures match current main at 2e78fc57af in unrelated extensions/codex and extensions/microsoft-foundry type checks, outside this PR diff. No required checks are reported for this branch.
2026-05-07 02:16:46 -05:00
Peter Steinberger
2e78fc57af fix: accept aws-sdk auth profiles 2026-05-07 08:09:55 +01:00
Peter Steinberger
4721ca8e45 chore: update Bedrock provider lockfile 2026-05-07 07:57:48 +01:00
Peter Steinberger
c018d8405b fix: refresh Bedrock profile credentials live 2026-05-07 07:57:48 +01:00
Vincent Koc
a35067f872 fix(media): avoid provider listing for exact media defaults 2026-05-06 23:44:34 -07:00
Peter Steinberger
64bbe96d88 fix(media): resolve slash-containing generation model overrides 2026-05-07 07:35:59 +01:00
Peter Steinberger
10341c6158 fix(llm-task): resolve model aliases before dispatch 2026-05-07 07:35:59 +01:00
Peter Steinberger
42ecd5d95e fix(acpx): harden session lifecycle cleanup
Harden ACPX process cleanup with lease-backed ownership verification, startup orphan reaping, reusable cancel semantics, and spawned-session visibility fixes.
2026-05-07 07:30:37 +01:00
Kevin Lin
5b9672b4bb fix: surface cron model rejection diagnostics
Fixes #78597
2026-05-06 23:28:02 -07:00
Vincent Koc
a4b8cc307c docs: refresh plugin sdk api baseline hash 2026-05-06 23:16:24 -07:00
NVIDIAN
a2efabf4c9 fix(whatsapp): dedupe captioned MEDIA auto-replies (#78770)
* fix(whatsapp): dedupe captioned MEDIA auto-replies

* docs: note whatsapp media directive dedupe

---------

Co-authored-by: Marcus Castro <mcaxtr@openclaw.ai>
2026-05-07 03:15:19 -03:00
Vincent Koc
7dc597b921 docs: refresh config baseline hash 2026-05-06 23:10:46 -07:00
Peter Steinberger
a428568157 fix(gemini): gate thought-signature replay trust 2026-05-07 07:08:54 +01:00
Vincent Koc
5b34805895 test(agents): remove unused bundle snapshot variable 2026-05-06 22:52:32 -07:00
Shakker
c233e813a5 docs: clarify provider env metadata reuse 2026-05-07 06:48:13 +01:00
Shakker
835b884606 fix: guard provider env metadata reuse 2026-05-07 06:48:13 +01:00
Shakker
3a718ed491 docs: clarify metadata discovery reuse 2026-05-07 06:48:13 +01:00
Shakker
a7cc9e8a56 fix: require default discovery for metadata reuse 2026-05-07 06:48:13 +01:00
Shakker
917ccde7bf docs: clarify load path metadata reuse 2026-05-07 06:48:13 +01:00
Shakker
ee7da91346 fix: guard metadata reuse on load paths 2026-05-07 06:48:13 +01:00
Shakker
fb2f3fbb08 docs: clarify metadata reuse changelog 2026-05-07 06:48:13 +01:00
Shakker
0caa8e22d7 fix: thread registry model workspace 2026-05-07 06:48:13 +01:00
Shakker
156068a3cf fix: keep secret target cache unscoped 2026-05-07 06:48:13 +01:00
Peter Steinberger
5aefe6abd6 feat: stream elevenlabs tts into discord voice 2026-05-07 06:47:31 +01:00
Peter Steinberger
85b914a4e1 fix(model): repair provider replay edge cases 2026-05-07 06:41:59 +01:00
Val Alexander
a8d8d49ab8 fix(ui): label inherited thinking overrides
Closes #77581.

## Summary

- Add a shared thinking-label formatter for inherited vs explicit reasoning values.
- Show inherited thinking controls as `Inherited (Default: <Level>)` in chat and Sessions selectors.
- Preserve provider/model labels for explicit thinking overrides and document the inherited/default wording.

## Verification

- `pnpm docs:list`
- `pnpm tsgo:core` on current `origin/main` (`70defcc046`) -> passes
- `git diff --check`
- `pnpm exec oxfmt --check --threads=1 ui/src/ui/thinking-labels.ts ui/src/ui/chat/session-controls.ts ui/src/ui/views/chat.test.ts ui/src/ui/views/sessions.ts ui/src/ui/views/sessions.test.ts docs/tools/thinking.md CHANGELOG.md`
- `pnpm changed:lanes --json` -> `core`, `coreTests`, `docs`
- `pnpm test ui/src/ui/views/chat.test.ts ui/src/ui/views/sessions.test.ts src/gateway/server.sessions.list-changed.test.ts` -> 3 Vitest shards, 58 tests
- Testbox `pnpm check:changed` on `a906cb75ce` -> passes
- GitHub PR checks for #78176 on `a906cb75ce` -> no pending or failed jobs
2026-05-07 00:31:41 -05:00
Vincent Koc
0a3c7d34e6 test(discord): type voice capture stream mock 2026-05-06 22:30:36 -07:00
Vincent Koc
6e5ba8b047 fix(discord): smooth voice capture prompts 2026-05-06 22:30:36 -07:00
Vincent Koc
93747f6955 test(qa): add discord voice autojoin smoke 2026-05-06 22:30:36 -07:00
Vincent Koc
5a67b57b4b chore(changelog): note slack hot-path perf 2026-05-06 22:27:50 -07:00
Vincent Koc
7eaabc0b3b perf(slack): trim thread context allocation
(cherry picked from commit 0caa419f76)
2026-05-06 22:27:50 -07:00
Vincent Koc
ac74a92845 perf(slack): avoid redundant thread participation lookups
(cherry picked from commit 098a8b34b9)
2026-05-06 22:27:49 -07:00
Vincent Koc
b09033e587 perf(slack): cache stream recipient team lookup
(cherry picked from commit 8ce7cc8aae)
2026-05-06 22:27:49 -07:00
Vincent Koc
c0302512d4 perf(slack): reduce message hot-path overhead
(cherry picked from commit 9962328b7c)
2026-05-06 22:27:49 -07:00
Vincent Koc
70defcc046 fix(commands): audit explicit task records 2026-05-06 22:22:39 -07:00
Vincent Koc
60313069ba docs(changelog): move reply queue note to unreleased 2026-05-06 22:22:39 -07:00
Vincent Koc
f05f9f69d7 fix(agents): leave trusted media guard out of perf churn 2026-05-06 22:22:39 -07:00
Vincent Koc
f0a7b8a6a8 fix(core): satisfy perf bucket lint 2026-05-06 22:22:39 -07:00
Vincent Koc
42cddcae0a fix(agents): keep transcript repair tool names typed 2026-05-06 22:22:39 -07:00
Vincent Koc
8a23485472 fix(reply): preserve queue metadata after perf cherry-picks 2026-05-06 22:22:39 -07:00
Vincent Koc
eee7307891 perf(core): trim reply helper churn 2026-05-06 22:22:39 -07:00
Vincent Koc
468c6a0101 perf(core): trim reply and agent allocation churn 2026-05-06 22:22:39 -07:00
Vincent Koc
8bff73cfb0 perf(core): reduce queue head churn 2026-05-06 22:22:39 -07:00
Vincent Koc
16b0a6202c perf(reply): avoid queue churn in dedupe paths 2026-05-06 22:22:39 -07:00
Vincent Koc
e2d5e1b38d fix(plugins): expose config to transport normalization 2026-05-06 22:20:24 -07:00
Peter Steinberger
25f16f8fe6 fix: preserve cli oauth session continuity 2026-05-07 06:10:19 +01:00
Shakker
597dcb15c0 docs: record dashboard metadata scan reduction 2026-05-07 06:10:05 +01:00
Shakker
111cef04ca perf: reuse metadata for bundle settings 2026-05-07 06:10:05 +01:00
Shakker
fb49bcaf21 perf: reuse metadata for auth lookups 2026-05-07 06:10:05 +01:00
Shakker
6cc4323699 perf: reuse activation metadata registry 2026-05-07 06:10:05 +01:00
Shakker
e8efb7339e perf: reuse metadata across gateway runtime config 2026-05-07 06:10:05 +01:00
Shakker
1235f7f981 perf: reuse compatible auto-enable metadata 2026-05-07 06:10:05 +01:00
Shakker
5f60479f18 fix: scope async model runtime hooks 2026-05-07 06:10:05 +01:00
Vincent Koc
9910cdb7a9 test(openai): retry stalled websocket reasoning turn 2026-05-06 22:06:07 -07:00
Peter Steinberger
0597e8a065 test: align task audit fixture with lost-task projection 2026-05-07 06:05:24 +01:00
Peter Steinberger
96b7d9e6d8 fix: preserve mantis recordings on record errors (#78768) 2026-05-07 06:05:24 +01:00
Peter Steinberger
9f7abf9e3a build: refresh plugin sdk api baseline 2026-05-07 06:05:24 +01:00
Peter Steinberger
f65e357e00 test: fix discord external output mock typing 2026-05-07 06:05:24 +01:00
Peter Steinberger
252a76d25c refactor: stage external output writes through fs-safe 2026-05-07 06:05:24 +01:00
Peter Steinberger
759965a316 docs: explain slow nvidia custom providers 2026-05-07 05:52:12 +01:00
Peter Steinberger
2b4b60b551 fix: label claude cli oauth status 2026-05-07 05:52:12 +01:00
Peter Steinberger
c22f414c69 fix(codex): keep app-server alive after turn activity
Co-authored-by: Rubén Cuevas <4742789+rubencu@users.noreply.github.com>
2026-05-07 05:51:16 +01:00
Peter Steinberger
ab8166b380 docs(changelog): note model bug fixes 2026-05-07 05:48:35 +01:00
Peter Steinberger
610e882dbf fix(openrouter): canonicalize auto selector refs 2026-05-07 05:48:14 +01:00
Peter Steinberger
32c1356926 fix(cli): normalize heic model-run files 2026-05-07 05:48:14 +01:00
Peter Steinberger
ea116ca36e fix(openai): honor embedding output dimensions 2026-05-07 05:48:14 +01:00
Peter Steinberger
f37fba8d5a fix(anthropic): reject uppercase dynamic model ids 2026-05-07 05:48:14 +01:00
Peter Steinberger
1c2832526f fix: prevent discord voice self-feedback 2026-05-07 05:37:17 +01:00
Peter Steinberger
6009b86f0d fix: bound stale task reload blockers 2026-05-07 05:25:54 +01:00
Vincent Koc
b680360fde test(browser): allow fs-safe download staging 2026-05-06 21:11:18 -07:00
Vincent Koc
dddd9cb3b6 test(browser): use existing outside trash path 2026-05-06 21:03:06 -07:00
Vincent Koc
b8545d069e fix(memory-wiki): reserve fs-safe temp filename space 2026-05-06 21:02:05 -07:00
Vincent Koc
0c4111de9d test(browser): use real trash fixture paths 2026-05-06 21:01:53 -07:00
Eden
fcdfa30703 fix(whatsapp): resolve outbound PN to LID via auth-dir forward mapping (#74925)
Merged via squash.

Prepared head SHA: 5f51cb7cb3
Co-authored-by: edenfunf <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-05-07 00:49:53 -03:00
Peter Steinberger
a4d7206558 fix(discord): audit voice channel permissions 2026-05-07 04:47:35 +01:00
Vincent Koc
db82380819 test(telegram): satisfy reaction guard lint 2026-05-06 20:47:24 -07:00
Vincent Koc
88f22b34ea test(telegram): type reaction body mock 2026-05-06 20:47:24 -07:00
Vincent Koc
96c9368f4b test(telegram): type reaction guard mock 2026-05-06 20:47:24 -07:00
Vincent Koc
c4b5fed025 fix(telegram): narrow lazy status reaction config 2026-05-06 20:47:24 -07:00
Vincent Koc
8a5170d1d9 test(telegram): cover message context perf guards 2026-05-06 20:47:24 -07:00
Vincent Koc
91e324377c perf(telegram): reuse ack reaction gate result
(cherry picked from commit ae3d2816c7)
2026-05-06 20:47:24 -07:00
Vincent Koc
f71b702387 perf(telegram): defer status reaction variant setup
(cherry picked from commit 902c7cada7)
2026-05-06 20:47:24 -07:00
Vincent Koc
e80f67e372 perf(telegram): skip non-forum topic cache setup
(cherry picked from commit c0f79b8814)
2026-05-06 20:47:24 -07:00
Vincent Koc
a846b577a5 test(live): stabilize gateway timeout assertion 2026-05-06 20:39:52 -07:00
Neerav Makwana
447182a852 fix(telegram): avoid fallback after message tool send (#78726) (thanks @neeravmakwana)
* telegram: correlate message-tool sends with inbound turn (#78685)

Register the active Telegram inbound SessionKey/outbound peer while dispatching,
and mark inbound lane delivery when deliverOutbound emits a matching telegram
message:sent success. Prevents rewritten silent-reply fillers after visible
tool-routed replies with an empty final.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(telegram): track message action delivery

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-05-07 09:09:36 +05:30
Peter Steinberger
92284bc460 fix(agents): clean subagent fallback scaffolding (#78700)
* fix(agents): clean subagent completion fallback scaffolding

* refactor(agents): use prompt data blocks for child results

* fix(agents): satisfy sanitizer lint

* refactor(agents): remove raw subagent completion fallback
2026-05-07 04:30:04 +01:00
Peter Steinberger
58fa23b4a2 test: align fs-safe dependency expectations 2026-05-07 04:16:13 +01:00
sallyom
a859638cc2 fix: preserve node exec approvals for control ui
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-06 22:59:53 -04:00
Peter Steinberger
f66a2dc41d ci: run channel contract shards on blacksmith 2026-05-07 03:56:53 +01:00
Sarah Fortune
7d5d01b4f9 chore(deps): bump @openclaw/fs-safe pin to 3412e03 (#78670)
Pulls in 26 commits since the previous pin (3c50873):

- fix(workspace): add packages field so pnpm prepare succeeds
  (openclaw/fs-safe#10) — unblocks fresh installs that were failing
  with ERR_PNPM_INVALID_WORKSPACE_CONFIGURATION during the prepare step
  pnpm runs inside the github-hosted dep tarball.
- Filesystem boundary-guard hardening: centralized boundary primitives,
  guarded fallback handles, prune/trash race fixes, durable queue id
  validation, archive staged-merge fixes, public path mode preservation.
- json: avoid copy fallback symlink writes.
- temp: keep helpers in private dirs; preserve workspace leaf filename
  contract.

Verification:
- corepack pnpm install — clean install, no prepare error.
- pnpm openclaw setup — wrote ~/.openclaw/openclaw.json, workspace, and
  sessions dirs.
- pnpm test src/infra/{fs-safe,boundary-file-read,fs-safe-defaults,
  fs-safe-import-boundary}.test.ts — 37/37 passed.
2026-05-06 19:47:14 -07:00
Chunyue Wang
42a32298f9 fix(plugins): forward setChannelRuntime from non-bundled external setup entries (#77799)
Merged via squash.

Prepared head SHA: 7b7676be0d
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Reviewed-by: @openperf
2026-05-07 10:33:17 +08:00
Arnab Saha
1c331a814a fix(subagents): honor archiveAfterMinutes for session-mode reaping (#78263)
Merged via squash.

Prepared head SHA: b415467008
Co-authored-by: arniesaha <3646287+arniesaha@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-06 19:24:09 -07:00
Peter Steinberger
f2458d8828 ci: isolate prompt snapshot check 2026-05-07 03:07:58 +01:00
sallyom
bf2511098f fix: persist rotated gateway session files
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-06 21:41:03 -04:00
Peter Steinberger
69d446d178 test(telegram): lock draft finalization ordering
Add regression coverage for Telegram draft-lane final ordering and shared draft clear-vs-stop lifecycle semantics.\n\nVerification:\n- pnpm test src/channels/draft-stream-controls.test.ts extensions/telegram/src/bot-message-dispatch.test.ts -- --reporter=verbose\n- pnpm exec oxfmt --check --threads=1 src/channels/draft-stream-controls.test.ts extensions/telegram/src/bot-message-dispatch.test.ts\n- git diff --check\n- OPENCLAW_TESTBOX=0 pnpm check:changed\n- exact-tree CI previously passed on 1f822d7c22
2026-05-07 02:12:07 +01:00
Peter Steinberger
0e330c3fa0 Revert "test(telegram): lock draft finalization ordering"
This reverts commit 1f822d7c22.
2026-05-07 02:08:13 +01:00
Peter Steinberger
1f822d7c22 test(telegram): lock draft finalization ordering 2026-05-07 01:58:00 +01:00
Peter Steinberger
d3fc1985fe ci: keep runner fallback label specific 2026-05-07 01:45:20 +01:00
Peter Steinberger
a8801350d8 docs: clarify planned monthly support lines 2026-05-07 01:42:20 +01:00
Peter Steinberger
13770167a0 ci: split core runtime cron shard 2026-05-07 01:41:53 +01:00
NVIDIAN
440111ff6f fix(telegram): keep polling watchdog on getUpdates liveness (#78646) 2026-05-07 01:40:55 +01:00
Peter Steinberger
25343f3242 ci: scope prompt snapshot support tests 2026-05-07 01:39:13 +01:00
Peter Steinberger
7cab067bce ci: scope prompt snapshot boundary check 2026-05-07 01:31:50 +01:00
Peter Steinberger
b7d0d92600 fix(agents): cap live exec update payloads 2026-05-07 01:28:48 +01:00
Sally O'Malley
a74894a954 fix(agents): fail fast on session lock fallback (#78633)
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-06 20:22:47 -04:00
Sally O'Malley
20c34b8c0f fix(cron): preflight implicit announce targets (#78644)
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-06 20:21:20 -04:00
Peter Steinberger
8cffc5a2f4 ci: fall back by runner family 2026-05-07 01:13:35 +01:00
Peter Steinberger
c53f63ccb8 ci: fix runner telemetry lint 2026-05-07 01:10:32 +01:00
Peter Steinberger
372e270871 fix(delivery): require outbound send result for success 2026-05-07 01:04:50 +01:00
Peter Steinberger
b6ae0b83a6 fix(telegram): honor access group allowlists 2026-05-07 01:04:49 +01:00
Peter Steinberger
c3853611ee ci: add runner fallback timing telemetry 2026-05-07 01:04:22 +01:00
Vincent Koc
8934095c82 changelog: credit @sjf for #78659 2026-05-06 16:52:04 -07:00
Sarah Fortune
5ff283cfbb fix(cli/completion): guard shell profile source line with file-exists check (#78659) 2026-05-06 16:48:26 -07:00
Vincent Koc
b2368e1040 ci(crabbox): enable owned capacity hints 2026-05-06 16:40:50 -07:00
Vincent Koc
d3cfc5fd6a ci(crabbox): default owned aws to standard 2026-05-06 16:40:50 -07:00
Vincent Koc
75f7f30209 docs(ci): document blacksmith outage fallback 2026-05-06 16:40:50 -07:00
Vincent Koc
6c9a848dd3 docs(ci): reduce owned aws pressure 2026-05-06 16:40:50 -07:00
Vincent Koc
120eb3426a test(e2e): serialize default e2e runner 2026-05-06 16:27:03 -07:00
Vincent Koc
14336e3325 test(agents): isolate client tool policy hook e2e 2026-05-06 16:06:49 -07:00
Peter Steinberger
33b112ad31 docs: clarify sub-agent security boundaries 2026-05-07 00:01:36 +01:00
Shakker
e66edcc8b9 perf: reuse auto-enable manifest registry 2026-05-06 23:39:37 +01:00
Kevin Lin
6aafdf121a fix(cron): repair bad persisted model sentinels (#78641)
* fix(cron): repair bad persisted model sentinels

* test(cron): relax model preservation assertion
2026-05-06 15:31:21 -07:00
Josh Avant
5572ee1a1a fix gateway optional plugin startup (#78642) 2026-05-06 17:28:31 -05:00
Josh Avant
3ee7c02bca fix(agents): bound live exec output events (#78645)
* fix gateway exec output starvation

* docs changelog for exec output fix
2026-05-06 17:28:26 -05:00
Vincent Koc
2ab74e9ef7 fix(deps): pin fs-safe with full git sha 2026-05-06 15:22:59 -07:00
pashpashpash
d05415d603 docs: remind contributors to redact proof evidence (#78630) 2026-05-07 06:53:01 +09:00
Vincent Koc
93579a8a42 test(agents): refresh codex group prompt snapshot 2026-05-06 14:51:59 -07:00
adzendo
ac43135984 fix: clamp compaction max_tokens to model output limit (#54392)
Merged via squash.

Prepared head SHA: 8a888213ed
Co-authored-by: adzendo <246828680+adzendo@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-06 14:42:25 -07:00
Vincent Koc
6b97f577ed changelog: credit @Patrick-Erichsen for #78625 2026-05-06 14:35:12 -07:00
brokemac79
609a5d70a5 [AI-assisted] fix(agents): invalidate context engine cache (#78163)
Merged via squash.

Prepared head SHA: 6ed3add797
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-06 14:33:51 -07:00
Sally O'Malley
3be4251f21 fix(deps): bump basic-ftp (#78637)
* fix(deps): bump basic-ftp

Signed-off-by: sallyom <somalley@redhat.com>

* docs: note basic-ftp advisory fix

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
2026-05-06 17:31:12 -04:00
Patrick Erichsen
51356620e9 fix(discord): parse provider-prefixed channel targets (#78625)
* fix(discord): parse provider-prefixed channel targets

* fix(discord): resolve allowlisted numeric dm targets
2026-05-06 14:26:54 -07:00
Vincent Koc
eb3de95025 chore(plugin-sdk): update api baseline 2026-05-06 14:16:35 -07:00
Vincent Koc
0f4f7e32cb test(live): type provider scoped model loading 2026-05-06 14:08:32 -07:00
Vincent Koc
3a12a7a7e6 test(live): tolerate codex ask-back preflight refusal 2026-05-06 14:08:32 -07:00
Vincent Koc
6587832f25 test(live): read gateway provider models 2026-05-06 14:08:31 -07:00
Vincent Koc
d47497c99f test(live): guard provider scoped gateway path 2026-05-06 14:08:31 -07:00
Vincent Koc
beee6449a1 test(live): scope gateway model discovery 2026-05-06 14:08:31 -07:00
Vincent Koc
e921755762 test(live): load default priority models directly 2026-05-06 14:08:30 -07:00
Vincent Koc
f5746bb278 test(live): cap default model discovery 2026-05-06 14:08:30 -07:00
Vincent Koc
90f7134535 test(docker): disable live model transform cache 2026-05-06 14:08:30 -07:00
Vincent Koc
1ed1185974 test(live): bound model auth discovery 2026-05-06 14:08:29 -07:00
Vincent Koc
cfa2b90752 test(live): defer model normalization until after filtering 2026-05-06 14:08:29 -07:00
Vincent Koc
78b252682b test(docker): ignore artifacts in live source staging 2026-05-06 14:08:29 -07:00
Vincent Koc
445dda54f1 fix(docker): preserve functional image dependencies 2026-05-06 14:08:28 -07:00
Vincent Koc
ffd212ca43 fix(docker): avoid package node_modules copy conflicts 2026-05-06 14:08:28 -07:00
Peter Steinberger
3e8b5b4ee7 fix: skip npm uninstall when managed root is absent 2026-05-06 22:06:59 +01:00
Peter Steinberger
1d685304c3 ci: skip Testbox workflow checks for draft PRs 2026-05-06 22:06:59 +01:00
Peter Steinberger
9dd5014cf3 ci: skip CodeQL critical selector for draft PRs 2026-05-06 22:06:59 +01:00
Peter Steinberger
858038320d fix: clean stale managed npm plugin deps on uninstall 2026-05-06 22:06:59 +01:00
Peter Steinberger
4647400c22 fix(discord): default to progress previews 2026-05-06 22:02:52 +01:00
Peter Steinberger
63dc7321ef docs(imessage): deprecate bluebubbles for new setups 2026-05-06 21:58:21 +01:00
Shakker
f011d6bc0a Fix repeated Codex native approval prompts after allow-always (#78234)
* fix: reuse codex native approvals

* fix: scope native approval reuse by session

* fix: let codex guardian own native permission approvals

* fix: refresh plugin approval protocol models

---------

Co-authored-by: pashpashpash <nik@vault77.ai>
2026-05-07 05:55:38 +09:00
Vincent Koc
97b07eaeaf changelog: add Discord groups quiet-when-addressed and link-embed entries 2026-05-06 13:34:02 -07:00
Peter Steinberger
9c7c0ae891 fix(discord): keep agents quiet when others are addressed (#78615)
* fix(discord): keep agents quiet when others are addressed

* fix(groups): tighten addressed-elsewhere prompt
2026-05-06 21:29:42 +01:00
Peter Steinberger
197edaa33d fix(discord): prompt agents to suppress link embeds (#78614)
* fix(discord): prompt agents to suppress link embeds

* fix(discord): tighten link embed prompt
2026-05-06 21:27:03 +01:00
Peter Steinberger
3baf4de2cf test(commands): skip temp home cleanup in agent suite 2026-05-06 21:25:48 +01:00
Peter Steinberger
0f9f956bbd perf(infra): skip usage auth discovery on default path 2026-05-06 21:16:28 +01:00
Peter Steinberger
4f73cd23b7 test(plugins): cache bundled metadata fixture scans 2026-05-06 21:11:09 +01:00
Peter Steinberger
5d3d1f8718 docs: document hourly translation debounce 2026-05-06 21:10:21 +01:00
Peter Steinberger
a74b459f7a test(plugins): reuse conversation binding imports 2026-05-06 21:06:03 +01:00
Peter Steinberger
afc46e9233 docs: document internal-only translation skip 2026-05-06 20:52:26 +01:00
Peter Steinberger
0eeb19f300 test(plugins): parallelize publishable package scans 2026-05-06 20:51:01 +01:00
Peter Steinberger
cd06bab466 docs: note internal i18n docs are not translated 2026-05-06 20:46:10 +01:00
Peter Steinberger
9dca1ff672 docs: clarify post-deploy smoke 2026-05-06 20:45:30 +01:00
Peter Steinberger
b0bc29ea6a docs: clarify translation deploy dispatch 2026-05-06 20:44:14 +01:00
Peter Steinberger
a357045cf0 docs: document aggregate translation workflow 2026-05-06 20:42:37 +01:00
Peter Steinberger
66f3fac34a docs: update i18n workflow notes 2026-05-06 20:24:56 +01:00
Peter Steinberger
86e0066169 ci: dispatch aggregate docs translations 2026-05-06 20:24:03 +01:00
Vincent Koc
aa9247e0ce fix(plugins): skip managed npm peer resolution (#78348) 2026-05-06 12:17:34 -07:00
Kevin Lin
7175b1b5c6 fix(cron): repair stale future next-run slots (#78272)
* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots
2026-05-06 12:05:59 -07:00
Peter Steinberger
9c0b84eaa4 test(memory): tighten qmd manager wait polling 2026-05-06 19:58:29 +01:00
Josh Lehman
b22c8998ca fix(doctor): discover load-path plugin contracts (#77477)
Merged via squash.

Prepared head SHA: d428fd47f5
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-06 11:50:47 -07:00
Peter Steinberger
16922649d2 perf(doctor): skip plugin scans for unrelated session state 2026-05-06 19:38:45 +01:00
Vincent Koc
1ab00c4469 test(codex): remove unused runtime plan helper 2026-05-06 11:36:02 -07:00
Vincent Koc
e43ae8e8cd fix(googlechat): import action name contract type 2026-05-06 11:36:02 -07:00
Peter Steinberger
aed96bb60c perf(status): skip unused status pricing lookups 2026-05-06 19:32:20 +01:00
Peter Steinberger
71a6260034 fix(googlechat): remove duplicate channel import 2026-05-06 19:24:19 +01:00
Peter Steinberger
90b69cac02 test(perf): slim channel directory contracts 2026-05-06 19:18:53 +01:00
Peter Steinberger
2d97dcebb5 perf(config): skip bootstrap for false env channel probes 2026-05-06 19:06:02 +01:00
Peter Steinberger
11a0b1248d docs: clarify Codex OAuth hotfix recovery 2026-05-06 19:01:41 +01:00
Peter Steinberger
2daf3d332f fix(infra): avoid redundant usage summary refresh 2026-05-06 18:42:32 +01:00
Peter Steinberger
3c7a641b8d test(perf): run memory lane on thread pool 2026-05-06 18:19:15 +01:00
Peter Steinberger
a2e77c101f test(perf): run contract lanes on thread pool 2026-05-06 18:15:26 +01:00
Peter Steinberger
3117558570 test(perf): run command lane on thread pool 2026-05-06 18:13:30 +01:00
Vincent Koc
5a4b79d419 changelog: add provider auth alias scope fix entry 2026-05-06 10:12:48 -07:00
Shakker
bc97182d71 fix: preserve provider auth alias scope 2026-05-06 17:33:46 +01:00
Shakker
3dffef651b fix: reuse turn plugin metadata snapshot 2026-05-06 17:33:46 +01:00
Shakker
c795a1a8ef fix: propagate diagnostics timeline phase 2026-05-06 17:33:46 +01:00
Shakker
61223a74a4 chore: add coarse agent turn timeline spans 2026-05-06 17:33:46 +01:00
Vincent Koc
462b96b33f docs(channels/msteams): remove ampersands from headings 2026-05-06 09:31:55 -07:00
Vincent Koc
74ec956e42 docs: sentence-case 4 stray Title Case headings 2026-05-06 09:22:35 -07:00
Vincent Koc
e2898eaa88 docs(providers): remove duplicate H1s in models and kilocode 2026-05-06 09:12:01 -07:00
Vincent Koc
204971f2a9 docs: remove duplicate body H1s + sentence-case headings across 10 pages 2026-05-06 09:06:11 -07:00
Peter Steinberger
c738539b1e fix(ci): reject mixed-case staging debris before pack 2026-05-06 17:02:23 +01:00
Vincent Koc
09f8624b1a docs(plugins/agent-tools): remove duplicate H1 2026-05-06 09:00:20 -07:00
Peter Steinberger
4c177bbe65 test(perf): fold duplicate compaction fallback coverage 2026-05-06 16:57:12 +01:00
Vincent Koc
ec8283e3e5 docs: typography hygiene across 9 pages (cli/channels) 2026-05-06 08:55:00 -07:00
Vincent Koc
d70e06334e fix(ci): keep cross-os matrix resolution dependency-free 2026-05-06 08:53:41 -07:00
Vincent Koc
0b65f0c108 test(auto-reply): mark source delivery cases as non-command 2026-05-06 08:53:40 -07:00
Vincent Koc
d648673b31 docs: typography hygiene across 6 pages (gateway/cli/debug) 2026-05-06 08:49:27 -07:00
Peter Steinberger
99b17263a1 perf: avoid duplicate provider policy artifact misses 2026-05-06 16:17:58 +01:00
Peter Steinberger
167e43345a test(perf): pin runtime metadata fixtures 2026-05-06 16:09:13 +01:00
Peter Steinberger
17a7bc7352 test(perf): pin model fallback probe metadata 2026-05-06 16:05:47 +01:00
Peter Steinberger
589f6685e6 test(perf): pin session status metadata fixtures 2026-05-06 16:04:43 +01:00
Peter Steinberger
3a9aec120c test(perf): pin model selection manifest fixtures 2026-05-06 15:57:49 +01:00
Shakker
96f80fa3ff fix: normalize debug proxy fetch headers 2026-05-06 15:40:33 +01:00
Shakker
79f21a4442 fix: normalize symbolic fetch headers 2026-05-06 15:40:33 +01:00
Peter Steinberger
74b1fdce2c test(perf): trim context engine overflow fixtures 2026-05-06 15:37:53 +01:00
Peter Steinberger
2e10ffe813 test(perf): bound google meet retry waits 2026-05-06 15:25:23 +01:00
Jacob Tomlinson
298cae67bb fix(mattermost): collect setup URL in wizard
Fixes #76670.\n\nSummary:\n- Collect the Mattermost bot token and server URL as separate wizard patches so validation does not run before the URL is entered.\n- Preserve non-interactive Mattermost setup validation for explicit --bot-token + --http-url flows.\n- Add a regression test and changelog entry.\n\nVerification:\n- Reporter manually verified setup against a real Mattermost server.\n- pnpm test extensions/mattermost\n- pnpm tsgo:extensions\n- pnpm tsgo:extensions:test\n- pnpm exec oxfmt --check --threads=1 extensions/mattermost/src/setup-core.ts extensions/mattermost/src/setup-surface.ts extensions/mattermost/src/setup.test.ts\n- git diff --check upstream/main...HEAD
2026-05-06 15:23:34 +01:00
Peter Steinberger
fd5352bc18 test(perf): pin gateway session list model fixtures 2026-05-06 15:13:34 +01:00
Peter Steinberger
9324af7d46 test(perf): trim gateway session list fixtures 2026-05-06 15:05:27 +01:00
Peter Steinberger
b70a2451f8 test(perf): avoid compaction hook plugin metadata scans 2026-05-06 14:50:49 +01:00
Alex Knight
c58ccae727 fix(cron): allow restricted self introspection (#78403)
* fix(cron): allow restricted self introspection

* fix(cron): tighten self-scoped introspection

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 23:38:20 +10:00
Peter Steinberger
ed6e9ae0a6 test(perf): avoid spawn workspace plugin metadata scans 2026-05-06 14:29:03 +01:00
Alex Knight
77480212c7 fix(update): preserve pnpm custom global root (#78393)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 22:46:21 +10:00
Peter Steinberger
2d5df741f5 test(perf): avoid codex failure runtime plan setup 2026-05-06 13:43:51 +01:00
the sun gif man
d4b4660026 config: stop automatic writes and guard Nix mutators (#78047)
Keep startup-derived plugin enablement, gateway auth tokens, control UI origins, and owner-display secrets runtime-only instead of persisting them into openclaw.json.

Refuse config writers, mutating update/plugin lifecycle commands, and doctor repair/token generation in Nix mode with agent-first nix-openclaw guidance.

Verification:
- pnpm check
- pnpm build
- pnpm test -- src/config/io.write-config.test.ts src/config/mutate.test.ts src/config/io.owner-display-secret.test.ts src/gateway/server-startup-config.recovery.test.ts src/gateway/startup-auth.test.ts src/gateway/startup-control-ui-origins.test.ts src/cli/plugins-cli.install.test.ts src/cli/plugins-cli.policy.test.ts src/cli/plugins-cli.uninstall.test.ts src/cli/plugins-cli.update.test.ts src/cli/update-cli.test.ts src/auto-reply/reply/commands-plugins.install.test.ts src/auto-reply/reply/commands-plugins.test.ts src/commands/onboarding-plugin-install.test.ts src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts src/commands/doctor/shared/codex-route-warnings.test.ts src/commands/doctor/repair-sequencing.test.ts src/agents/auth-profile-runtime-contract.test.ts src/auto-reply/reply/agent-runner-execution.test.ts
- GitHub CI green on 05a2c71b90

Co-authored-by: Codex <noreply@openai.com>
2026-05-06 14:43:32 +02:00
Peter Steinberger
3317b79e5f test(perf): avoid codex hook runtime plan setup 2026-05-06 13:40:14 +01:00
simplyclever914
20906f56e2 fix: make conversation labels work with Codex (#78450)
Summary:
- The PR changes the shared conversation-label generator to send label instructions as `systemPrompt`, omit `temperature` for Codex simple completions, log error stop reasons, and add focused tests plus a changelog entry.
- Reproducibility: yes. Source reproduction is high-confidence: current main sends the prompt only inside user ... ple transport reads instructions from `context.systemPrompt` and only includes `temperature` when supplied.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: note Codex topic label fix

Validation:
- ClawSweeper review passed for head 9380907984.
- Required merge gates passed before the squash merge.

Prepared head SHA: 9380907984
Review: https://github.com/openclaw/openclaw/pull/78450#issuecomment-4387573775

Co-authored-by: Clever <clever@users.noreply.github.com>
2026-05-06 12:39:30 +00:00
Peter Steinberger
458ce2da94 test(perf): settle codex hook turn startup 2026-05-06 13:32:45 +01:00
Peter Steinberger
12a42bf3da test(perf): narrow codex trajectory import 2026-05-06 13:22:04 +01:00
Peter Steinberger
0bc83b0fdf test(perf): narrow codex harness test import 2026-05-06 13:16:09 +01:00
Peter Steinberger
14a113f7e5 test(perf): remove codex hook polling 2026-05-06 12:57:15 +01:00
Peter Steinberger
5d7878dff1 test(perf): narrow codex session key test 2026-05-06 12:45:38 +01:00
Ayaan Zaidi
855a7c7be7 docs(changelog): note web fetch timeout cleanup (#78439) 2026-05-06 17:11:48 +05:30
Ayaan Zaidi
2465217b23 fix(net): bound guarded fetch dispatcher cleanup 2026-05-06 17:11:48 +05:30
Ayaan Zaidi
b559fce7a1 test(net): cover stalled dispatcher close after fetch timeout 2026-05-06 17:11:48 +05:30
Jesse Merhi
1c42c77433 feat: add user input blocking lifecycle gates (#75035)
Summary:
- The PR adds a `before_agent_run` plugin hook with pass/block decisions, redacted blocked-turn persistence, diagnostics/docs/changelog updates, and focused runner, gateway, session, and plugin tests.
- Reproducibility: not applicable. as a feature PR rather than a current-main bug report. Current main lacks ` ... un`, while the PR head adds source coverage and copied live Gateway/WebChat log proof for the new behavior.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: trim before agent hook PR scope
- PR branch already contained follow-up commit before automerge: fix: keep before-agent blocks redacted
- PR branch already contained follow-up commit before automerge: fix: keep runtime context out of model prompt
- PR branch already contained follow-up commit before automerge: docs: refresh config baseline after rebase
- PR branch already contained follow-up commit before automerge: fix: align blocked turn clients with redacted content
- PR branch already contained follow-up commit before automerge: fix: remove out-of-scope client block UI changes

Validation:
- ClawSweeper review passed for head 767e46fde8.
- Required merge gates passed before the squash merge.

Prepared head SHA: 767e46fde8
Review: https://github.com/openclaw/openclaw/pull/75035#issuecomment-4351843275

Co-authored-by: Jesse Merhi <jessejmerhi@gmail.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-06 11:41:04 +00:00
Ayaan Zaidi
2915f45233 docs(changelog): add Telegram transcript PR attribution 2026-05-06 17:09:25 +05:30
Ayaan Zaidi
ba5f43b3ab docs(changelog): note Telegram transcript fix 2026-05-06 17:09:25 +05:30
Ayaan Zaidi
440cf63cca test(agent): cover Telegram transcript gap-fill 2026-05-06 17:09:25 +05:30
Ayaan Zaidi
f0c174607b fix(agent): persist visible embedded final replies 2026-05-06 17:09:25 +05:30
Peter Steinberger
c4537fa6c3 test(perf): shorten codex app-server hot test 2026-05-06 12:34:22 +01:00
Peter Steinberger
cc9f88e6e6 ci: fix release cross-os loader path 2026-05-06 12:20:56 +01:00
Peter Steinberger
fc1e2c505a fix(reply): preserve private group replies for text turns 2026-05-06 12:09:28 +01:00
Peter Steinberger
cf21cbafc4 ci: harden release validation harness checks 2026-05-06 12:08:45 +01:00
Peter Steinberger
9bcb56b45b test(perf): narrow runtime web tools state test 2026-05-06 11:54:13 +01:00
Mason Huang
bb25e48972 test(scripts): clean up temp dirs after each case (#78421) 2026-05-06 18:51:25 +08:00
Peter Steinberger
7af6c25aa5 ci: cap native MiniMax release live gateway lane 2026-05-06 11:47:54 +01:00
Peter Steinberger
8256b747be test(perf): narrow provider contract imports 2026-05-06 11:40:15 +01:00
Vincent Koc
86c4809a40 test(gateway): skip opencode acp image probe by default 2026-05-06 03:25:00 -07:00
Vincent Koc
4996153b6d test(docker): source cli backend live profile before auth check 2026-05-06 03:25:00 -07:00
Vincent Koc
2c7c19ac2d test(docker): use api-key auth for codex cli live lane 2026-05-06 03:25:00 -07:00
Vincent Koc
1df3850a17 test(docker): source testbox profile for codex npm plugin 2026-05-06 03:24:59 -07:00
Vincent Koc
3d46e2c366 test(docker): pass openai env to codex npm plugin lane 2026-05-06 03:24:59 -07:00
Vincent Koc
7fd7f6f355 fix(gateway): mark chat slash commands as text 2026-05-06 03:24:59 -07:00
Vincent Koc
e4b629c6d3 test(gateway): deliver codex bind commands 2026-05-06 03:24:59 -07:00
Vincent Koc
a2f1d1dfd8 fix(reply): keep text command replies visible 2026-05-06 03:24:59 -07:00
Vincent Koc
5e218b402f test(gateway): capture codex bind outbound replies 2026-05-06 03:24:58 -07:00
Vincent Koc
53423a2a7f test(gateway): disable heartbeat in codex bind live 2026-05-06 03:24:58 -07:00
Vincent Koc
2dc8748b59 test(gateway): accept compact codex status 2026-05-06 03:24:58 -07:00
Vincent Koc
6c7c0e559a test(docker): hydrate codex harness live profile 2026-05-06 03:24:58 -07:00
Vincent Koc
377c69773f test(docker): hydrate openwebui live profile 2026-05-06 03:24:58 -07:00
Vincent Koc
87f3501b91 test(docker): skip external auth sync in filtered gateway live 2026-05-06 03:24:58 -07:00
Vincent Koc
c686eda8f8 test(docker): bound live gateway setup 2026-05-06 03:24:57 -07:00
Vincent Koc
ff09f8022d test(docker): scope live gateway discovery 2026-05-06 03:24:57 -07:00
Vincent Koc
8a47c79826 test(docker): preserve live gateway heap 2026-05-06 03:24:57 -07:00
Vincent Koc
11f0aeeb62 test(docker): use matrix live gateway image 2026-05-06 03:24:57 -07:00
Vincent Koc
359c60948f test(docker): isolate live gateway profile plugins 2026-05-06 03:24:57 -07:00
Vincent Koc
dfb160db6d test(docker): widen live profile lane timeout 2026-05-06 03:24:56 -07:00
Vincent Koc
64ab50e42b fix(update): preserve plugin warning context 2026-05-06 03:24:56 -07:00
Vincent Koc
a3aa0a457f fix(test): harden docker live harness 2026-05-06 03:24:56 -07:00
Peter Steinberger
1d3efb7e9e test(perf): trim focused runtime contract imports 2026-05-06 11:23:49 +01:00
Mason Huang
cbba122cdd test(update-cli): cleanup temp dirs after each case (#78408) 2026-05-06 18:20:47 +08:00
Alex Knight
1c2915677b fix: recognize custom compaction conversation (#78390)
* fix: recognize custom compaction conversation

* fix: use branch fallback for compaction safeguard

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 20:13:31 +10:00
Peter Steinberger
9e7fd27577 ci: narrow MiniMax release live gateway lane 2026-05-06 11:11:21 +01:00
Vincent Koc
1fe15f2306 docs(logging): document bounded Talk log records 2026-05-06 03:02:02 -07:00
Vincent Koc
16321a27b6 fix(talk): add bounded lifecycle logging 2026-05-06 03:02:02 -07:00
Alex Knight
28e27ca5d1 fix(msteams): preserve proactive thread replies (#78387)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 20:01:18 +10:00
Alex Knight
fa445003b5 fix(acp): preserve streamed progress chunks (#78383)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 20:00:34 +10:00
Alex Knight
7a73b37f87 fix memory wiki empty related blocks (#78399)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 19:55:22 +10:00
Peter Steinberger
3323327f6b test(voice-call): wait for media stream readiness 2026-05-06 10:51:05 +01:00
Vincent Koc
5d557171b3 fix(plugins): apply npm overrides to managed roots (#78386) 2026-05-06 02:47:25 -07:00
Pavan Kumar Gondhi
b895c6d939 Gate Slack startup user allowlist resolution [AI] (#77898)
* fix: gate slack user allowlist resolution

* addressing codex review

* addressing ci

* docs: add changelog entry for PR merge
2026-05-06 15:16:55 +05:30
Peter Steinberger
ceaa56fb12 fix(release): stabilize final validation checks 2026-05-06 10:45:29 +01:00
Peter Steinberger
bf0f547632 fix(release): tolerate optional plugin beta tag mirror failure 2026-05-06 10:45:29 +01:00
Peter Steinberger
39b17310b6 ci: parallelize release publish workflows 2026-05-06 10:45:29 +01:00
Chunyue Wang
34b67c3f25 fix(web_search): skip redundant provider re-resolution for external Brave plugin
Guards the secondary resolveProviders call with `!allProviders.some(p => p.id === rawProvider)` so it only fires when the first pass genuinely missed the configured provider. Eliminates the spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and incorrect `providerSource: "none"` for external Brave plugin installs. Fixes #77676.
2026-05-06 17:45:20 +08:00
Peter Steinberger
8cb58813f2 test(openai): align codex modern model expectation 2026-05-06 10:42:06 +01:00
Conan-Scott
e437763246 fix(agents): deliver agent TTS audio when block streaming is off (#78355)
Summary:
- The branch changes non-streaming block reply delivery to direct-send all media-bearing block replies, updates reply-delivery/media-path regression tests, and adds a changelog entry.
- Reproducibility: yes. Current main's predicate and unit test show captioned media-bearing block replies are  ... sent when block streaming is disabled, and the PR body adds real Telegram after-fix proof for the TTS path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(agents): align direct media block delivery coverage

Validation:
- ClawSweeper review passed for head e9bb1314fe.
- Required merge gates passed before the squash merge.

Prepared head SHA: e9bb1314fe
Review: https://github.com/openclaw/openclaw/pull/78355#issuecomment-4386200162

Co-authored-by: Clawdbot <clawdbot@apilab.us>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-05-06 09:37:22 +00:00
Peter Steinberger
ffafa9008d test(agents): avoid provider runtime in fallback tests 2026-05-06 10:21:34 +01:00
Peter Steinberger
a24d5fe790 perf(config): avoid duplicate plugin auto-enable channel probes 2026-05-06 10:17:31 +01:00
Vincent Koc
34dc7f6ea6 Merge pull request #78378 from openclaw/fix/diagnostics-talk-prom
* commit '827e602d3a1bb726aaf68a02229a25ff3d848fc0':
  fix(diagnostics): include talk events in stability snapshots
  chore(plugin-sdk): refresh api baseline
  fix(diagnostics): export talk and recovery metrics
2026-05-06 02:03:19 -07:00
Vincent Koc
e2501b2d6d fix(diagnostics): export Talk metrics after SDK refactor
Adds bounded Talk lifecycle/audio diagnostics and session recovery metrics for OTEL, Prometheus, and stability snapshots after the Talk SDK/session refactor. Includes changelog/docs updates and Testbox/live proof.
2026-05-06 02:01:52 -07:00
Alex Knight
d9ffc1aa63 fix cron run binding route (#78373)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 18:57:32 +10:00
Vincent Koc
827e602d3a fix(diagnostics): include talk events in stability snapshots 2026-05-06 01:49:21 -07:00
Vincent Koc
8d9e7c8178 chore(plugin-sdk): refresh api baseline 2026-05-06 01:49:20 -07:00
Vincent Koc
aca844014f fix(diagnostics): export talk and recovery metrics 2026-05-06 01:48:07 -07:00
Peter Steinberger
0b88d6286c chore: bump version to 2026.5.6 2026-05-06 09:47:34 +01:00
Peter Steinberger
5cf55ed3f1 fix(openai): suppress stale Codex OAuth models 2026-05-06 09:38:07 +01:00
JC
85ded4d444 pdf: add Codex instructions for extraction fallback (#51329)
* Fix Codex PDF extraction fallback missing instructions

- add a Codex-specific systemPrompt on the PDF extraction fallback path
- keep non-Codex PDF fallback requests unchanged
- add regression coverage proving openai-codex-responses requests include instructions for PDF tool calls

* test: cover Codex text-only extraction fallback

- add regression coverage for the branch where PDF extraction includes images
  but the selected Codex model only accepts text input
- assert Codex-specific extraction instructions are still attached in that path

* test: fix extracted image mock shape

- add the required `type: "image"` field to the text-only fallback regression mock
- keep the new Codex coverage test aligned with PdfExtractedImage

* test: align Codex PDF fallback tests

* docs(changelog): note PDF Codex fallback fix

---------

Co-authored-by: Dr JCai <jingxiao.cai@gmail.com>
Co-authored-by: anyech <8743351+anyech@users.noreply.github.com>
2026-05-06 09:34:42 +01:00
Peter Steinberger
674c447264 ci: move additional checks to blacksmith 2026-05-06 09:33:43 +01:00
Peter Steinberger
ce8b0da9a2 test: slim secret runtime coverage 2026-05-06 09:33:28 +01:00
Alex Knight
ff655cb346 fix: preserve subagent task overrides (#78356)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-06 18:31:01 +10:00
Vincent Koc
0ddbf2e258 fix(plugins): keep managed npm mutations in legacy peer mode 2026-05-06 01:29:52 -07:00
Edionwheels
b902d86318 fix(cli): pass instructions for local openai-codex model probes (#76470)
* fix infer model run codex instructions

* docs changelog for codex model probe fix

* fix codex model probe instructions only

* docs: note codex model probe instruction shim

* chore: rerun proof gate

---------

Co-authored-by: Le LI <leli@LedeMacBook-Air.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-06 09:24:56 +01:00
Frank Yang
3e04755874 docs: add Frank Yang to maintainers 2026-05-06 16:19:19 +08:00
Peter Steinberger
a1b49c4b20 fix: stabilize google meet twilio joins 2026-05-06 09:18:20 +01:00
Peter Steinberger
2eaf8ad712 feat(plugins): support npm pack installs 2026-05-06 09:16:49 +01:00
Peter Steinberger
54e23b6d11 test: satisfy lint in optimized tests 2026-05-06 09:12:55 +01:00
Peter Steinberger
3fb1abcdcb test: isolate directory contract fixtures 2026-05-06 09:12:55 +01:00
Shubhankar Tripathy
9edeffc751 fix(codex/app-server): forward bootstrap into developerInstructions (#77372)
The OpenClaw workspace bootstrap block (SOUL.md, IDENTITY.md, USER.md,
TOOLS.md, BOOTSTRAP.md, MEMORY.md, HEARTBEAT.md) was only being merged into
Codex's config.instructions. The Codex app-server runtime overlay
consistently applies the explicit developerInstructions field, so persona
and style guidance present in the workspace was failing to shape Codex
behavior on session resume.

Build the workspace bootstrap block before finalizing developerInstructions
and join it into both:

- the baseline developerInstructions (initial assignment), and
- the context-engine developerInstructions (when context engine is active),
  preserving the existing config-engine projection addition.

The existing config.instructions merge stays intact, so the bootstrap now
reaches Codex through both paths and downstream hooks
(resolveAgentHarnessBeforePromptBuildResult) see what Codex will actually
receive. AGENTS.md remains excluded because Codex loads it natively.

Update the existing 'passes OpenClaw bootstrap files through ...' test to
also assert the developerInstructions field carries SOUL.md and the Codex
AGENTS.md substitution note while still excluding the native AGENTS.md
content.

Fixes #77363.
2026-05-06 09:09:59 +01:00
sliverp
af2719a7b9 docs(changelog): add entry for #78328 onboard stale channel plugin fallback 2026-05-06 16:01:32 +08:00
Sliverp
329580c64d fix(onboard): recover externalized channel plugin from stale config (#78328)
When a user's config has a stale `channels.<id>` entry (e.g. `appId`
or tokens left over from an earlier install) and the plugin is no
longer on disk -- for instance because the externalized npm package
was uninstalled or pruned during an upgrade -- `handleChannelChoice`
used to dead-end with "<channel> plugin not available." and leave
onboard stuck until the user manually deleted the config entry and
re-ran the CLI.

Two discovery paths are affected:

1. The `installedCatalogEntry` branch: when
   `loadScopedChannelPlugin` returns null but the catalog entry still
   carries `install.npmSpec`, fall back to
   `ensureChannelSetupPluginInstalled` with the same entry so onboard
   can reinstall the plugin from the official catalog.

2. The bundled-enable `else` branch: with a non-empty
   `channels.<id>` record, `isStaticallyChannelConfigured` drops the
   channel from `installableCatalogEntries`; if the plugin is also
   missing on disk (so it never enters `manifestInstalledIds`), both
   discovery buckets come back empty and the channel falls through to
   `enableBundledPluginForSetup`. Before delegating to that bundled
   path, consult the trusted catalog via
   `getTrustedChannelPluginCatalogEntry` and, if an `install.npmSpec`
   is available, drive the same catalog install flow used by a fresh
   pick of the channel.

Both new fallbacks re-apply the `resolveConfigDisabledHint` guard
that `enableBundledPluginForSetup` has always enforced, so an
operator-disabled channel (`plugins.entries.<id>.enabled === false`
or explicit `channels.<id>.enabled === false`) with a stale config
entry cannot be silently reinstalled or re-enabled through the
catalog path.

Both branches also keep their previous behavior when no catalog npm
spec is available (e.g. purely bundled channels), so this change is
a superset of the old flow rather than a replacement.

Affects all externalized channel plugins listed in the core
package's `files` exclusion (qqbot, bluebubbles, discord, whatsapp,
line, msteams, feishu, googlechat, nostr, zalo, zalouser,
synology-chat, tlon, twitch, and similar).
2026-05-06 15:55:16 +08:00
Edionwheels
58f81b0e04 fix(codex): honor OAuth contextTokens in native harness
Fixes #77858.

Co-authored-by: Edionwheels <267595845+lilesjtu@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-06 08:54:52 +01:00
Peter Steinberger
3915089a25 test: cache provider contract entries 2026-05-06 08:51:25 +01:00
Peter Steinberger
5969ac8ccf test: parallelize plugin package scan 2026-05-06 08:38:58 +01:00
Peter Steinberger
c5fcfa1b56 test: remove reload deferral wait 2026-05-06 08:34:17 +01:00
Ayaan Zaidi
3e0fcafb87 test(codex): use full runtime plan in app server tests 2026-05-06 13:03:54 +05:30
Ayaan Zaidi
6be5422fd6 fix(gateway): avoid plugin model resolution in session lists 2026-05-06 13:03:54 +05:30
Forge
ef517e1a54 Preserve session list model normalization 2026-05-06 13:03:54 +05:30
Forge
948375f494 Optimize session list model row resolution 2026-05-06 13:03:54 +05:30
Forge
8bfec5b9ac fix(sessions): fast-path qualified row model refs 2026-05-06 13:03:54 +05:30
Peter Steinberger
e59890eff0 test: speed up gateway cron history case 2026-05-06 08:31:28 +01:00
Vincent Koc
1a8a72e367 changelog: credit @keshavbotagent for #77949 2026-05-06 00:29:29 -07:00
Vincent Koc
8cc6638017 docs(cli): fix smart apostrophes in dns and health 2026-05-06 00:23:48 -07:00
keshavbotagent
3f210b10ce fix: show Codex tool progress in channel drafts (#77949)
Summary:
- Normalize Codex app-server dynamic and native tool activity into channel-visible tool progress.
- Keep Telegram message-tool-only progress drafts visible without duplicate dynamic item/tool lines.
- Preserve suppressed item progress while avoiding duplicate tool callbacks.

Verification:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.test.ts extensions/telegram/src/bot-message-dispatch.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/dispatch-from-config.test.ts --pool=forks --maxWorkers=1
- pnpm tsgo:extensions:test
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/codex/src/app-server/event-projector.ts extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.ts extensions/codex/src/app-server/run-attempt.test.ts extensions/codex/src/app-server/tool-progress-normalization.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/bot-message-dispatch.test.ts src/auto-reply/get-reply-options.types.ts src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/dispatch-from-config.test.ts src/infra/agent-events.ts
- pnpm lint:extensions
- pnpm build
- CI on 6ff6a1f868: 88 success, 20 skipped, 1 neutral, no failures or pending checks

Fixes #75641.
2026-05-06 08:18:20 +01:00
Peter Steinberger
900e416688 test: avoid deepseek loader cold path 2026-05-06 08:17:44 +01:00
Vincent Koc
53809e52e9 docs(install/ansible): remove duplicate H1 2026-05-06 00:13:53 -07:00
Peter Steinberger
95fd321b68 test: mock web provider fast-path artifacts 2026-05-06 08:08:48 +01:00
Vincent Koc
13504f693d docs(tools/brave-search): remove duplicate H1 2026-05-06 00:03:33 -07:00
Vincent Koc
f8bb00bb8b fix(deps): override vulnerable ip-address 2026-05-05 23:59:43 -07:00
Peter Steinberger
f956d0993c test: avoid discord native command cold load 2026-05-06 07:56:37 +01:00
Peter Steinberger
e37607349b test: trim codex app-server test setup 2026-05-06 07:56:37 +01:00
Shakker
934247b4b7 docs: note gateway metadata scan reuse 2026-05-06 07:55:27 +01:00
Shakker
d46859d886 fix: reuse plugin snapshot for agent metadata 2026-05-06 07:55:27 +01:00
Shakker
fe393e4427 fix: reuse plugin snapshot for read-only channels 2026-05-06 07:55:27 +01:00
Shakker
df209586bd fix: reuse plugin snapshot for auto enable 2026-05-06 07:55:27 +01:00
Shakker
5655c2b066 fix: pass current snapshot to embedded runs 2026-05-06 07:55:27 +01:00
Shakker
ba1800e1bd fix: reuse plugin snapshot for embedded settings 2026-05-06 07:55:27 +01:00
Vincent Koc
852b9e7246 docs(channels/line): fix smart apostrophe 2026-05-05 23:53:36 -07:00
Peter Steinberger
ecf06d7abe test(line): narrow config schema parse failures 2026-05-06 07:49:27 +01:00
Peter Steinberger
8f3a34e2a1 refactor: share fs-safe JSON helpers 2026-05-06 07:40:10 +01:00
Peter Steinberger
cf83c5827d docs: clarify targeted local validation 2026-05-06 07:37:38 +01:00
Peter Steinberger
5e05052bb9 fix(line): require wildcard for open dm policy 2026-05-06 07:35:46 +01:00
Vincent Koc
24fc6a435f docs(providers/senseaudio): add missing Related section 2026-05-05 23:34:07 -07:00
Peter Steinberger
8e533490ab fix(plugins): repair managed npm openclaw peers
Remove stale managed-root openclaw manifests, locks, hidden locks, and installed copies before npm plugin installs.

Relink plugin-local openclaw peer symlinks after shared-root npm install, rollback, update, and uninstall mutations so SDK-using plugins keep resolving openclaw/plugin-sdk/*.

Force safe npm commands out of inherited legacy/strict peer-dependency modes.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-05-06 07:32:25 +01:00
Peter Steinberger
8cc762daff fix(feishu): keep topic sessions stable
Fixes Feishu native topic starter routing by hydrating a missing topic thread ID before session resolution.\n\nCloses #78262.
2026-05-06 07:30:27 +01:00
Vincent Koc
c0c38194f6 changelog: add Matrix approval delivery retry entry (#78179) 2026-05-05 23:29:14 -07:00
Vincent Koc
506b0bbaad docs(providers): remove duplicate H1 in provider directory 2026-05-05 23:25:47 -07:00
Patrick Erichsen
5107384e67 fix: stabilize Matrix tool progress QA (#78179)
* fix: stabilize matrix tool progress QA

* fix: handle backtick matrix progress previews

* fix: reuse observed matrix approvals

* fix: retry matrix generated image QA

* fix: wait for matrix sas trust propagation

* fix: resolve matrix target both approvals by reaction

* fix: avoid matrix target both approval echo wait

* fix: reuse observed matrix target both dm approval

* fix: retry matrix approval delivery

* fix: accept active matrix approval dm

* test: align matrix approval retry receipt

* test: include matrix approval view in retry fixture
2026-05-05 23:20:08 -07:00
Vincent Koc
eb4d654796 docs: typography hygiene across 6 pages (start/tools/nodes/mac/platforms) 2026-05-05 23:14:49 -07:00
Vincent Koc
6921a47562 docs: typography hygiene across 6 pages (channels/nodes/mac platforms) 2026-05-05 23:11:28 -07:00
Peter Steinberger
627b0073f2 test: remove gateway restart delay wait 2026-05-06 07:02:27 +01:00
Shakker
7544beea17 fix: preserve embedded dispatcher timeouts 2026-05-06 07:01:02 +01:00
Shakker
d52f581f76 fix: avoid fetch runtime proxy imports 2026-05-06 07:01:02 +01:00
Shakker
c9c66d7a1d fix: restore no-proxy dispatcher boundary 2026-05-06 07:01:02 +01:00
Vincent Koc
6807da544b fix(net): preserve no-proxy undici stream timeouts 2026-05-06 07:01:02 +01:00
Shakker
6cf7ae1d98 docs: note plugin fetch dispatcher fix 2026-05-06 07:01:02 +01:00
Shakker
95652d5867 test: cover no-proxy undici startup 2026-05-06 07:01:02 +01:00
Shakker
85ed972217 fix: lazy-load undici dispatchers 2026-05-06 07:01:02 +01:00
Ayaan Zaidi
98cbf7f11c fix: show current think level in Telegram picker (#78278) 2026-05-06 11:24:31 +05:30
Peter Steinberger
1672d35ef5 perf: avoid no-op plugin auto-enable scans 2026-05-06 06:53:51 +01:00
Peter Steinberger
5da9f5e57c test: remove cli retry test waits 2026-05-06 06:50:06 +01:00
Vincent Koc
fa2a32d0c5 docs: typography hygiene across 6 pages (cli/gateway/platforms) 2026-05-05 22:44:56 -07:00
hcl
5f783d7ddd Plugin skills: use Windows junction links
Fixes #77958.\n\nMaintainer-prepped by narrowing the branch to the Windows plugin-skills junction fix, rebasing onto current main, adding cleanup/idempotence regression coverage and changelog, and verifying local gates plus green CI.\n\nCo-authored-by: hcl <7755017+hclsys@users.noreply.github.com>\nCo-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
2026-05-06 00:37:09 -05:00
拐爷&&老拐瘦
03e6a029ab Windows startup: handle localized schtasks access denied
Fixes #77993.\n\nMaintainer-prepped by rebasing onto current main, keeping the localized Windows schtasks Access Denied fallback scoped, adding focused regression coverage and changelog, and verifying local gates plus green CI.\n\nCo-authored-by: 拐爷&&老拐瘦 <geyunfei@gmail.com>\nCo-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
2026-05-06 00:36:54 -05:00
Vincent Koc
e85fd2abcd docs: typography hygiene + dup H1 across 5 pages (cli/gateway/help) 2026-05-05 22:35:00 -07:00
Peter Steinberger
6febffb6fe test: harden active memory timeout specs 2026-05-06 06:30:48 +01:00
Vincent Koc
b23232d560 docs: typography hygiene across 6 pages (mac platform + sandbox/wizard) 2026-05-05 22:25:27 -07:00
Peter Steinberger
6c743021d7 test: stabilize active memory timeout mocks 2026-05-06 06:18:57 +01:00
Vincent Koc
f505c84285 docs: typography hygiene across 7 high-traffic pages 2026-05-05 22:16:37 -07:00
Peter Steinberger
4ec693a81a test: interleave cold full-suite shards 2026-05-06 06:08:27 +01:00
Vincent Koc
f531eff629 docs: audit and fix 5 pages (typography hygiene + dup H1) 2026-05-05 22:04:37 -07:00
Peter Steinberger
06c490f818 test: support higher vitest shard parallelism 2026-05-06 05:57:53 +01:00
Vincent Koc
981e32d05d docs(reference): audit and fix 4 pages (typography, dup H1, Related) 2026-05-05 21:56:31 -07:00
Peter Steinberger
1f6ce72b8a test: trim cron and context-engine waits 2026-05-06 05:55:34 +01:00
Vincent Koc
8a68ea092d changelog: add xAI thinking-profile clamp entry 2026-05-05 21:50:33 -07:00
Peter Steinberger
f2ce83833a test: avoid spawning cli help in metadata test 2026-05-06 05:48:21 +01:00
Vincent Koc
963073088d docs: audit and fix 5 pages (sentence-case headings + Related/title) 2026-05-05 21:48:05 -07:00
Peter Steinberger
6da5eda488 test: avoid real waits in cdp and outbound tests 2026-05-06 05:43:48 +01:00
Vincent Koc
cbaf999bd2 docs: audit and fix 4 pages (sentence-case headings + Related links) 2026-05-05 21:42:03 -07:00
Jesse Merhi
5b00cd1ae1 fix: narrow Gateway proxy bypass target (#77018)
* fix: narrow Gateway proxy bypass target

* fix: narrow Gateway proxy bypass target

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (1)

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (2)

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (validation-3)

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (4-final)

* fix: narrow Gateway proxy bypass target

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (1)

* fix(clawsweeper): address review for automerge-openclaw-openclaw-77018 (2)

* fix(clawsweeper): reconcile automerge-openclaw-openclaw-77018 with main (1)

---------

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-06 14:40:31 +10:00
Peter Steinberger
be1c99b76a test: pass env to fallback metadata snapshot 2026-05-06 05:33:38 +01:00
Peter Steinberger
e9987ffc3a fix: clamp xAI live gateway thinking 2026-05-06 05:33:38 +01:00
Peter Steinberger
afc2c2e207 test(browser): avoid real retry waits 2026-05-06 05:33:28 +01:00
Vincent Koc
1ded8de5a9 docs: audit and fix 3 pages (typography across help/channels) 2026-05-05 21:28:47 -07:00
Peter Steinberger
82c4fd8f56 test: cache fallback metadata snapshot 2026-05-06 05:20:55 +01:00
Vincent Koc
41736de923 docs: audit and fix 4 pages (pi version bump + 3 typography/H1) 2026-05-05 21:14:55 -07:00
Peter Steinberger
ea26a9dba0 fix: omit xAI reasoning efforts 2026-05-06 05:13:10 +01:00
pickaxe
d221d7b6a9 fix(plugins): isolate peer-link repair failures 2026-05-06 05:13:01 +01:00
pickaxe
4d248b887f test(plugins): remove unnecessary peer-link assertion 2026-05-06 05:13:01 +01:00
pickaxe
fb42c722f0 fix(plugins): repair peer links after npm updates 2026-05-06 05:13:01 +01:00
Brandon
eecda912ee fix(msteams): surface network errors blocking bot JWT validation and outbound replies (#77674) (#78081)
* fix(msteams): surface network errors blocking Teams bot JWT validation and outbound replies (#77674)

When login.botframework.com or smba.trafficmanager.net egress is blocked,
errors previously disappeared completely. JWT validator swallowed network
errors and returned false (401 looked identical to a bad credential), and
outbound send failures with transport-level codes had no hint pointing to
the Connector endpoint.

- sdk.ts: rethrow ECONNREFUSED/ENOTFOUND/EHOSTUNREACH/ETIMEDOUT/ECONNRESET
  from the JWKS key fetch so callers can distinguish firewall blocks from bad
  credentials; add isJwksNetworkError() helper
- monitor.ts: catch rethrown network errors in JWT middleware and log at
  runtime.error level with an actionable message pointing to
  login.botframework.com:443; upgrade allowlist resolution failures from
  runtime.log (optional/silent) to runtime.error
- errors.ts: add "network" kind to classifyMSTeamsSendError for transport-level
  errors (ECONNREFUSED, ENOTFOUND, etc.); add formatMSTeamsSendErrorHint for
  "network" kind pointing to smba.trafficmanager.net and egress rules
- monitor-handler.ts, message-handler.ts: remove spurious ?. from runtime.error
  calls (RuntimeEnv.error is a required non-optional field)

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

* fix(msteams): surface blocked botframework egress

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
2026-05-05 23:11:06 -05:00
Peter Steinberger
5d7262c410 test: align telegram reply assertions with streaming defaults 2026-05-06 05:08:51 +01:00
Vincent Koc
c5ea7c4d0f docs: typography hygiene across 6 pages 2026-05-05 21:04:19 -07:00
Peter Steinberger
2df7ec5671 test: avoid bundled channel cold loads in message tool tests 2026-05-06 05:04:03 +01:00
Peter Steinberger
b85b1c68d1 Refactor file access to use fs-safe primitives (#78255)
* refactor: use fs-safe primitives across file access

* fix: preserve invalid managed npm manifests

* fix: keep fs seams for startup metadata
2026-05-06 05:03:11 +01:00
Vincent Koc
0d73f174a9 docs: typography hygiene + 2 in-body H1 removals across 5 pages 2026-05-05 21:01:44 -07:00
Peter Steinberger
f35fb7288a test: mock manifest normalization in fallback tests 2026-05-06 04:58:33 +01:00
Vincent Koc
68a82cb2e2 docs: typography hygiene + 2 in-body H1 removals across 6 pages
Replaced 60 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/start/openclaw.md: 10 chars; removed the duplicate '# Building
  a personal assistant with OpenClaw' H1 (Mintlify renders title from
  frontmatter).
- docs/platforms/mac/remote.md: 10 chars; removed the duplicate
  '# Remote OpenClaw (macOS ⇄ remote host)' H1 (the U+21C4 codepoint
  and parens both produced brittle anchors).
- docs/tools/thinking.md: 10 chars
- docs/reference/templates/BOOTSTRAP.md: 10 chars (kept the in-body
  '# BOOTSTRAP.md - Hello, World' heading because the page is a
  template whose content is meant to be copied verbatim into a
  workspace BOOTSTRAP.md).
- docs/plugins/sdk-provider-plugins.md: 10 chars
- docs/platforms/macos.md: 10 chars
2026-05-05 20:58:10 -07:00
Ayaan Zaidi
3afc902f3d fix(telegram): finalize streamed replies in place (#77947) 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
814b125f11 fix(telegram): separate progress drafts from final replies 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
e27f179361 fix(telegram): verify final stream edit landed 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
748d6dc75e test(qa): assert telegram streamed final count 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
512f777099 test(qa): thread telegram long final prompts 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
25fc85afa2 test(telegram): cover single stream delivery 2026-05-06 09:27:08 +05:30
Ayaan Zaidi
bca16d0f00 fix(telegram): finalize streamed text in place 2026-05-06 09:27:08 +05:30
Peter Steinberger
d7bd9fe049 fix(discord): route guild text commands (#78080) 2026-05-06 04:56:09 +01:00
Bryce D. Greybeard
b5c33bc204 fix(discord): avoid false heartbeat ACK timeouts
Fix the Discord Gateway heartbeat scheduler so ACK timeout checks are measured from the actual heartbeat send, not from the fixed HELLO-time interval. This prevents late randomized first heartbeats from causing false reconnect loops while the Discord channel is still awaiting readiness.\n\nVerification:\n- pnpm test extensions/discord/src/internal/gateway-lifecycle.test.ts extensions/discord/src/internal/gateway.test.ts\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/discord/src/internal/gateway-lifecycle.ts extensions/discord/src/internal/gateway-lifecycle.test.ts extensions/discord/src/internal/gateway.test.ts\n- git diff --check\n- Real behavior proof check passed on PR head bf239b886020c11d55af33f16674e953535f9b4c\n\nFixes #77668.\nSupersedes #77956.\nThanks @bryce-d-greybeard and @NikolaFC.
2026-05-06 04:46:46 +01:00
Vincent Koc
4ee234f8ee docs: typography hygiene across 6 pages
Replaced 66 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/channels/mattermost.md: 12 chars
- docs/tools/plugin.md: 11 chars
- docs/providers/xai.md: 11 chars
- docs/plugins/building-plugins.md: 11 chars
- docs/concepts/streaming.md: 11 chars
- docs/concepts/model-providers.md: 11 chars
2026-05-05 20:45:39 -07:00
Peter Steinberger
ebb8bed78f fix: cap memory wiki filenames for safe writes 2026-05-06 04:44:14 +01:00
Peter Steinberger
777c539daf fix: harden sandboxed patch parent paths 2026-05-06 04:44:14 +01:00
Peter Steinberger
cbc228f0f6 docs: explain blocked plugin ownership repair 2026-05-06 04:43:37 +01:00
Alex Alaniz
b971ebaaab fix(exec-approvals): guard Windows rename fallback (#77907)
* fix exec approvals Windows rename fallback

* fix(exec-approvals): restore approvals directory mode

* fix(exec-approvals): normalize fallback temp mode

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
2026-05-05 22:39:41 -05:00
Vincent Koc
f4a63940cc docs: typography hygiene across 6 pages
Replaced 74 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/gateway/opentelemetry.md: 13 chars
- docs/channels/msteams.md: 13 chars
- docs/tools/skills.md: 12 chars
- docs/start/setup.md: 12 chars
- docs/nodes/location-command.md: 12 chars
- docs/concepts/context-engine.md: 12 chars
2026-05-05 20:34:37 -07:00
Vincent Koc
ae9f779e5f docs: typography hygiene + 1 in-body H1 removal across 6 pages
Replaced 84 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/gateway/tools-invoke-http-api.md: 14 chars; removed the
  duplicate '# Tools Invoke (HTTP)' H1 (Mintlify renders title from
  frontmatter; the in-body H1 with parens produced a brittle anchor).
- docs/tools/browser-control.md: 14 chars
- docs/security/formal-verification.md: 14 chars
- docs/gateway/configuration-reference.md: 14 chars
- docs/concepts/agent.md: 14 chars
- docs/channels/qa-channel.md: 14 chars
2026-05-05 20:26:16 -07:00
github-actions[bot]
d71c11983f chore(ui): refresh nl control ui locale 2026-05-06 03:22:57 +00:00
github-actions[bot]
186d247209 chore(ui): refresh fa control ui locale 2026-05-06 03:22:53 +00:00
github-actions[bot]
020581ac7f chore(ui): refresh vi control ui locale 2026-05-06 03:22:49 +00:00
github-actions[bot]
f51436868b chore(ui): refresh th control ui locale 2026-05-06 03:22:09 +00:00
github-actions[bot]
9ce00b7756 chore(ui): refresh pl control ui locale 2026-05-06 03:22:01 +00:00
github-actions[bot]
a0a74608ff chore(ui): refresh id control ui locale 2026-05-06 03:21:47 +00:00
github-actions[bot]
b868f4e2be chore(ui): refresh uk control ui locale 2026-05-06 03:21:39 +00:00
github-actions[bot]
4e867ea2c9 chore(ui): refresh tr control ui locale 2026-05-06 03:21:05 +00:00
github-actions[bot]
1a3d77531d chore(ui): refresh it control ui locale 2026-05-06 03:20:59 +00:00
github-actions[bot]
b9eb969d9a chore(ui): refresh ar control ui locale 2026-05-06 03:20:54 +00:00
github-actions[bot]
fc6737bd0a chore(ui): refresh fr control ui locale 2026-05-06 03:20:29 +00:00
github-actions[bot]
c17bcb99e1 chore(ui): refresh ko control ui locale 2026-05-06 03:20:03 +00:00
github-actions[bot]
3cff0d3dc8 chore(ui): refresh ja-JP control ui locale 2026-05-06 03:20:01 +00:00
github-actions[bot]
19071cc6a5 chore(ui): refresh es control ui locale 2026-05-06 03:19:54 +00:00
github-actions[bot]
76e8f59f17 chore(ui): refresh zh-CN control ui locale 2026-05-06 03:19:13 +00:00
github-actions[bot]
931645e090 chore(ui): refresh zh-TW control ui locale 2026-05-06 03:19:06 +00:00
github-actions[bot]
47b65154ae chore(ui): refresh de control ui locale 2026-05-06 03:19:02 +00:00
github-actions[bot]
9111f83765 chore(ui): refresh pt-BR control ui locale 2026-05-06 03:18:55 +00:00
Val Alexander
c17121b1cc test(control-ui): refresh i18n raw copy baseline 2026-05-05 22:16:30 -05:00
Val Alexander
8aa377babe fix(control-ui): refine sessions compaction details 2026-05-05 22:16:30 -05:00
Vincent Koc
861a593921 docs: typography hygiene across 5 pages
Replaced 75 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/plugins/skill-workshop.md: 15 chars
- docs/gateway/pairing.md: 15 chars
- docs/gateway/configuration.md: 15 chars
- docs/concepts/oauth.md: 15 chars
- docs/channels/bluebubbles.md: 15 chars
2026-05-05 20:14:18 -07:00
Peter Steinberger
c73f774b9b test: stabilize active-memory timeout partials 2026-05-06 04:11:02 +01:00
Val Alexander
e2858e70dd chore: update channel status protocol models 2026-05-05 22:09:45 -05:00
Val Alexander
60171e8638 Keep Control UI responsive under slow status and history loads 2026-05-05 22:07:39 -05:00
Peter Steinberger
3f6b481464 fix: serialize concurrent transcript appends 2026-05-06 04:06:28 +01:00
Vincent Koc
fafd76c5e6 docs: typography hygiene across 5 pages
Replaced 80 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/plugins/sdk-entrypoints.md: 17 chars
- docs/help/index.md: 17 chars
- docs/concepts/agent-workspace.md: 16 chars
- docs/tools/lobster.md: 15 chars
- docs/tools/exec-approvals.md: 15 chars
2026-05-05 20:04:12 -07:00
Val Alexander
49c4a13231 fix(sessions): restore Control UI /new hooks
Fixes #76957.

Restores the Control UI /new hook lifecycle through an explicit sessions.create emitCommandHooks opt-in, preserving hook-free defaults for programmatic parent-session creates.

Validation:
- pnpm protocol:check
- pnpm test src/gateway/server.sessions.reset-hooks.test.ts ui/src/ui/app-render.helpers.node.test.ts
- pnpm exec oxlint on touched TS files
- pnpm exec oxfmt --check --threads=1 on touched files
- git diff --check
- OPENCLAW_LOCAL_CHECK=1 OPENCLAW_LOCAL_CHECK_MODE=throttled env NODE_OPTIONS=--max-old-space-size=4096 pnpm check:changed
- GitHub PR checks green on 3a446ec78e
- ClawSweeper re-review completed with no blocking findings and security cleared

Duplicate triage:
- #77376, #77004, and #76967 were superseded closed attempts for #76957
- #77562 is a closed duplicate issue
- #77880 mentions #76957 but is not a duplicate of this hook fix
2026-05-05 21:57:22 -05:00
Val Alexander
3110c621df fix(gateway): preserve mixed assistant history text
Preserve visible assistant text from mixed text/tool-use transcript turns in chat.history while keeping commentary-only assistant turns hidden.

Fixes #77374.

Verification:
- pnpm test src/gateway/server-methods/server-methods.test.ts src/gateway/server.chat.gateway-server-chat-b.test.ts
- pnpm exec oxfmt --check --threads=1 src/gateway/chat-display-projection.ts src/gateway/server-methods/server-methods.test.ts src/gateway/server.chat.gateway-server-chat-b.test.ts
- git diff --check
- pnpm changed:lanes --json
- PR CI passed on 048266c5a5
2026-05-05 21:56:56 -05:00
Vincent Koc
7a39551685 docs: typography hygiene + 2 in-body H1 removals across 5 pages
Replaced 92 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/channels/feishu.md: 19 chars; removed the duplicate
  '# Feishu / Lark' H1 (Mintlify renders title from frontmatter; the
  in-body H1 with a slash produced a brittle anchor).
- docs/gateway/bonjour.md: 18 chars; removed the duplicate
  '# Bonjour / mDNS discovery' H1.
- docs/channels/matrix.md: 19 chars
- docs/tools/browser.md: 18 chars
- docs/automation/standing-orders.md: 18 chars
2026-05-05 19:54:53 -07:00
Vincent Koc
4395f1dd66 docs: typography hygiene + drop one in-body H1 across 5 pages
Replaced 98 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/plugins/sdk-migration.md: 20 chars
- docs/help/testing.md: 20 chars
- docs/automation/tasks.md: 20 chars
- docs/plugins/sdk-channel-plugins.md: 19 chars
- docs/channels/yuanbao.md: 19 chars; removed the duplicate '# Yuanbao'
  H1 (Mintlify renders title from frontmatter).
2026-05-05 19:46:32 -07:00
Peter Steinberger
8489d0eb68 test: update spawn workspace pi settings mock 2026-05-06 03:43:39 +01:00
Peter Steinberger
ea391c6df2 test: stabilize cron and pairing shard hangs 2026-05-06 03:36:46 +01:00
Brad Hallett
0bdba47a3e fix: disable Pi auto-compaction when safeguard mode is active (#73839)
Merged via squash.

Prepared head SHA: d554201343
Co-authored-by: bradhallett <53977268+bradhallett@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-05 19:35:47 -07:00
Vincent Koc
2b8d91d9ee docs: typography hygiene + 2 in-body H1 removals across 5 pages
Replaced 112 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules.

- docs/help/gpt55-codex-agentic-parity.md: 22 chars; removed the
  duplicate '# GPT-5.5 / Codex Agentic Parity in OpenClaw' H1 (Mintlify
  renders the title from frontmatter; the in-body H1 with the slash
  produced a brittle anchor).
- docs/platforms/mac/menu-bar.md: 21 chars; removed the duplicate
  '# Menu Bar Status Logic' H1.
- docs/tools/acp-agents.md: 23 chars
- docs/concepts/qa-matrix.md: 23 chars
- docs/concepts/qa-e2e-automation.md: 23 chars
2026-05-05 19:34:52 -07:00
Vincent Koc
b9f711089a docs: typography hygiene + drop one in-body H1 across 5 pages
Replaced 138 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents per
docs/CLAUDE.md heading and content hygiene rules so grep, copy-paste,
and Mintlify search hit clean tokens.

- docs/reference/AGENTS.default.md: 29 chars, plus removed the
  duplicate '# AGENTS.md - OpenClaw Personal Assistant (default)' H1
  (Mintlify renders title from frontmatter; the in-body H1 with
  parens and a bare hyphen produced a brittle anchor).
- docs/help/testing-live.md: 29 chars
- docs/tools/image-generation.md: 28 chars
- docs/channels/index.md: 27 chars
- docs/tools/video-generation.md: 25 chars
2026-05-05 19:25:16 -07:00
Peter Steinberger
74532265f4 test: tolerate archive race outcomes 2026-05-06 03:23:25 +01:00
Vincent Koc
736f627fb5 docs: typography hygiene across 4 large pages
Replaced 152 typography characters (curly quotes, apostrophes, em/en
dashes, non-breaking hyphens) with ASCII equivalents so grep,
copy-paste, and Mintlify search hit clean tokens. Per docs/CLAUDE.md
heading and content hygiene rules.

- docs/gateway/security/index.md: 59 chars
- docs/plugins/hooks.md: 34 chars
- docs/reference/session-management-compaction.md: 30 chars
- docs/tools/clawhub.md: 29 chars
2026-05-05 19:19:15 -07:00
Peter Steinberger
585bff4b75 test: accept archive race refusal variants 2026-05-06 03:17:59 +01:00
Peter Steinberger
b60d5f4024 test: keep voice-call runtime tests on public seams 2026-05-06 03:09:33 +01:00
Peter Steinberger
1d1b3a398d test: keep voice-call runtime test on sdk seam 2026-05-06 03:07:44 +01:00
Val Alexander
36df0d93b9 fix: repair iOS LAN pairing
Fix iOS LAN/setup-code pairing policy for #47887.

- Allow explicit private LAN and .local plaintext ws:// setup/manual connects where policy allows it.
- Keep public hosts, .ts.net, and Tailscale CGNAT plaintext fail-closed.
- Prefer explicit passwords over stale bootstrap tokens in Swift and TypeScript gateway clients.
- Update setup-code/device-pair coverage, docs, and changelog with source credit for #65185.

Verification:
- pnpm install
- git diff --check origin/main..HEAD
- pnpm exec oxfmt --check --threads=1 src/gateway/client.ts src/gateway/client.test.ts src/pairing/setup-code.ts src/pairing/setup-code.test.ts extensions/device-pair/index.ts extensions/device-pair/index.test.ts
- pnpm format:docs:check
- pnpm test src/gateway/client.test.ts src/pairing/setup-code.test.ts extensions/device-pair/index.test.ts
- cd apps/shared/OpenClawKit && swift test --filter 'DeepLinksSecurityTests|GatewayNodeSessionTests'
- pnpm lint:swift passes with the existing TalkModeRuntime.swift type-body-length warning

Blocked locally:
- iOS app-target xcodebuild tests require unavailable watchOS 26.4 runtime here.
- Testbox check:changed previously failed because the image lacks swiftlint; local swiftlint passes.
2026-05-05 21:07:19 -05:00
Peter Steinberger
ae7c13e284 test: restore current-main test isolation 2026-05-06 03:04:55 +01:00
Vincent Koc
bff5051e38 docs: drop in-body H1s and typography hygiene across 4 pages
docs/install/macos-vm.md: removed the duplicate '# OpenClaw on macOS
VMs (Sandboxing)' H1 (Mintlify renders title from frontmatter; the
in-body H1 plus parens produced a brittle anchor).

docs/install/development-channels.md: removed the duplicate
'# Development channels' H1.

docs/install/index.md: replaced 3 typography characters (curly quotes
and en-dash) with ASCII equivalents.

docs/concepts/delegate-architecture.md: replaced 10 typography
characters (curly quotes, apostrophes, em/en dashes) with ASCII
equivalents.
2026-05-05 19:04:46 -07:00
Peter Steinberger
6ad601d195 test: align archive hardlink guard expectation 2026-05-06 03:04:27 +01:00
Peter Steinberger
8b9b849b19 test: align fs-safe race expectations 2026-05-06 03:02:47 +01:00
Vincent Koc
9671a91590 docs: Related CardGroups + typography hygiene across 4 pages
docs/install/clawdock.md: renamed '## Related pages' to '## Related'
for consistency with sibling install docs and converted the 3-bullet
list into a CardGroup linking docker, docker-vm-runtime, and updating.

docs/install/nix.md: replaced 2 typography characters with ASCII
equivalents and converted the 3-bullet Related list into a CardGroup,
adding an Updating card so readers wiring nix-openclaw next to a
managed install see the upgrade path.

docs/concepts/features.md: converted the 2-bullet Related list into a
CardGroup, adding cross-links to channels and plugins so the page now
points readers at both deeper concepts (experimental features, agent
runtime) and direct surfaces (channels, plugins).

docs/tools/pdf.md: replaced 2 typography characters with ASCII
equivalents.
2026-05-05 18:56:25 -07:00
Peter Steinberger
9e108fa9a7 fix: repair fs-safe ci expectations 2026-05-06 02:56:12 +01:00
Peter Steinberger
b43efd3793 fix: clean up post-land CI guards 2026-05-06 02:51:53 +01:00
Peter Steinberger
8294229592 test: refresh fs-safe boundary expectations 2026-05-06 02:50:36 +01:00
Peter Steinberger
a6a4140ee7 fix(media): handle canonical inbound media paths 2026-05-06 02:50:36 +01:00
Peter Steinberger
d47c624370 docs(release): clarify unpublished beta tag movement 2026-05-06 02:49:47 +01:00
Peter Steinberger
9ff7fe08e9 docs: standardize compact PR author activity 2026-05-06 02:46:27 +01:00
Vincent Koc
e36cb33379 docs: drop in-body H1s and typography hygiene across 4 pages
docs/install/gcp.md: removed the duplicate '# OpenClaw on GCP Compute
Engine (Docker, Production VPS Guide)' H1 plus its redundant '## Goal'
header. Mintlify renders the title from frontmatter, so the body H1
created a brittle anchor and the prose now starts directly with the
goal sentence.

docs/install/node.md: replaced 8 typography characters (curly quotes
and non-breaking hyphens) with ASCII equivalents.

docs/tools/duckduckgo-search.md: replaced 9 typography characters with
ASCII equivalents.

docs/tools/browser-login.md: removed the duplicate '# Browser login +
X/Twitter posting' H1 (Mintlify renders title from frontmatter; the
'+' would also have produced a brittle anchor). Replaced 2 typography
characters with ASCII equivalents.
2026-05-05 18:46:03 -07:00
Peter Steinberger
73d9044204 docs(agents): prefer crabbox webvnc inspection 2026-05-06 02:43:49 +01:00
Peter Steinberger
057d3a43c0 feat(mantis): capture logged-in discord web evidence 2026-05-06 02:43:49 +01:00
Peter Steinberger
20163313af fix: resolve fs-safe post-land fallout 2026-05-06 02:41:36 +01:00
Peter Steinberger
71cd132f1f docs: remove refactor notes 2026-05-06 02:40:34 +01:00
Peter Steinberger
9b1d28edf1 chore: refresh talk sdk baseline 2026-05-06 02:39:15 +01:00
Peter Steinberger
df29682384 test: update talk unit-fast paths 2026-05-06 02:39:15 +01:00
Peter Steinberger
e02ddf71af fix: guard managed talk room control 2026-05-06 02:39:15 +01:00
Peter Steinberger
0402ae327e test: generate hook install archives 2026-05-06 02:39:15 +01:00
Peter Steinberger
c7b69a319b test: retry gateway chat temp cleanup 2026-05-06 02:39:15 +01:00
Peter Steinberger
df4db5a721 test: isolate main auth profile fixtures 2026-05-06 02:39:15 +01:00
Peter Steinberger
f1636d5e28 refactor: unify talk session runtime 2026-05-06 02:39:15 +01:00
Peter Steinberger
7431cb8def docs: detail talk refactor plan 2026-05-06 02:39:15 +01:00
Peter Steinberger
7760edc68e chore: refresh talk generated metadata 2026-05-06 02:39:15 +01:00
Peter Steinberger
ada560ece4 feat: adapt voice surfaces to talk events 2026-05-06 02:39:15 +01:00
Peter Steinberger
9e6f38f4e1 feat: unify browser realtime talk clients 2026-05-06 02:39:15 +01:00
Peter Steinberger
466f718320 feat: wire talk handoff into native nodes 2026-05-06 02:39:15 +01:00
Peter Steinberger
c434d7720b feat: add unified talk gateway sessions 2026-05-06 02:39:15 +01:00
Peter Steinberger
7225a2678e feat: expose talk-capable realtime providers 2026-05-06 02:39:15 +01:00
Peter Steinberger
c90c68c636 feat: add shared talk runtime primitives 2026-05-06 02:39:15 +01:00
Peter Steinberger
24853ced11 docs: outline unified talk API 2026-05-06 02:39:15 +01:00
Vincent Koc
1f7d0ef310 docs: typography hygiene + Related CardGroups across 4 pages
docs/concepts/context.md: replaced 12 curly quote and italic-marker
typography characters with ASCII equivalents so grep, copy-paste, and
Mintlify search hit clean tokens. Converted the 4-bullet Related list
into a CardGroup linking context-engine, compaction, system-prompt,
and agent-loop. Verified all four targets exist.

docs/concepts/soul.md: replaced 7 typography characters (curly
apostrophe in 'agent's' and similar) with ASCII equivalents. Renamed
'## Related docs' to '## Related' for consistency with sibling pages
and converted the 3-bullet list into a CardGroup linking
agent-workspace, system-prompt, and the SOUL.md template.

docs/tools/perplexity-search.md: removed the duplicate
'# Perplexity Search API' H1 (Mintlify renders title from frontmatter).
Replaced 2 typography characters and converted the 4-bullet Related
list into a CardGroup; verified web/brave-search/exa-search targets.

docs/tools/apply-patch.md: converted the 3-bullet Related list into a
CardGroup linking diffs, exec, and code-execution.
2026-05-05 18:36:06 -07:00
Vincent Koc
7f71e84248 docs(concepts): typography hygiene + Related CardGroups across 3 pages
docs/concepts/presence.md: replaced 8 curly quote and non-breaking
hyphen characters (U+201C/U+201D/U+2019/U+2011) with ASCII equivalents
so grep, copy-paste, and Mintlify search hit the right tokens.
Converted the 2-bullet Related list into a CardGroup adding cross-links
to gateway architecture and gateway protocol since presence is produced
by both surfaces.

docs/concepts/markdown-formatting.md: replaced 5 typography characters
(en-dash and curly quotes) with ASCII equivalents and converted the
2-bullet Related list into a CardGroup pointing at streaming/chunking
and system prompt.

docs/concepts/typing-indicators.md: replaced 4 typography characters
with ASCII equivalents and converted the 2-bullet Related list into a
CardGroup with the same Presence and Streaming cross-links.

Verified /concepts/streaming, /concepts/system-prompt,
/concepts/architecture, and /gateway/protocol targets all exist.
2026-05-05 18:30:39 -07:00
Peter Steinberger
29ddcc688e docs: require global GitHub activity in PR triage 2026-05-06 02:28:22 +01:00
Peter Steinberger
601b4819cb test: refresh plugin loader boundary assertions 2026-05-06 02:24:43 +01:00
Peter Steinberger
538605ff44 [codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
2026-05-06 02:15:17 +01:00
Vincent Koc
61481eb34f docs: tighten architecture, btw, agent-send hygiene
docs/concepts/architecture.md: replaced 8 non-breaking hyphen
characters (U+2011) with regular hyphens. Non-breaking hyphens defeat
copy-paste from rendered HTML, break grep on the raw markdown, and
make Mintlify search miss otherwise-correct queries. Affected words:
'long-lived', 'server-push', 'device-based'.

docs/tools/btw.md: converted the 3-bullet Related list into a
CardGroup. Renamed 'Thinking Levels' to sentence-case 'Thinking
levels' and added a Steer-command card so readers comparing ephemeral
vs in-run intervention paths see both options.

docs/tools/agent-send.md: converted the 3-bullet Related list into a
CardGroup. Removed two em-dash characters in the bullet copy
('Sub-agents — background sub-agent spawning', 'Sessions — how
session keys work') and added a Slash-commands card. Verified
/cli/agent, /tools/subagents, /concepts/session, and
/tools/slash-commands targets all exist.
2026-05-05 18:13:16 -07:00
Peter Steinberger
c744b2c236 docs: improve OpenClaw PR skill trigger 2026-05-06 02:10:11 +01:00
Peter Steinberger
947e530ad1 fix: improve slack socket mode diagnostics 2026-05-06 02:09:36 +01:00
4120 changed files with 119512 additions and 150201 deletions

View File

@@ -22,6 +22,8 @@ Blacksmith fallback playbook.
command -v crabbox
../crabbox/bin/crabbox --version
pnpm crabbox:run -- --help | sed -n '1,120p'
../crabbox/bin/crabbox desktop launch --help
../crabbox/bin/crabbox webvnc --help
```
- OpenClaw scripts prefer `../crabbox/bin/crabbox` when present. The user PATH
@@ -30,6 +32,14 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
Even if config still says AWS, maintainer validation should normally pass
`--provider blacksmith-testbox`.
- Prefer local targeted tests for tight edit loops. Broad gates belong remote.
- Do not treat inherited shell env as operator intent. In particular,
`OPENCLAW_LOCAL_CHECK_MODE=throttled` from the local shell is not permission
to move broad `pnpm check:changed`, `pnpm test:changed`, full `pnpm test`, or
lint/typecheck fan-out onto the laptop.
- Only use `OPENCLAW_LOCAL_CHECK_MODE=throttled|full` when the user explicitly
asks for local proof in the current task. If Testbox is queued or capacity is
constrained, report the blocker and keep only targeted local edit-loop checks
running.
## macOS And Windows Targets
@@ -139,6 +149,35 @@ pnpm crabbox:stop -- <id-or-slug>
blacksmith testbox stop --id <tbx_id>
```
## Interactive Desktop And WebVNC
Prefer WebVNC for human inspection because the browser portal can preload the
lease VNC password and avoids a native VNC client's copy/paste/password dance.
Use native `crabbox vnc` only when WebVNC is unavailable, the browser portal is
broken, or the user explicitly wants a local VNC client.
Common desktop flow:
```sh
../crabbox/bin/crabbox warmup --provider hetzner --desktop --browser --class standard --idle-timeout 60m --ttl 240m
../crabbox/bin/crabbox desktop launch --provider hetzner --id <cbx_id-or-slug> --browser --url https://example.com --webvnc --open
```
Useful WebVNC commands:
```sh
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --open
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --daemon --open
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --status
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --stop
../crabbox/bin/crabbox screenshot --provider hetzner --id <cbx_id-or-slug> --output desktop.png
```
`desktop launch --webvnc --open` is usually the nicest one-shot: it starts the
browser/app inside the visible session, bridges the lease into the authenticated
WebVNC portal, and opens the portal. Keep browsers windowed for human QA; use
`--fullscreen` only for capture/video workflows.
## If Crabbox Fails
Keep the fallback narrow. First decide whether the failure is Crabbox itself,
@@ -167,6 +206,10 @@ Common Crabbox-only failures:
printed Actions URL.
- Cleanup uncertainty: run `blacksmith testbox list` and stop only boxes you
created.
- Testbox queued/capacity pressure: do not convert a broad changed gate or full
suite into local `OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm ...`. Leave the
remote lane queued, switch to a narrower targeted local check, or stop and
report the capacity blocker.
If Crabbox cannot dispatch, sync, attach, or stop but Blacksmith itself works,
use direct Blacksmith from the repo root:
@@ -253,9 +296,27 @@ Install/auth for owned Crabbox if needed:
```sh
brew install openclaw/tap/crabbox
printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox.openclaw.ai --provider aws --token-stdin
crabbox login --url https://crabbox.openclaw.ai --provider aws
```
New users should self-resolve broker auth before anyone asks for AWS keys:
```sh
crabbox config show
crabbox doctor
crabbox whoami
```
- If broker auth is missing, run `crabbox login --url https://crabbox.openclaw.ai --provider aws`.
- If the CLI asks for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, or AWS
profile setup during normal OpenClaw validation, assume the agent selected
the wrong path. Use brokered `crabbox login`, `--provider blacksmith-testbox`,
or an existing brokered lease before asking the user for cloud credentials.
- Ask for AWS keys only for explicit direct-provider/account administration,
not for normal brokered OpenClaw proof.
- Trusted automation may still use
`printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox.openclaw.ai --provider aws --token-stdin`.
macOS config lives at:
```text
@@ -268,11 +329,11 @@ when Blacksmith proof is requested; pass `--provider blacksmith-testbox`.
### Interactive Desktop / WebVNC
For human WebVNC demos, keep the remote desktop visible and windowed. Do not
fullscreen the remote browser or hide the XFCE panel/window chrome unless the
explicit goal is video/capture output. After launch, verify a screenshot shows
the desktop panel plus browser title bar. If Chrome is fullscreen, toggle it
back with:
For human desktop demos, prefer `webvnc` over native `vnc` and keep the remote
desktop visible/windowed. Do not fullscreen the remote browser or hide the XFCE
panel/window chrome unless the explicit goal is video/capture output. After
launch, verify a screenshot shows the desktop panel plus browser title bar. If
Chrome is fullscreen, toggle it back with:
```sh
crabbox run --id <lease> --shell -- 'DISPLAY=:99 xdotool search --onlyvisible --class google-chrome windowactivate key F11'

View File

@@ -1,6 +1,6 @@
---
name: openclaw-pr-maintainer
description: Review, triage, close, label, comment on, or land OpenClaw PRs/issues with maintainer evidence checks.
description: Use immediately for any pasted OpenClaw GitHub issue or PR URL/number, and for OpenClaw issue/PR review, triage, duplicate search, opener identity/who wrote it, author account age/activity, comments, labels, close, land, or maintainer evidence checks.
---
# OpenClaw PR Maintainer
@@ -28,8 +28,9 @@ gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --
- For every reviewed, triaged, closed, or landed issue/PR, show the opener's human name when available, GitHub login, and account age.
- Get the login from `gh issue view` / `gh pr view` (`author.login`), then fetch profile metadata once with `gh api users/<login> --jq '{login,name,created_at,type}'`.
- Report account age as created date plus rough age, for example `Opened by Jane Doe (@jane, account created 2021-04-03, ~5y old)`.
- Also show recent GitHub activity when it informs maintainer risk: OpenClaw PRs, issues, and commits in the last 12 months; for linked issue-fixing PRs, include both the PR author and issue opener when they differ.
- Report opener identity as one compact line:
`By: Jane Doe (@jane, acct 2021-04-03) | OpenClaw: 4 PRs, 2 issues, 11 commits/12mo | GitHub: 9 repos, 86 commits, 9 PRs, 3 issues, 12 reviews`
- Always show recent activity in two lanes: OpenClaw-local PRs, issues, and commits in the last 12 months; and general public GitHub activity over the same window. For linked issue-fixing PRs, include both the PR author and issue opener when they differ.
- Prefer the bundled helper for activity lookups:
```bash
@@ -37,9 +38,11 @@ gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --
.agents/skills/openclaw-pr-maintainer/scripts/github-activity.sh --global <login>
```
- The helper reports repo-local activity first and can fetch public GitHub contribution totals for the same window with `--global`.
- The helper reports repo-local activity first and can fetch public GitHub contribution totals for the same window with `--global`; run the global form by default for review/triage identity summaries.
- If the global contribution graph reports zero or looks inconsistent with visible public activity, sanity-check with `gh api users/<login>`, `gh api 'users/<login>/events/public?per_page=100'`, and recent public repo commits before calling the account inactive.
- The helper is intentionally cache-friendly for gitcrawl-backed `gh`: it rounds repo-local windows to the UTC day, rounds global contribution windows to the UTC hour, and counts PRs/issues from one paginated issues response before fetching commits separately. Prefer reusing the helper instead of hand-rolling several `gh api` loops.
- Report activity compactly, for example `OpenClaw last 12mo: 4 PRs, 2 issues, 11 commits; GitHub public last 12mo: 86 commits, 9 PRs, 3 issues, 12 reviews`.
- If the contribution graph is misleading or zero but public events/repos show activity, keep it one line, for example:
`By: pickaxe (@ProspectOre, acct 2019-08-24) | OpenClaw: 5 PRs, 0 issues, 5 commits/12mo | GitHub: 5 repos, 29 recent events, 100 public own-repo commits; graph=0`
- If `name` is empty, use the login only. If profile lookup is rate-limited or unavailable, say `account age unknown` rather than omitting the opener.
- Use identity and activity as triage signal, not proof by itself: new, low-activity, or bot-like accounts can raise review caution, but code, repro, and CI evidence still decide.

View File

@@ -42,10 +42,12 @@ Use this skill for release and publish-time workflow. Keep ordinary development
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after their matching npm package has been
published. If a pushed beta tag fails preflight before npm publish, delete and
recreate the tag and prerelease at the fixed commit so npm prerelease versions
stay contiguous. If a published beta needs a fix, commit the fix on the
release branch and increment to the next `-beta.N`.
published. If a pushed beta tag fails before npm publish, the version is not
consumed: keep the same `-beta.N`, delete/recreate or force-move the git tag
and prerelease to the fixed commit, and rerun preflight. Do not increment to
the next beta number until the matching npm package has actually published.
If a published 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 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

View File

@@ -1,12 +1,17 @@
profile: openclaw-check
provider: aws
class: beast
class: standard
capacity:
market: spot
strategy: most-available
fallback: on-demand-after-120s
hints: true
regions:
- eu-west-1
- eu-west-2
- eu-central-1
- us-east-1
- us-west-2
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate

View File

@@ -14,7 +14,6 @@ query-filters:
- security
paths:
- extensions/bluebubbles/src
- extensions/discord/src
- extensions/feishu/src
- extensions/googlechat/src

View File

@@ -0,0 +1,28 @@
name: openclaw-codeql-network-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: ./.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
- uses: ./.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql
paths:
- src
- extensions
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"
- "extensions/diffs/assets/**"

View File

@@ -0,0 +1,30 @@
---
lockVersion: 1.0.0
dependencies:
codeql/concepts:
version: 0.0.22
codeql/controlflow:
version: 2.0.32
codeql/dataflow:
version: 2.1.4
codeql/javascript-all:
version: 2.6.28
codeql/mad:
version: 1.0.48
codeql/regex:
version: 1.0.48
codeql/ssa:
version: 2.0.24
codeql/threat-models:
version: 1.0.48
codeql/tutorial:
version: 1.0.48
codeql/typetracking:
version: 2.0.32
codeql/util:
version: 2.0.35
codeql/xml:
version: 1.0.48
codeql/yaml:
version: 1.0.48
compiled: false

View File

@@ -0,0 +1,6 @@
name: openclaw/codeql-boundary-queries
version: 0.0.0
library: false
dependencies:
codeql/javascript-all: 2.6.28
extractor: javascript

View File

@@ -0,0 +1,325 @@
/**
* @name Managed proxy runtime mutation
* @description Proxy-related process.env and GLOBAL_AGENT runtime mutations must stay in managed proxy owner scopes.
* @kind problem
* @problem.severity error
* @precision high
* @id js/openclaw/managed-proxy-runtime-mutation
* @tags maintainability
* security
* external/cwe/cwe-441
*/
import javascript
predicate forbiddenEnvKey(string key) {
key =
[
"HTTP_PROXY",
"HTTPS_PROXY",
"http_proxy",
"https_proxy",
"NO_PROXY",
"no_proxy",
"GLOBAL_AGENT_HTTP_PROXY",
"GLOBAL_AGENT_HTTPS_PROXY",
"GLOBAL_AGENT_NO_PROXY",
"GLOBAL_AGENT_FORCE_GLOBAL_AGENT",
"OPENCLAW_PROXY_ACTIVE",
"OPENCLAW_PROXY_LOOPBACK_MODE"
]
}
predicate forbiddenGlobalAgentKey(string key) { key = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"] }
predicate relevantSourceFile(File file) {
exists(string path |
path = file.getRelativePath() and
path.regexpMatch("^(src|extensions)/.*\\.(ts|mts|js|mjs)$") and
not path.regexpMatch(".*\\.(test|spec)\\.(ts|mts|js|mjs)$") and
not path.regexpMatch(".*\\.(test-utils|test-harness|e2e-harness)\\.ts$") and
not path.regexpMatch(".*/test-support/.*") and
not path.regexpMatch(".*/vendor/.*") and
not path.regexpMatch(".*\\.min\\.js$") and
not path.regexpMatch("^extensions/diffs/assets/.*")
)
}
predicate namedExpr(Expr expr, string name) {
expr.getUnderlyingValue().(Identifier).getName() = name
}
predicate directProcessEnvExpr(Expr expr) {
exists(PropAccess access |
expr.getUnderlyingValue() = access and
access.getPropertyName() = "env" and
namedExpr(access.getBase(), "process")
)
}
predicate envAlias(Variable variable) {
exists(VariableDeclarator decl |
decl.getBindingPattern().getAVariable() = variable and
directProcessEnvExpr(decl.getInit())
)
or
exists(VariableDeclarator decl, ObjectPattern pattern, PropertyPattern property |
decl.getBindingPattern() = pattern and
namedExpr(decl.getInit(), "process") and
property = pattern.getAPropertyPattern() and
property.getName() = "env" and
property.getValuePattern().(BindingPattern).getAVariable() = variable
)
}
predicate processEnvExpr(Expr expr) {
directProcessEnvExpr(expr)
or
exists(VarAccess access |
expr.getUnderlyingValue() = access and
envAlias(access.getVariable())
)
}
predicate stringConst(Variable variable, string value) {
exists(VariableDeclarator decl |
decl.getBindingPattern().getAVariable() = variable and
value = decl.getInit().getStringValue()
)
}
predicate stringArrayContains(Variable variable, string value) {
exists(VariableDeclarator decl, ArrayExpr array, Expr element |
decl.getBindingPattern().getAVariable() = variable and
decl.getInit().getUnderlyingValue() = array and
element = array.getAnElement().getUnderlyingValue() and
value = element.getStringValue()
)
or
exists(VariableDeclarator decl, ArrayExpr array, SpreadElement spread, VarAccess access |
decl.getBindingPattern().getAVariable() = variable and
decl.getInit().getUnderlyingValue() = array and
spread = array.getAnElement().getUnderlyingValue() and
spread.getOperand().getUnderlyingValue() = access and
stringArrayContains(access.getVariable(), value)
)
}
predicate forbiddenEnvLoopVariable(Variable variable) {
exists(ForOfStmt loop, VarAccess domain, string key |
variable = loop.getAnIterationVariable() and
loop.getIterationDomain().getUnderlyingValue() = domain and
stringArrayContains(domain.getVariable(), key) and
forbiddenEnvKey(key)
)
}
predicate envKeyExprForbidden(Expr keyExpr) {
forbiddenEnvKey(keyExpr.getStringValue())
or
exists(VarAccess access, string key |
keyExpr.getUnderlyingValue() = access and
stringConst(access.getVariable(), key) and
forbiddenEnvKey(key)
)
or
exists(VarAccess access |
keyExpr.getUnderlyingValue() = access and
forbiddenEnvLoopVariable(access.getVariable())
)
}
predicate globalAgentKeyExprForbidden(Expr keyExpr) {
forbiddenGlobalAgentKey(keyExpr.getStringValue())
or
exists(VarAccess access, string key |
keyExpr.getUnderlyingValue() = access and
stringConst(access.getVariable(), key) and
forbiddenGlobalAgentKey(key)
)
}
predicate directGlobalExpr(Expr expr) {
namedExpr(expr, "global")
or
namedExpr(expr, "globalThis")
}
predicate globalAlias(Variable variable) {
exists(VariableDeclarator decl |
decl.getBindingPattern().getAVariable() = variable and
directGlobalExpr(decl.getInit())
)
}
predicate globalExpr(Expr expr) {
directGlobalExpr(expr)
or
exists(VarAccess access |
expr.getUnderlyingValue() = access and
globalAlias(access.getVariable())
)
}
predicate directGlobalAgentExpr(Expr expr) {
exists(PropAccess access |
expr.getUnderlyingValue() = access and
access.getPropertyName() = "GLOBAL_AGENT" and
globalExpr(access.getBase())
)
}
predicate globalAgentAlias(Variable variable) {
exists(VariableDeclarator decl |
decl.getBindingPattern().getAVariable() = variable and
directGlobalAgentExpr(decl.getInit())
)
}
predicate globalAgentExpr(Expr expr) {
directGlobalAgentExpr(expr)
or
exists(VarAccess access |
expr.getUnderlyingValue() = access and
globalAgentAlias(access.getVariable())
)
}
predicate envMutationTarget(Expr target) {
exists(PropAccess access |
target.getUnderlyingReference() = access and
processEnvExpr(access.getBase()) and
(
forbiddenEnvKey(access.getPropertyName())
or
envKeyExprForbidden(access.getPropertyNameExpr())
)
)
}
predicate globalAgentMutationTarget(Expr target) {
globalAgentExpr(target)
or
exists(PropAccess access |
target.getUnderlyingReference() = access and
globalAgentExpr(access.getBase()) and
(
forbiddenGlobalAgentKey(access.getPropertyName())
or
globalAgentKeyExprForbidden(access.getPropertyNameExpr())
)
)
}
predicate objectPropertyWithKey(Expr expr, string key) {
exists(ObjectExpr object, Property property |
expr.getUnderlyingValue() = object and
property = object.getAProperty() and
property.getName() = key
)
}
Expr managedProxyRuntimeMutation() {
exists(Assignment assignment |
result = assignment and
(
envMutationTarget(assignment.getTarget())
or
globalAgentMutationTarget(assignment.getTarget())
)
)
or
exists(DeleteExpr delete |
result = delete and
(
envMutationTarget(delete.getOperand())
or
globalAgentMutationTarget(delete.getOperand())
)
)
or
exists(MethodCallExpr call |
result = call and
namedExpr(call.getReceiver(), "Object") and
call.getMethodName() = "assign" and
(
processEnvExpr(call.getArgument(0)) and
exists(string key |
forbiddenEnvKey(key) and
objectPropertyWithKey(call.getArgument(1), key)
)
or
globalAgentExpr(call.getArgument(0)) and
exists(string key |
forbiddenGlobalAgentKey(key) and
objectPropertyWithKey(call.getArgument(1), key)
)
)
)
or
exists(MethodCallExpr call |
result = call and
namedExpr(call.getReceiver(), "Object") and
call.getMethodName() = "defineProperty" and
(
processEnvExpr(call.getArgument(0)) and
envKeyExprForbidden(call.getArgument(1))
or
globalAgentExpr(call.getArgument(0)) and
globalAgentKeyExprForbidden(call.getArgument(1))
)
)
}
predicate allowedFunctionOwnerScope(Expr mutation, string path, string functionName) {
exists(Function owner |
mutation.getFile().getRelativePath() = path and
owner.getFile() = mutation.getFile() and
owner.getName() = functionName and
mutation.getParent*() = owner.getBody()
)
}
predicate allowedMethodOwnerScope(Expr mutation, string path, string methodName) {
exists(MethodDeclaration method |
mutation.getFile().getRelativePath() = path and
method.getFile() = mutation.getFile() and
method.getDeclaringType().getName() + "." + method.getName() = methodName and
mutation.getParent*() = method.getBody().getBody()
)
}
predicate allowedManagedProxyRuntimeMutation(Expr mutation) {
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts", "applyProxyEnv")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts", "restoreProxyEnv")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
"restoreGlobalAgentRuntime")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
"restoreNodeHttpStack")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
"bootstrapNodeHttpStack")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
"writeGlobalAgentNoProxy")
or
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
"disableGlobalAgentProxyForIpv6GatewayLoopback")
or
allowedMethodOwnerScope(mutation, "extensions/browser/src/browser/cdp-proxy-bypass.ts",
"NoProxyLeaseManager.acquire")
or
allowedMethodOwnerScope(mutation, "extensions/browser/src/browser/cdp-proxy-bypass.ts",
"NoProxyLeaseManager.release")
}
from Expr mutation
where
managedProxyRuntimeMutation() = mutation and
relevantSourceFile(mutation.getFile()) and
not allowedManagedProxyRuntimeMutation(mutation)
select mutation,
"Only managed proxy owner scopes may mutate proxy-related process.env or GLOBAL_AGENT runtime state."

View File

@@ -0,0 +1,92 @@
/**
* @name Raw socket client callsite classification
* @description Raw net/tls/http2 client egress must be classified before landing.
* @kind problem
* @problem.severity error
* @precision high
* @id js/openclaw/raw-socket-callsite-classification
* @tags maintainability
* security
* external/cwe/cwe-441
*/
import javascript
predicate rawModule(string moduleName) {
moduleName = ["net", "node:net", "tls", "node:tls", "http2", "node:http2"]
}
predicate netModule(string moduleName) { moduleName = ["net", "node:net"] }
predicate rawConnectMember(string memberName) { memberName = ["connect", "createConnection"] }
predicate relevantSourceFile(File file) {
exists(string path |
path = file.getRelativePath() and
path.regexpMatch("^(src|extensions)/.*\\.ts$") and
not path.regexpMatch(".*\\.(test|spec|test-utils|test-harness|e2e-harness)\\.ts$") and
not path.regexpMatch(".*/test-support/.*") and
not path.regexpMatch("^extensions/diffs/assets/.*")
)
}
Expr rawSocketClientCall() {
exists(API::CallNode call, string moduleName, string memberName |
rawModule(moduleName) and
rawConnectMember(memberName) and
call = API::moduleImport(moduleName).getMember(memberName).getACall() and
result = call.asExpr()
)
or
exists(string moduleName |
netModule(moduleName) and
result =
DataFlow::moduleMember(moduleName, "Socket")
.getAnInstantiation()
.getAMethodCall("connect")
.asExpr()
)
}
predicate allowedOwnerScope(Expr call, string path, string functionName) {
exists(Function owner |
call.getFile().getRelativePath() = path and
owner.getFile() = call.getFile() and
owner.getName() = functionName and
call.getParent*() = owner.getBody()
)
}
predicate allowedRawSocketClientCall(Expr call) {
allowedOwnerScope(call, "src/cli/gateway-cli/run-loop.ts", "waitForGatewayPortReady")
or
allowedOwnerScope(call, "src/infra/ssh-tunnel.ts", "canConnectLocal")
or
allowedOwnerScope(call, "src/infra/gateway-lock.ts", "checkPortFree")
or
allowedOwnerScope(call, "src/infra/jsonl-socket.ts", "requestJsonlSocket")
or
allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "connectToProxy")
or
allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "startTargetTls")
or
allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "openProxiedApnsHttp2Session")
or
allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "connectApnsHttp2Session")
or
allowedOwnerScope(call, "src/proxy-capture/proxy-server.ts", "startDebugProxyServer")
or
allowedOwnerScope(call, "extensions/irc/src/client.ts", "connectIrcClient")
or
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-capture.ts", "probeTcpReachability")
or
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-ui.ts", "proxyUpgradeRequest")
}
from Expr call
where
rawSocketClientCall() = call and
relevantSourceFile(call.getFile()) and
not allowedRawSocketClientCall(call)
select call,
"Classify raw net/tls/http2 client egress as managed/proxied, local-only, diagnostic guarded, or documented unsupported before adding this callsite."

5
.github/labeler.yml vendored
View File

@@ -1,8 +1,3 @@
"channel: bluebubbles":
- changed-files:
- any-glob-to-any-file:
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"plugin: azure-speech":
- changed-files:
- any-glob-to-any-file:

View File

@@ -37,7 +37,7 @@ If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta
## Real behavior proof (required for external PRs)
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count.
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.
- Behavior or issue addressed:
- Real environment tested:

View File

@@ -19,6 +19,7 @@ env:
jobs:
build-artifacts:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "build-artifacts"

View File

@@ -18,6 +18,7 @@ env:
jobs:
check:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check"

View File

@@ -547,11 +547,13 @@ jobs:
path: dist-runtime-build.tar.zst
retention-days: 1
- name: Upload A2UI bundle artifact
- name: Upload bundled plugin asset artifacts
uses: actions/upload-artifact@v7
with:
name: canvas-a2ui-bundle
path: src/canvas-host/a2ui/
name: bundled-plugin-assets
path: |
extensions/*/src/host/**/.bundle.hash
extensions/*/src/host/**/*.bundle.js
include-hidden-files: true
retention-days: 1
@@ -852,7 +854,7 @@ jobs:
name: ${{ matrix.checkName }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ubuntu-24.04
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1461,7 +1463,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
runs-on: ubuntu-24.04
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1740,7 +1742,17 @@ jobs:
with:
install-bun: "false"
- name: Checkout ClawHub docs source
uses: actions/checkout@v6
with:
repository: openclaw/clawhub
path: clawhub-source
fetch-depth: 1
persist-credentials: false
- name: Check docs
env:
OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source
run: pnpm check:docs
skills-python:

View File

@@ -21,17 +21,21 @@ on:
- plugin-sdk-package-contract
- plugin-sdk-reply-runtime
- provider-runtime-boundary
- network-runtime-boundary
- session-diagnostics-boundary
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/codeql/**"
- ".github/workflows/codeql-critical-quality.yml"
- "extensions/*.ts"
- "extensions/**/*.ts"
- "packages/plugin-package-contract/**"
- "packages/plugin-sdk/**"
- "packages/memory-host-sdk/**"
- "src/*.ts"
- "src/**/*.ts"
- "src/config/**"
- "extensions/bluebubbles/src/**"
- "extensions/discord/src/**"
- "extensions/feishu/src/**"
- "extensions/googlechat/src/**"
@@ -144,6 +148,7 @@ permissions:
jobs:
quality-shards:
name: Select Critical Quality shards
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
@@ -158,6 +163,7 @@ jobs:
plugin_sdk_package: ${{ steps.detect.outputs.plugin_sdk_package }}
plugin_sdk_reply: ${{ steps.detect.outputs.plugin_sdk_reply }}
provider: ${{ steps.detect.outputs.provider }}
network_runtime: ${{ steps.detect.outputs.network_runtime }}
session_diagnostics: ${{ steps.detect.outputs.session_diagnostics }}
steps:
- name: Detect PR shard paths
@@ -181,6 +187,7 @@ jobs:
plugin_sdk_package=false
plugin_sdk_reply=false
provider=false
network_runtime=false
session_diagnostics=false
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
@@ -195,6 +202,7 @@ jobs:
plugin_sdk_package=true
plugin_sdk_reply=true
provider=true
network_runtime=true
session_diagnostics=true
else
while IFS= read -r file; do
@@ -211,6 +219,7 @@ jobs:
plugin_sdk_package=true
plugin_sdk_reply=true
provider=true
network_runtime=true
session_diagnostics=true
;;
src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts)
@@ -219,7 +228,7 @@ jobs:
src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts)
session_diagnostics=true
;;
extensions/bluebubbles/src/*|extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
channel=true
;;
src/config/*)
@@ -280,6 +289,12 @@ jobs:
plugin_sdk_package=true
;;
esac
case "${file}" in
src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts)
network_runtime=true
;;
esac
done < <(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename')
fi
@@ -295,6 +310,7 @@ jobs:
echo "plugin_sdk_package=${plugin_sdk_package}"
echo "plugin_sdk_reply=${plugin_sdk_reply}"
echo "provider=${provider}"
echo "network_runtime=${network_runtime}"
echo "session_diagnostics=${session_diagnostics}"
} >> "${GITHUB_OUTPUT}"
@@ -390,6 +406,62 @@ jobs:
with:
category: "/codeql-critical-quality/channel-runtime-boundary"
network-runtime-boundary:
name: Critical Quality (network-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.network_runtime == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'network-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
- name: Analyze
id: analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
output: sarif-results
category: "/codeql-critical-quality/network-runtime-boundary"
- name: Fail on network runtime boundary findings
env:
SARIF_OUTPUT: sarif-results
run: |
set -euo pipefail
shopt -s nullglob
files=("$SARIF_OUTPUT"/*.sarif)
if [ "${#files[@]}" -eq 0 ]; then
echo "No SARIF files found in $SARIF_OUTPUT" >&2
exit 1
fi
findings="$(jq -s '[.[].runs[]?.results[]?] | length' "${files[@]}")"
if [ "$findings" = "0" ]; then
exit 0
fi
echo "Found ${findings} network runtime boundary finding(s):" >&2
jq -r '
.runs[]?.results[]?
| .locations[0].physicalLocation as $location
| "- "
+ ($location.artifactLocation.uri // "unknown")
+ ":"
+ (($location.region.startLine // 0) | tostring)
+ " "
+ (.message.text // .ruleId)
' "${files[@]}" >&2
exit 1
agent-runtime-boundary:
name: Critical Quality (agent-runtime-boundary)
needs: quality-shards

View File

@@ -22,6 +22,15 @@ jobs:
with:
fetch-depth: 0
- name: Checkout ClawHub docs source
uses: actions/checkout@v6
with:
repository: openclaw/clawhub
path: clawhub-source
fetch-depth: 1
persist-credentials: false
token: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -48,12 +57,17 @@ jobs:
- name: Sync docs into publish repo
run: |
clawhub_sha="$(git -C "$GITHUB_WORKSPACE/clawhub-source" rev-parse HEAD)"
node scripts/docs-sync-publish.mjs \
--target "$GITHUB_WORKSPACE/publish" \
--source-repo "$GITHUB_REPOSITORY" \
--source-sha "$GITHUB_SHA"
--source-sha "$GITHUB_SHA" \
--clawhub-repo "$GITHUB_WORKSPACE/clawhub-source" \
--clawhub-source-repo "openclaw/clawhub" \
--clawhub-source-sha "$clawhub_sha"
- name: Install docs MDX checker dependency
working-directory: publish
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
- name: Check publish docs MDX

View File

@@ -1,4 +1,4 @@
name: Docs Trigger Locale Translate On Release
name: Docs Trigger Translations On Release
on:
release:
@@ -12,36 +12,16 @@ jobs:
dispatch-translate:
runs-on: ubuntu-latest
steps:
- name: Trigger locale translates in publish repo
- name: Trigger translation coordinator in publish repo
env:
GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
for event_type in \
translate-zh-cn-release \
translate-zh-tw-release \
translate-ja-jp-release \
translate-es-release \
translate-pt-br-release \
translate-ko-release \
translate-de-release \
translate-fr-release \
translate-ar-release \
translate-it-release \
translate-vi-release \
translate-nl-release \
translate-fa-release \
translate-tr-release \
translate-uk-release \
translate-id-release \
translate-pl-release \
translate-th-release
do
gh api repos/openclaw/docs/dispatches \
--method POST \
-f event_type="${event_type}" \
-f client_payload[release_tag]="${RELEASE_TAG}" \
-f client_payload[source_repository]="${GITHUB_REPOSITORY}" \
-f client_payload[source_sha]="${GITHUB_SHA}"
done
gh api repos/openclaw/docs/dispatches \
--method POST \
-f event_type="translate-all-release" \
-f client_payload[mode]="incremental" \
-f client_payload[release_tag]="${RELEASE_TAG}" \
-f client_payload[source_repository]="${GITHUB_REPOSITORY}" \
-f client_payload[source_sha]="${GITHUB_SHA}"

View File

@@ -98,5 +98,5 @@ jobs:
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml\` with tag \`${RELEASE_TAG}\` and wait for the private mac validation lane to pass."
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\` and \`preflight_only=true\` for the full private mac preflight."
echo "- For the real publish path, run the same private mac publish workflow from \`main\` with the successful private preflight \`preflight_run_id\` so it promotes the prepared artifacts instead of rebuilding them."
echo "- For stable releases, also download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
echo "- For stable releases, the private publish workflow also publishes the signed \`appcast.xml\` to public \`main\`, or opens an appcast PR if direct push is blocked."
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -245,6 +245,24 @@ jobs:
- name: Build Mantis harness
run: pnpm build
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
with:
go-version: "1.26.x"
cache: false
- name: Install Crabbox CLI
shell: bash
run: |
set -euo pipefail
install_dir="${RUNNER_TEMP}/crabbox"
mkdir -p "$install_dir" "$HOME/.local/bin"
git clone --depth 1 https://github.com/openclaw/crabbox.git "$install_dir/src"
go build -C "$install_dir/src" -o "$HOME/.local/bin/crabbox" ./cmd/crabbox
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/crabbox" --version
"$HOME/.local/bin/crabbox" warmup --help 2>&1 | grep -q -- "-desktop"
- name: Prepare baseline and candidate worktrees
shell: bash
env:
@@ -307,6 +325,14 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_DISCORD_CAPTURE_CONTENT: "1"
MANTIS_DISCORD_VIEWER_CHROME_PROFILE_TGZ_B64: ${{ secrets.MANTIS_DISCORD_VIEWER_CHROME_PROFILE_TGZ_B64 }}
MANTIS_DISCORD_VIEWER_CHROME_PROFILE_DIR: ${{ vars.MANTIS_DISCORD_VIEWER_CHROME_PROFILE_DIR }}
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }}
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }}
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
CANDIDATE_SHA: ${{ needs.validate_candidate.outputs.candidate_revision }}
BASELINE_LABEL: ${{ needs.resolve_request.outputs.baseline_ref }}
run: |
@@ -331,7 +357,14 @@ jobs:
local lane="$1"
local repo_root="${GITHUB_WORKSPACE}/${worktree_root}/${lane}"
local output_dir=".artifacts/qa-e2e/mantis/discord-thread-attachment/${lane}"
pnpm --dir "$repo_root" openclaw qa discord \
local lane_env=()
if [[ "$lane" == "candidate" ]]; then
lane_env=(
OPENCLAW_QA_DISCORD_CAPTURE_UI_METADATA=1
OPENCLAW_QA_DISCORD_KEEP_THREADS=1
)
fi
env "${lane_env[@]}" pnpm --dir "$repo_root" openclaw qa discord \
--repo-root "$repo_root" \
--output-dir "$output_dir" \
--provider-mode mock-openai \
@@ -347,6 +380,73 @@ jobs:
run_lane baseline
run_lane candidate
capture_candidate_discord_web() {
if [[ -z "${MANTIS_DISCORD_VIEWER_CHROME_PROFILE_TGZ_B64:-}" && -z "${MANTIS_DISCORD_VIEWER_CHROME_PROFILE_DIR:-}" ]]; then
echo "::notice::No Mantis Discord viewer browser profile is configured; skipping logged-in Discord Web video."
return 0
fi
CRABBOX_COORDINATOR="${CRABBOX_COORDINATOR:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR:-}}"
CRABBOX_COORDINATOR_TOKEN="${CRABBOX_COORDINATOR_TOKEN:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN:-}}"
export CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN
if [[ -z "${CRABBOX_COORDINATOR_TOKEN:-}" ]]; then
echo "::warning::Crabbox coordinator token missing; skipping logged-in Discord Web video."
return 0
fi
local ui_json="$root/candidate/discord-thread-reply-filepath-attachment-ui.json"
if [[ ! -f "$ui_json" ]]; then
echo "::warning::Candidate Discord UI metadata is missing; skipping logged-in Discord Web video."
return 0
fi
local discord_url
discord_url="$(jq -r '.discordWebUrl // empty' "$ui_json")"
if [[ -z "$discord_url" ]]; then
echo "::warning::Candidate Discord UI URL is empty; skipping logged-in Discord Web video."
return 0
fi
local desktop_dir="$root/candidate/discord-web"
local profile_args=()
if [[ -n "${MANTIS_DISCORD_VIEWER_CHROME_PROFILE_TGZ_B64:-}" ]]; then
profile_args+=(--browser-profile-archive-env MANTIS_DISCORD_VIEWER_CHROME_PROFILE_TGZ_B64)
fi
if [[ -n "${MANTIS_DISCORD_VIEWER_CHROME_PROFILE_DIR:-}" ]]; then
profile_args+=(--browser-profile-dir "$MANTIS_DISCORD_VIEWER_CHROME_PROFILE_DIR")
fi
pnpm openclaw qa mantis desktop-browser-smoke \
--browser-url "$discord_url" \
"${profile_args[@]}" \
--video-duration 24 \
--output-dir "$desktop_dir" \
--provider hetzner \
--class standard \
--idle-timeout 30m \
--ttl 90m
cp "$desktop_dir/desktop-browser-smoke.png" "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.png"
if [[ -f "$desktop_dir/desktop-browser-smoke.mp4" ]]; then
cp "$desktop_dir/desktop-browser-smoke.mp4" "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.mp4"
fi
if [[ -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.mp4" ]]; then
if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v ffprobe >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y ffmpeg || true
fi
crabbox media preview \
--input "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.mp4" \
--output "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-preview.gif" \
--trimmed-video-output "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-change.mp4" \
--json > "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-preview.json" || {
rm -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-preview.gif"
rm -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-change.mp4"
rm -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-preview.json"
echo "::warning::Could not generate logged-in Discord Web motion preview; keeping screenshot/full MP4."
}
fi
}
capture_candidate_discord_web
baseline_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/baseline/discord-qa-summary.json")"
candidate_status="$(jq -r '.scenarios[] | select(.id == "discord-thread-reply-filepath-attachment") | .status' "$root/candidate/discord-qa-summary.json")"
comparison_status="fail"
@@ -380,6 +480,18 @@ jobs:
echo "- Result: \`${comparison_status}\`"
echo "- Baseline screenshot: \`baseline/discord-thread-reply-filepath-attachment-attachment.png\`"
echo "- Candidate screenshot: \`candidate/discord-thread-reply-filepath-attachment-attachment.png\`"
if [[ -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.png" ]]; then
echo "- Candidate logged-in Discord Web screenshot: \`candidate/discord-thread-reply-filepath-attachment-discord-web.png\`"
fi
if [[ -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-preview.gif" ]]; then
echo "- Candidate logged-in Discord Web preview: \`candidate/discord-thread-reply-filepath-attachment-discord-web-preview.gif\`"
fi
if [[ -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web-change.mp4" ]]; then
echo "- Candidate logged-in Discord Web change clip: \`candidate/discord-thread-reply-filepath-attachment-discord-web-change.mp4\`"
fi
if [[ -f "$root/candidate/discord-thread-reply-filepath-attachment-discord-web.mp4" ]]; then
echo "- Candidate logged-in Discord Web video: \`candidate/discord-thread-reply-filepath-attachment-discord-web.mp4\`"
fi
} > "$root/mantis-report.md"
jq -n \
@@ -402,6 +514,12 @@ jobs:
artifacts: [
{ kind: "timeline", lane: "baseline", label: "Baseline missing filePath attachment", path: "baseline/discord-thread-reply-filepath-attachment-attachment.png", targetPath: "baseline.png", alt: "Baseline Discord thread reply without filePath attachment", width: 420 },
{ kind: "timeline", lane: "candidate", label: "Candidate includes filePath attachment", path: "candidate/discord-thread-reply-filepath-attachment-attachment.png", targetPath: "candidate.png", alt: "Candidate Discord thread reply with filePath attachment", width: 420 },
{ kind: "desktopScreenshot", lane: "candidate", label: "Candidate logged-in Discord Web", path: "candidate/discord-thread-reply-filepath-attachment-discord-web.png", targetPath: "candidate-discord-web.png", alt: "Logged-in Discord Web showing the candidate thread attachment", width: 560, required: false, inline: true },
{ kind: "motionPreview", lane: "candidate", label: "Candidate logged-in Discord Web motion", path: "candidate/discord-thread-reply-filepath-attachment-discord-web-preview.gif", targetPath: "candidate-discord-web-preview.gif", alt: "Animated logged-in Discord Web proof for the candidate thread attachment", width: 560, required: false, inline: true },
{ kind: "motionClip", lane: "candidate", label: "Candidate logged-in Discord Web change MP4", path: "candidate/discord-thread-reply-filepath-attachment-discord-web-change.mp4", targetPath: "candidate-discord-web-change.mp4", required: false },
{ kind: "fullVideo", lane: "candidate", label: "Candidate logged-in Discord Web MP4", path: "candidate/discord-thread-reply-filepath-attachment-discord-web.mp4", targetPath: "candidate-discord-web.mp4", required: false },
{ kind: "metadata", lane: "candidate", label: "Candidate logged-in Discord Web preview metadata", path: "candidate/discord-thread-reply-filepath-attachment-discord-web-preview.json", targetPath: "candidate-discord-web-preview.json", required: false },
{ kind: "metadata", lane: "candidate", label: "Candidate Discord UI metadata", path: "candidate/discord-thread-reply-filepath-attachment-ui.json", targetPath: "candidate-discord-ui.json", required: false },
{ kind: "metadata", lane: "run", label: "Comparison JSON", path: "comparison.json", targetPath: "comparison.json" },
{ kind: "report", lane: "run", label: "Mantis report", path: "mantis-report.md", targetPath: "mantis-report.md" }
]

View File

@@ -1910,7 +1910,7 @@ jobs:
profiles: stable full
- suite_id: native-live-src-gateway-profiles-minimax
label: Native live gateway profiles MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 90
profile_env_only: false
profiles: stable full
@@ -2135,8 +2135,8 @@ jobs:
# inside the already-isolated container to keep MCP cron/tool
# execution representative instead of failing on nested sandbox
# setup.
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
@@ -2212,7 +2212,7 @@ jobs:
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=30000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=60000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 25m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=30000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=60000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 25m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 30
profile_env_only: false
profiles: stable full
@@ -2354,8 +2354,8 @@ jobs:
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"

View File

@@ -33,14 +33,19 @@ on:
required: false
type: string
publish_openclaw_npm:
description: Publish the OpenClaw npm package after plugin npm and ClawHub publish complete
description: Publish the OpenClaw npm package after plugin npm succeeds; ClawHub may still run
required: true
default: true
type: boolean
wait_for_clawhub:
description: Wait for ClawHub plugin publish before marking this workflow complete
required: true
default: false
type: boolean
permissions:
actions: write
contents: read
contents: write
concurrency:
group: openclaw-release-publish-${{ inputs.tag }}
@@ -166,18 +171,19 @@ jobs:
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
run: |
set -euo pipefail
dispatch_and_wait() {
dispatch_workflow() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
local before_json dispatch_output run_id
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
printf '%s\n' "$dispatch_output" >&2
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
@@ -202,24 +208,34 @@ jobs:
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
{
echo "- ${workflow}: dispatched (https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id})"
} >> "$GITHUB_STEP_SUMMARY"
printf '%s\n' "${run_id}"
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel --repo "$GITHUB_REPOSITORY" "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
wait_for_run() {
local workflow="$1"
local run_id="$2"
local status conclusion url updated_at last_state
last_state=""
while true; do
status="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status --jq '.status')"
run_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,url,updatedAt)"
status="$(printf '%s' "$run_json" | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
url="$(printf '%s' "$run_json" | jq -r '.url')"
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
state="${status}:${updated_at}"
if [[ "$state" != "$last_state" ]]; then
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
last_state="$state"
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json url --jq '.url')"
@@ -229,8 +245,69 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$conclusion" != "success" ]]; then
gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
return 1
fi
}
wait_for_run_background() {
local workflow="$1"
local run_id="$2"
local result_file="$3"
(
if wait_for_run "${workflow}" "${run_id}"; then
printf 'success\n' > "${result_file}"
else
printf 'failure\n' > "${result_file}"
fi
) &
wait_run_pid="$!"
}
create_or_update_github_release() {
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
release_version="${RELEASE_TAG#v}"
notes_version="${release_version}"
if [[ "${notes_version}" =~ ^([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*)-(alpha|beta)\.[1-9][0-9]*$ ]]; then
notes_version="${BASH_REMATCH[1]}"
fi
title="openclaw ${release_version}"
changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
notes_file="${RUNNER_TEMP}/release-notes.md"
gh api --repo "$GITHUB_REPOSITORY" "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=${TARGET_SHA}" \
--jq '.content' | base64 --decode > "${changelog_file}"
awk -v version="${notes_version}" '
$0 == "## " version { in_section = 1; next }
/^## / && in_section { exit }
in_section { print }
' "${changelog_file}" > "${notes_file}"
if [[ ! -s "${notes_file}" ]]; then
echo "CHANGELOG.md does not contain release notes for ${notes_version}." >&2
exit 1
fi
prerelease_args=()
latest_arg="--latest=false"
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
prerelease_args=(--prerelease)
elif [[ "${RELEASE_NPM_DIST_TAG}" == "latest" ]]; then
latest_arg="--latest"
fi
if gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
--title "${title}" \
--notes-file "${notes_file}" \
"${prerelease_args[@]}"
else
gh release create "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "${title}" \
--notes-file "${notes_file}" \
"${prerelease_args[@]}" \
"${latest_arg}"
fi
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
}
{
@@ -239,6 +316,17 @@ jobs:
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- OpenClaw npm publish: starts after plugin npm succeeds; ClawHub may still be running"
else
echo "- OpenClaw npm publish: skipped by input"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
echo "- Workflow completion waits for ClawHub"
else
echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately"
fi
} >> "$GITHUB_STEP_SUMMARY"
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
@@ -248,15 +336,63 @@ jobs:
clawhub_args+=(-f plugins="${PLUGINS}")
fi
dispatch_and_wait plugin-npm-release.yml "${npm_args[@]}"
dispatch_and_wait plugin-clawhub-release.yml "${clawhub_args[@]}"
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
exit 1
fi
openclaw_npm_run_id=""
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
dispatch_and_wait openclaw-npm-release.yml \
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
-f tag="${RELEASE_TAG}" \
-f preflight_only=false \
-f preflight_run_id="${PREFLIGHT_RUN_ID}" \
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}"
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
else
echo "- OpenClaw npm publish: skipped by input" >> "$GITHUB_STEP_SUMMARY"
fi
clawhub_result=""
clawhub_pid=""
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
clawhub_pid="${wait_run_pid}"
else
echo "- plugin-clawhub-release.yml: not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
openclaw_result=""
openclaw_pid=""
if [[ -n "${openclaw_npm_run_id}" ]]; then
openclaw_result="$RUNNER_TEMP/openclaw-npm-result.txt"
wait_run_pid=""
wait_for_run_background openclaw-npm-release.yml "${openclaw_npm_run_id}" "${openclaw_result}"
openclaw_pid="${wait_run_pid}"
fi
failed=0
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
failed=1
fi
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
failed=1
fi
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
failed=1
fi
if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then
failed=1
fi
if [[ "${failed}" != "0" ]]; then
exit 1
fi
if [[ -n "${openclaw_npm_run_id}" ]]; then
create_or_update_github_release
fi

View File

@@ -182,7 +182,7 @@ jobs:
contents: read
strategy:
fail-fast: false
max-parallel: 6
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
@@ -263,7 +263,7 @@ jobs:
id-token: write
strategy:
fail-fast: false
max-parallel: 6
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:

3
.gitignore vendored
View File

@@ -68,6 +68,8 @@ apps/ios/*.xcfilelist
vendor/a2ui/renderers/lit/dist/
src/canvas-host/a2ui/*.bundle.js
src/canvas-host/a2ui/*.map
extensions/canvas/src/host/a2ui/*.bundle.js
extensions/canvas/src/host/a2ui/*.map
.bundle.hash
# fastlane (iOS)
@@ -220,3 +222,4 @@ extensions/**/.openclaw-runtime-deps-stamp.json
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
/.opengrep-out/
/.crabbox-artifacts
.comux*

View File

@@ -14,6 +14,7 @@
"docker-compose.yml",
"dist/",
"docs/_layouts/",
"**/*.json",
"node_modules/",
"patches/",
"pnpm-lock.yaml/",

View File

@@ -32,10 +32,16 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- 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.
- Dependency ownership follows runtime ownership: extension-only deps stay plugin-local; root deps only for core imports or intentionally internalized bundled plugin runtime.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- No legacy compatibility in core/runtime paths. When old config/store shapes need support, add an `openclaw doctor --fix` rewrite/repair rule with tests and keep runtime code on the canonical contract.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
- Channels: `src/channels/**` is implementation; plugin authors get SDK seams.
- Providers: core owns generic loop; provider plugins own auth/catalog/runtime hooks.
- Request-time runtime resolution: when a path already knows the provider id, model ref, channel id, outbound target, capability family, or attachment class, carry that as a prepared runtime fact instead of rediscovering it later.
- Prepared runtime facts should be small typed values produced once near startup, reply dispatch, model selection, tool planning, or channel resolution, then passed through context to consumers. Prefer `AgentRuntimePlan`, `ProviderRuntimePluginHandle`, scoped model/catalog helpers, active/runtime registries, manifest/public-artifact lookups, single-provider resolvers, and lazy registry construction.
- Avoid broad request-time rediscovery: hot reply/tool/outbound/media paths should not call broad plugin/provider/channel/capability loaders such as `loadOpenClawPlugins`, `resolveProviderPluginsForHooks`, `resolvePluginCapabilityProviders`, `resolvePluginDiscoveryProvidersRuntime`, `getChannelPlugin`, or broad model/tool/media registry builders just to answer a question the caller already knows. Do not build multimodal/provider registries for document-only or otherwise non-participating paths.
- Compatibility fallbacks are allowed only for startup/setup/admin/standalone/legacy callers that genuinely lack prepared facts. Keep them explicit, tested, and outside migrated hot reply/tool/outbound paths.
- Do not fix repeated request-time discovery by adding scattered cache layers. Move the canonical fact earlier, reuse the existing prepared-runtime object, and delete duplicate lookup branches when the last migrated caller stops needing them.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor.
- Direction: manifest-first control plane; targeted runtime loaders; no hidden contract bypasses; broad mutable registries transitional.
@@ -57,8 +63,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- 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`.
- Crabbox: preferred live scenario runner when available. It has Linux, Windows, and macOS workers/targets; pick the OS that matches the bug. If unavailable, use the local system, Docker, Parallels, or CI live lane that proves the same behavior.
- Blacksmith/Testbox: on maintainer machines with Blacksmith access, broad/shared validation defaults to Testbox. This includes `pnpm check`, `pnpm check:changed`, `pnpm test`, `pnpm test:changed`, Docker/E2E/live/package/build gates, and any command likely to fan out across many Vitest projects. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops only, such as `pnpm test <specific-file>`, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Blacksmith/Testbox: use when the validation needs the remote environment, broad/shared suite capacity, cross-OS/package/Docker/E2E/live proof, or another end-to-end setup that is meaningfully better off-host. Broad fan-out commands such as `pnpm check`, full `pnpm test`, Docker/E2E/live/package/build gates, and wide changed gates belong in Testbox by default. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops stay local, such as `pnpm test <specific-file>`, narrow `pnpm test:changed` selections, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Testbox use: run from repo root, pre-warm early with `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`, reuse the returned `tbx_...` id for all `run`/`download` commands, and stop boxes you created before handoff. Timeout bins: `90` minutes default, `240` multi-hour, `720` all-day, `1440` overnight; anything above `1440` needs explicit approval and cleanup.
- Testbox full-suite profile: `blacksmith testbox run --id <ID> "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test"`. For installable package proof, prefer the GitHub `Package Acceptance` workflow over ad hoc Testbox commands.
@@ -98,8 +104,8 @@ 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 for code/test/runtime/config changes: run `pnpm check:changed` in Testbox by default on maintainer machines. Tests-only: run `pnpm test:changed` in Testbox by default. Full prod sweep: run `pnpm check` in Testbox. Use local only for narrow targeted proof or when explicitly requested.
- If `pnpm test:changed` or `pnpm check:changed` selects broad/shared lanes, it belongs in Testbox; do not let it continue locally after it fans out.
- Before handoff/push for code/test/runtime/config changes: prove the touched surface. Use local targeted tests/checks for narrow changes; use Testbox when `pnpm check:changed`, `pnpm test:changed`, or other validation selects broad/shared lanes or needs a remote/end-to-end environment. Full prod sweeps (`pnpm check`, full `pnpm test`) belong in Testbox by default on maintainer machines.
- If `pnpm test:changed` or `pnpm check:changed` stays narrowly scoped, it can run locally. If it fans out into broad/shared lanes, stop it and move the broad gate to Testbox.
- 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
@@ -189,7 +195,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Mac gateway: dev watch = `pnpm gateway:watch` (tmux `openclaw-gateway-watch-main`, auto-attach). Noninteractive: `OPENCLAW_GATEWAY_WATCH_ATTACH=0 pnpm gateway:watch`; attach/stop: `tmux attach -t openclaw-gateway-watch-main` / `tmux kill-session -t openclaw-gateway-watch-main`. Managed installs: `openclaw gateway restart/status --deep`. No launchd/ad-hoc tmux. Logs: `./scripts/clawlog.sh`.
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
- Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
- A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
- A2UI hash `extensions/canvas/src/host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
## Ops / Footguns

View File

@@ -6,13 +6,57 @@ Docs: https://docs.openclaw.ai
### Changes
- Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack.
- Control UI/usage: add transcript-backed historical lineage rollups for rotated logical sessions, with current-instance vs historical-lineage scope controls and long-range presets so usage history stays visible after restarts and updates. Fixes #50701. Thanks @dev-gideon-llc and @BunsDev.
- Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev.
- Channels/streaming: make progress draft labels scroll away with other progress lines, render structured tool rows as compact emoji/title/details, show web-search queries from provider-native argument shapes, and skip empty Discord apply-patch starts until a patch summary exists. (#79146)
- Telegram: preserve the channel-specific 10-option poll cap in the unified outbound adapter so over-limit polls are rejected before send. (#78762) Thanks @obviyus.
- Slack: route handled top-level channel turns in implicit-conversation channels to thread-scoped sessions when Slack reply threading is enabled, keeping the root turn and later thread replies on one OpenClaw session. (#78522) Thanks @zeroth-blip.
- Telegram: re-probe the primary fetch transport after repeated sticky fallback success so transient IPv4 or pinned-IP fallback promotion can recover without a gateway restart. Fixes #77088. (#77157) Thanks @MkDev11.
- Runtime/install: raise the supported Node 22 floor to `22.16+` so native SQLite query handling can rely on the `node:sqlite` statement metadata API while continuing to recommend Node 24. (#78921)
- Discord/voice: make duplicate same-guild auto-join entries resolve to the last configured channel so moving an agent between voice channels does not keep joining the stale channel.
- Discord/voice: include a bounded one-line STT transcript preview in verbose voice logs so live voice debugging shows what speakers said before the agent reply.
- Codex app-server: pin the managed Codex harness and Codex CLI smoke package to `@openai/codex@0.129.0`, defer OpenClaw integration dynamic tools behind Codex tool search by default, and accept current Codex service-tier values so legacy `fast` settings survive the stable harness upgrade as `priority`.
- Codex app-server: default implicit local stdio app-server permissions to guardian when Codex system requirements disallow the YOLO approval, reviewer, or sandbox value, including hostname-scoped remote sandbox entries, avoiding turn-start failures on managed hosts that permit only reviewed approval or narrower sandboxes.
- Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner.
- Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics.
- iMessage: expose native private-API message actions through `imsg rpc` for reactions, edits, unsends, replies, rich sends, attachments, and group management when `imsg status --json` reports the required bridge capabilities.
- Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana.
- Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever.
- Gateway/sessions: keep session-store index writes atomic while skipping durable fsync inside the writer lock, reducing cron and channel-turn starvation on slow filesystems and addressing the session-store strand of #73655. Thanks @mmartoccia.
- Discord/voice: make `openclaw channels capabilities --channel discord --target channel:<id>` and `channels status --probe` audit voice-channel permissions, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`.
- Channels CLI: make `openclaw channels list` channel-only — drop the `Auth providers (OAuth + API keys)` block (use `openclaw models auth list`), drop the per-provider usage/quota fetch and the `--no-usage` flag (use `openclaw status` or `openclaw models list`), add `--all` to surface bundled-unconfigured, catalog-not-installed, and catalog-installed-but-unconfigured channels, and render explicit `installed` / `configured` / `enabled` tags per row plus an `origin` + `installed` field in JSON. Fixes WeCom-class catalog channels disappearing from `--all` when installed on disk but not yet configured. (#78456) Thanks @sliverp.
- CLI/cron: add computed `status` field to `cron list --json` and `cron show <id> --json` output, mirroring the human-readable status column (disabled/running/ok/error/skipped/idle) so external tooling can determine job state without re-deriving it from raw state fields. (#78701) Thanks @aweiker.
- Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add `voice.captureSilenceGraceMs` for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.
- Discord/streaming: default Discord replies to progress draft previews so tool/work activity appears in one edited Discord message unless `channels.discord.streaming.mode` is set to `off`.
- OpenAI: support `openai/chat-latest` as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.
- OpenAI/realtime: default realtime voice to `gpt-realtime-2`, use the GA Realtime WebSocket session shape for backend OpenAI bridges, and cover backend, WebRTC, Google Live, and Gateway relay paths in the live Talk smoke. (#79130)
- Plugins/install: add `npm-pack:<path.tgz>` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins.
- Channels/plugins: show configured official external channels as missing-plugin status rows and send errors with exact install/doctor repair commands after raw package-manager upgrades leave Feishu or WhatsApp uninstalled. Fixes #78702 and #78593. Thanks @MarkMa84 and @mkupiainen.
- Codex app-server: disarm the short post-tool completion watchdog after current-turn activity, expose `appServer.turnCompletionIdleTimeoutMs`, and include raw assistant item context in idle-timeout diagnostics so status-only post-tool stalls stop failing as idle. Fixes #77984. Thanks @roseware-dev and @rubencu.
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
- Contributor PRs: remind external contributors to redact private information like IP addresses, API keys, phone numbers, and non-public endpoints from real behavior proof. Thanks @pashpashpash.
- Codex/approvals: in Codex approval modes, stop installing the pre-guardian native `PermissionRequest` hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember `allow-always` decisions for identical Codex native `PermissionRequest` payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.
- Codex/plugins: enable migrated source-installed `openai-curated` Codex plugins in the same Codex harness thread with explicit `codexPlugins` config, cached app readiness, and fail-closed destructive-action policy. Thanks @kevinslin.
- Codex/plugins: enforce native plugin destructive-action policy with Codex app-level `destructive_enabled` config instead of OpenClaw-maintained per-tool deny lists, leave plugin app `open_world_enabled` on by default, and invalidate existing plugin app thread bindings so old generated app config is rebuilt. Thanks @kevinslin.
- PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement.
- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc.
- ACPX/Codex: preserve trusted Codex project declarations when launching isolated Codex ACP sessions, avoiding interactive trust prompts in headless runs. Thanks @Stedyclaw.
- ACPX/Codex: reap stale OpenClaw-owned ACPX/Codex ACP process trees on startup and after ACP session close, preventing orphaned harness processes from slowing the Gateway. Thanks @91wan.
- ACP bridge: implement stable session list, resume, and close handlers so ACP clients can page Gateway sessions, rebind existing sessions without replay, and close bridge sessions cleanly. Thanks @amknight.
- ACP bridge: replay complete ledger-backed ACP sessions on load, including user prompts, tool updates, session metadata, and usage snapshots, while keeping older sessions on the existing transcript fallback. Thanks @amknight.
- ACP sessions: allow parent agents to inspect and message their own spawned cross-agent ACP sessions without enabling broad agent-to-agent visibility. Thanks @barronlroth.
- Talk/voice: unify realtime relay, transcription relay, managed-room handoff, Voice Call, Google Meet, VoiceClaw, and native clients around a shared Talk session controller and add the Gateway-managed `talk.session.*` RPC surface.
- Diagnostics/Talk: export bounded Talk lifecycle/audio metrics and session recovery metrics through OpenTelemetry and Prometheus without exposing transcripts, audio payloads, room ids, turn ids, or session ids.
- Logging/Talk: route shared Talk lifecycle events into bounded file and OTLP log records while keeping transcript text, audio payloads, turn ids, call ids, and provider item ids out of logs.
- Google Meet/Voice Call: make Twilio dial-in joins speak through the realtime Gemini voice bridge with paced audio streaming, backpressure-aware buffering, barge-in queue clearing, same-session agent consult routing, duplicate-consult coalescing, and no TwiML fallback during realtime speech, giving Meet participants a much snappier OpenClaw voice agent. (#77064) Thanks @scoootscooob.
- Voice Call/realtime: add opt-in OpenClaw agent voice context capsules and consult-cadence guidance so Gemini/OpenAI realtime calls can sound like the configured agent without consulting the full agent on every ordinary turn. Thanks @scoootscooob.
- Docker/Gateway: harden the gateway container by dropping `NET_RAW` and `NET_ADMIN` capabilities and enabling `no-new-privileges` in the bundled `docker-compose.yml`. Thanks @VintageAyu.
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
- Telegram/streaming: keep draft preview rotation from reusing a pre-tool assistant preview after visible tool or media output lands between compaction replay and the next assistant message. Thanks @vincentkoc.
- Telegram/performance: skip non-forum topic-cache setup, defer status reaction variant work until reactions are needed, and reuse ack reaction gating during message context assembly. Thanks @vincentkoc.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc.
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
@@ -26,26 +70,36 @@ Docs: https://docs.openclaw.ai
- Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792.
- Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data.
- Slack/streaming: keep the newest rich progress lines when Block Kit limits trim long progress drafts. Thanks @vincentkoc.
- Slack/performance: reduce message preparation, stream recipient lookup, and thread-context allocation overhead on Slack reply hot paths. Thanks @vincentkoc.
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
- Control UI/chat: add an agent-first filter to the chat session picker, keep chat controls/composer responsive across phone/tablet/desktop widths, keep desktop chat controls on one row, avoid duplicate avatar refreshes during initial chat load, and hide that row while scrolling down the transcript. Thanks @BunsDev.
- Control UI/chat: collapse consecutive duplicate text messages into one bubble with a count so repeated text-only messages stay compact without hiding nearby context.
- Control UI/chat and Sessions: label inherited thinking defaults separately from explicit overrides while preserving provider-supplied option labels. Fixes #77581. Thanks @BunsDev and @Beandon13.
- Agents/runtime: add prepared runtime foundation contracts for carrying provider, model, tool, TTS, and outbound runtime facts through later reply-path migrations. Thanks @mcaxtr.
- Control UI/WhatsApp: keep Show QR available for unlinked WhatsApp accounts while switching linked accounts to the explicit Relink action and showing Wait for scan only when a QR is active. Thanks @BunsDev.
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
- TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc.
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
- Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd.
- Gateway/performance: reuse current plugin metadata for provider activation, auth/env candidate lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin discovery roots differ. Thanks @shakkernerd.
- Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd.
- Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd.
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and opt-in sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
- QA/Mantis: add an opt-in Discord thread attachment before/after scenario that creates a real thread, calls `message.thread-reply` with `filePath`, and captures baseline/candidate screenshot evidence.
- Discord: preserve `filePath` and `path` attachments when replying to a thread with the message tool.
- Discord/message: parse provider-prefixed targets like `discord:channel:<id>` as channel sends instead of legacy Discord DM targets, so cross-channel agent `message(action="send")` calls no longer misroute channel IDs into misleading `Unknown Channel` failures. Fixes #78572. (#78625) Thanks @Patrick-Erichsen.
- QA/Mantis: add visual desktop tasks with Crabbox MP4 recording, screenshot capture, and optional image-understanding assertions, and preserve video artifacts in Mantis before/after reports.
- QA/WhatsApp: add `pnpm openclaw qa whatsapp` for live DM canary and pairing-gate coverage using two pre-linked WhatsApp Web sessions from the QA credential pool.
- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.
- QA/Mantis: return the copied Slack desktop screenshot path even when remote Slack QA fails, so the CLI still prints the failure screenshot artifact. Thanks @vincentkoc.
- QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc.
- QA/Codex harness: add targeted live Docker/Testbox diagnostics, auth preflight checks, cache mount fixes, and app-server protocol checkout discovery so maintainer harness failures are easier to reproduce. Thanks @vincentkoc.
- CI/Crabbox: default owned AWS fallback to `standard` multi-region capacity with broker hints enabled, reserving `beast` for explicit CPU-bound maintainer lanes.
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
@@ -56,6 +110,10 @@ Docs: https://docs.openclaw.ai
- Plugins/update: treat official externalized bundled npm migrations and ClawHub-to-npm fallbacks as trusted source-linked installs, so prerelease-only official plugin packages can migrate from bundled builds without being rejected as unsafe prerelease resolutions. Thanks @vincentkoc.
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
- Plugins/update: repair stale managed npm-root `openclaw` peer packages before plugin installs, so beta-channel official plugin updates are not downgraded by old core package-lock state. Thanks @vincentkoc.
- Plugins/install: run managed npm-root install, rollback, repair, and uninstall mutations with legacy peer resolution so removing one plugin cannot rehydrate a stale registry `openclaw` package into the shared root. Thanks @vincentkoc.
- Plugins/install: reassert managed npm plugin `openclaw` peer links after shared-root npm installs, updates, and uninstalls, so mutating one plugin does not leave previously installed SDK-using plugins unable to resolve `openclaw/plugin-sdk/*`.
- Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
@@ -71,15 +129,20 @@ Docs: https://docs.openclaw.ai
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
- Plugin SDK/fs-safe: expose reusable atomic replacement, sibling-temp writes, and cross-device move fallback helpers through `plugin-sdk/security-runtime`, and move OpenClaw's duplicated safe filesystem write paths onto the shared `@openclaw/fs-safe` package.
- Plugin SDK/fs-safe: route browser, media, channel, and QA external output producers through staged fs-safe writes before final publication. (#78768)
- Plugin SDK/fs-safe: rename the public temp workspace helpers to `tempWorkspace`, `withTempWorkspace`, `tempWorkspaceSync`, and `withTempWorkspaceSync`, matching the cleaner `@openclaw/fs-safe` API before the package is published.
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
- Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532)
- Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532)
- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc.
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
- Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI.
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
- Control UI/performance: keep chat, config, and channel refreshes responsive by decoupling slow history/schema/status work, reducing the client history window, and logging over-budget chat/config renders. Refs #77060, #45698, #47979, #44107. Thanks @BunsDev.
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and opt-in sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
- QA/Codex harness: add targeted live Docker/Testbox diagnostics, auth preflight checks, cache mount fixes, and app-server protocol checkout discovery so maintainer harness failures are easier to reproduce. Thanks @vincentkoc.
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
- QA/Mantis: add visual desktop tasks with Crabbox MP4 recording, screenshot capture, and optional image-understanding assertions, and preserve video artifacts in Mantis before/after reports.
@@ -90,29 +153,169 @@ Docs: https://docs.openclaw.ai
- QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc.
- Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin.
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
- Control UI/WebChat: show a persistent compact context usage indicator from fresh session token data before the high-pressure warning state, while keeping the existing compaction prompt threshold. Fixes #46398; refs #45048, #50071, and #73744. Thanks @walterwkchoy, @AxelrodAI, @Brissux, @vincentkoc, and @BunsDev.
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
- Contributor PRs: require external pull requests to include after-fix real behavior proof from a real OpenClaw setup, with terminal screenshots, console output, redacted runtime logs, linked artifacts, and copied live output treated as valid evidence while unit tests, mocks, lint, typechecks, snapshots, and CI remain supplemental only.
- Plugins/catalog: add an `@tencent-weixin/openclaw-weixin` external entry pinned to `2.4.1` so onboarding and `openclaw channels add` can install the Tencent Weixin (personal WeChat) channel by default. (#77269) Thanks @pumpkinxing1.
- Developer tooling: add checked-in VS Code Gateway debugging configs and an opt-in `OUTPUT_SOURCE_MAPS=1` source-map build path for breakpoints in TypeScript source. (#45710) Thanks @SwissArmyBud.
- Managed proxy: add `proxy.loopbackMode` for Gateway loopback control-plane traffic, allowing operators to keep the default Gateway loopback bypass, force loopback Gateway traffic through the proxy, or block it. (#77018) Thanks @jesse-merhi.
- Telegram/native commands: show the current thinking level above the `/think` level picker so users can see the active setting before changing it. (#78278) Thanks @obviyus.
- Plugins/hooks: add a `before_agent_run` pass/block gate that can stop a user prompt before model submission while preserving a redacted transcript entry for the user, and clarify that raw conversation hooks require `hooks.allowConversationAccess=true`. (#75035) Thanks @jesse-merhi.
- Config/Nix: keep startup-derived plugin enablement, gateway auth tokens, control UI origins, and owner-display secrets runtime-only instead of rewriting `openclaw.json`; in Nix mode, config writers, mutating `openclaw update`, plugin lifecycle mutators, and doctor repair/token-generation now refuse with agent-first nix-openclaw guidance. (#78047) Thanks @joshp123.
- Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.
- Plugin SDK: add a generic `api.runtime.llm.complete` host completion helper with runtime-derived caller attribution, config-gated model/agent overrides, session-bound context-engine access, request-scoped config, audit metadata, and normalized usage attribution. (#64294) Thanks @DaevMithran.
### Breaking
- Channels/iMessage: remove the bundled BlueBubbles channel surface and deprecate BlueBubbles-backed iMessage setup in OpenClaw. Existing `channels.bluebubbles` configs must migrate to `channels.imessage` using `imsg` on a signed-in Mac or an SSH wrapper, and non-macOS default `imsg` configs now report remote-Mac wrapper guidance.
### Fixes
- Harden macOS shell wrapper allowlist parsing [AI]. (#78518) Thanks @pgondhi987.
- Gateway/macOS: `openclaw gateway stop` now uses `launchctl bootout` by default instead of unconditionally calling `launchctl disable`, so KeepAlive auto-recovery still works after unexpected crashes; use the new `--disable` flag to opt into the persistent-disable behavior when a manual stop should survive reboots. Fixes #77934. Thanks @bmoran1022.
- Gateway/macOS: `repairLaunchAgentBootstrap` no longer kickstarts an already-running LaunchAgent, preventing unnecessary service restarts and session disconnects when repair runs against a healthy gateway. Fixes #77428. Thanks @ramitrkar-hash.
- Gateway/macOS: `openclaw gateway stop --disable` now persists the LaunchAgent disable bit even after a previous bootout left the service not loaded, keeping the explicit stay-down path reliable. (#78412) Thanks @wdeveloper16.
- Control UI/chat: hide retired and non-public Google Gemini model IDs from chat model catalogs and route the bare `gemini-3-pro` alias to Gemini 3.1 Pro Preview instead of the shut-down Gemini 3 Pro Preview. Thanks @BunsDev.
- CLI/install: refuse state-mutating OpenClaw CLI runs as root by default, keep an explicit `OPENCLAW_ALLOW_ROOT=1` escape hatch for intentional root/container use, and update DigitalOcean setup guidance to run OpenClaw as a non-root user. Fixes #67478. Thanks @Jerry-Xin and @natechicago.
- Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested.
- Codex app-server: close stdio stdin before force-killing the managed app-server, matching Codex single-client shutdown behavior and avoiding unsettled CLI exits after successful runs.
- CLI/Codex: dispose registered agent harnesses during short-lived CLI shutdown so successful Codex-backed `agent --local` runs do not leave app-server child processes alive.
- Agents/Codex: auto-enable the Codex harness plugin for one-shot OpenAI model overrides so `openclaw agent --local --model openai/...` does not fail with an unregistered `codex` harness.
- Gateway/live tests: avoid full model-registry enumeration for explicit provider-qualified live model filters, preventing `.profile` OpenAI gateway profile runs from hanging before provider dispatch.
- Gateway/status: surface CLI and gateway runtime versions, warn about stale PATH/global wrappers when they differ, and add stale-wrapper checks to the newer-config warning. Refs #79091. Thanks @RamaAditya49 and @sallyom.
- Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180.
- Gateway/auth: make explicit `trusted-proxy` mode fail closed instead of accepting local password fallback credentials after trusted-proxy identity checks fail. Fixes #78684.
- Active memory: treat Google Chat `spaces/...` conversation ids as scoped targets instead of runnable channel names so recall runs no longer fail bundled-plugin dirName validation. Fixes #78918.
- Active memory: make `/active-memory status` honor the configured agent allowlist instead of reporting on for agents where recall is disabled. Fixes #78986.
- Mistral: normalize structured OpenAI-compatible completions content blocks so thinking objects are not persisted as `[object Object]` visible reply text. Fixes #78846.
- Tools/session status: render the active heartbeat/run model for `session_status({"sessionKey":"current"})` instead of falling back to the persisted session default. Fixes #77493.
- Doctor/secrets: allow safe inherited exec SecretRef `passEnv` names such as `HOME` while still blocking dangerous runtime env hooks. Fixes #78216.
- Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182.
- Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058.
- Agents/subagents: retry parent wake announces when the announce-summary model run fails with fallback cooldown exhaustion instead of dropping the wake on the first transient provider overload. Refs #78581.
- Providers/network: honor IPv4 CIDR and octet-wildcard `NO_PROXY` entries such as `100.64.0.0/10` and `100.64.*` before enabling trusted env-proxy mode for model-provider requests. Fixes #79030.
- Skills: cap skills watcher directory traversal at the same depth used by skill discovery so large non-skill trees under configured skill roots do not exhaust file descriptors on startup. Fixes #75501. Thanks @wzq-xzwj.
- Docs/Docker: document a local Compose override for Docker Desktop DNS failures in the shared-network `openclaw-cli` sidecar, keeping the default compose setup hardened while unblocking `openclaw plugins install` when users opt in. Fixes #79018. Thanks @Jason-Vaughan.
- Installer: when npm installs `openclaw` outside the parent shell PATH, print follow-up commands with the resolved binary path instead of telling users to run `openclaw` from a shell that will report `command not found`. Fixes #72382. Thanks @jbob762.
- Plugins/runtime: share MIME and JSON Schema helpers across bundled plugins while preserving canonical media MIME inference, browser URL wildcard semantics, migration home-path resolution, QA request-limit responses, and extensionless text file previews.
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean.
- feishu: honor config write policy for dynamic agents [AI]. (#78520) Thanks @pgondhi987.
- fix(skill-workshop): honor pending approval for tool suggestions [AI]. (#78516) Thanks @pgondhi987.
- BytePlus: mark Kimi K2.5 and Kimi K2 Thinking catalog entries as reasoning-capable, raise their output cap to 32k tokens, and fill Kimi cache-read pricing. Fixes #54149.
- Control UI/chat: wait for an in-flight model dropdown patch before sending the next chat message, so immediate sends use the selected session model instead of racing the previous override. Fixes #54240.
- Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev.
- Agents/compaction: cap summarization output reserve tokens to the selected model's `maxTokens` so 1M-context Anthropic compactions do not request more output than the API permits. Fixes #54383.
- Control UI/login: replace raw connection failures with structured, actionable login guidance for auth, pairing, insecure HTTP, origin, protocol, and transport failures. Thanks @BunsDev.
- Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev.
- Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev.
- Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev.
- Agents/tools: avoid warning messaging-only agents about inherited global `tools.exec` or `tools.fs` sections when the agent profile did not configure those tool sections itself. Thanks @BunsDev.
- Codex dynamic tools: normalize runtime `toolsAllow` entries the same way as Pi tool policy, so aliases like `bash` and `apply-patch` still expose the intended OpenClaw tools. Thanks @BunsDev.
- Memory/dreaming: read OpenAI-style `output_text` assistant parts from narrative subagent transcripts, so light-phase Dream Diary entries are not dropped as empty. Thanks @BunsDev.
- OpenAI-compatible providers: honor `compat.supportsTools=false` by stripping tool payload fields before dispatch to chat-only endpoints. Fixes #74664.
- OpenAI-compatible providers: apply model-declared unsupported tool-schema keyword stripping to native OpenAI transport payloads and mark Fireworks Kimi K2.5 as rejecting `not` schemas. Fixes #75467.
- OpenAI-compatible gateway: sanitize images supplied through request content even when the prompt text contains no image file references, preventing oversized attachment payloads from bypassing the resize/drop pipeline. Fixes #59913.
- Auth profiles: normalize inline API keys and tokens loaded from `auth-profiles.json` so masked or rich-text credential artifacts fail as auth errors instead of crashing HTTP header construction. Fixes #77624.
- llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166.
- Media generation: resolve slash-containing model-only overrides like `fal-ai/flux/dev` through registered provider model metadata so FAL image/video models do not get misparsed as provider `fal-ai`. Fixes #77444.
- Commands/BTW: show the `/btw` missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.
- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541.
- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.
- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745.
- Amazon Bedrock: refresh shared AWS profile/config file credentials before Bedrock model, discovery, and embedding requests so long-running Gateway processes pick up renewed profile credentials without restart. Fixes #77551.
- Amazon Bedrock: treat named `aws-sdk` auth profiles as config routing metadata instead of stored credentials, and let `doctor --fix` move legacy markers out of `auth-profiles.json`. Fixes #69708.
- Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715.
- OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126.
- CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081.
- CLI/infer: fall back to macOS `sips` when optional image tooling cannot decode HEIC/HEIF input files before model-run requests. Refs #50081.
- OpenRouter: keep the default `openrouter/auto` model ref canonical while preventing TUI and Control UI catalog pickers from displaying or submitting `openrouter/openrouter/auto`. Fixes #62655.
- Status/Claude CLI: show `oauth (claude-cli)` for working Claude CLI OAuth runtime sessions instead of `unknown` when no local auth profile exists. Fixes #78632. Thanks @gorkem2020.
- Memory search: preserve keyword-only hybrid FTS matches when vector scoring is unavailable or below the configured minimum score, so exact lexical hits are not dropped by weighted min-score filtering.
- Exec approvals/node: let trusted backend node invokes complete no-device Control UI approvals after the original request connection changes, while keeping node, command, cwd, env, and allow-once replay bindings enforced. Fixes #78569. Thanks @naturedogdog.
- Agents/subagents: keep background completion delivery on the requester-agent handoff/queue-retry path instead of raw-sending child results directly, and strip child-result wrapper or OpenClaw runtime-context scaffolding from queued outbound retries. Fixes #78531. Thanks @EthanSK.
- Sandbox: recreate cached browser bridges when JavaScript-evaluation permission changes, keep failed prune removals tracked for retry, and make cross-device directory moves copy-then-commit without partially emptying the source on failure.
- CLI/completion: guard the shell-profile source line written by `openclaw completion --install` with a file existence check (`[ -f ... ] && source ...` for bash/zsh, `test -f ...; and source ...` for fish) so uninstalling OpenClaw no longer makes new login shells error on a missing completion cache. (#78659) Thanks @sjf.
- Cron/doctor: repair persisted cron jobs whose `payload.model` was stored as `"default"`, `"null"`, blank, or JSON `null` by removing the bad override during `openclaw doctor --fix` while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.
- Telegram: honor `accessGroup:*` sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.
- Agent delivery: report `deliverySucceeded=false` when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.
- Cron/isolated runs: fail implicit announce delivery before model execution when `delivery.channel=last` has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.
- Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.
- Doctor/OpenAI Codex: repair legacy `openai-codex/*` agent model refs and stale OpenAI PI session pins to `openai/*` with the Codex runtime, preserving existing `openai-codex` auth profiles so ChatGPT/Codex OAuth users do not fall back to OpenAI API-key routing. Fixes #78407.
- Telegram: keep the polling watchdog tied to `getUpdates` liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.
- Discord/groups: instruct group-chat agents to stay silent when a message is addressed to someone else, replying only when invited or correcting key facts. (#78615)
- Discord/groups: tell Discord-channel agents to wrap bare URLs as `<https://example.com>` so link previews do not expand into uninvited embeds. (#78614)
- Agents/fallback: fail fast on session write-lock timeouts instead of trying fallback models for local file contention. Fixes #66646. Thanks @sallyom.
- Telegram/Codex: generate DM topic labels with Codex-compatible simple-completion requests so auto-created private topics can be renamed instead of staying `New Chat`.
- Plugins/runtime fetch: drop third-party symbol metadata from plain request header dictionaries before passing them into native `fetch` or `Headers`, so SDK and guarded/proxy fetch paths do not reject otherwise valid plugin requests. Fixes #77846. Thanks @shakkernerd.
- Web fetch: bound guarded dispatcher cleanup after request timeouts so timed-out fetches return tool errors instead of leaving Gateway tool lanes active. (#78439) Thanks @obviyus.
- Mattermost/setup: prompt for and persist the server base URL after the bot token in `openclaw setup --wizard`, instead of failing validation before `--http-url` is collected. Fixes #76670. Thanks @jacobtomlinson.
- Gate Slack startup user allowlist resolution [AI]. (#77898) Thanks @pgondhi987.
- OpenAI/Codex: suppress stale `openai-codex` GPT-5.1/5.2/5.3 model refs that ChatGPT/Codex OAuth accounts now reject, keeping model lists, config validation, and forward-compat resolution on current 5.4/5.5 routes. Fixes #67158. Thanks @drpau.
- CLI/update: keep pnpm package updates on the running custom global install root and pass pnpm's `--global-dir` so `openclaw update` does not create a second default-prefix install when `OPENCLAW_HOME` or the shell points at a custom OpenClaw directory. Fixes #78377. Thanks @amknight.
- Google Meet/Voice Call: wait longer before playing PIN-derived Twilio DTMF for Meet dial-in prompts and retire stale delegated phone sessions instead of reusing completed calls.
- PDF/Codex: include extraction-fallback instructions for `openai-codex/*` PDF tool requests so Codex Responses receives its required system prompt. Fixes #77872. Thanks @anyech.
- Gateway/startup: keep the Gateway running when a configured optional plugin-owned capability such as a web_search provider or channel points at a known installable plugin that is currently unavailable; startup now logs a config warning and leaves `openclaw doctor --fix` to install or enable the plugin. (#78642) Thanks @joshavant.
- Onboard/channels: recover externalized channel plugins from stale `channels.<id>` config by falling back to `ensureChannelSetupPluginInstalled` via the trusted catalog when the plugin is missing on disk, so leftover `appId`/token entries no longer dead-end onboard with "<channel> plugin not available." (#78328) Thanks @sliverp.
- Agents/Gateway: throttle and cap live exec command-output events so noisy tool runs cannot flood Gateway WebSocket clients or starve RPC handling. (#78645) Thanks @joshavant.
- Codex/app-server: forward the OpenClaw workspace bootstrap block through Codex `developerInstructions` instead of `config.instructions`, so persona/style guidance reaches the behavior-shaping app-server lane. Fixes #77363. Thanks @lonexreb.
- MS Teams: route proactive channel sends with stored thread roots through the configured threaded reply path instead of forcing every CLI/message-tool send into a new top-level post. Fixes #78298. Thanks @amknight.
- CLI/infer: pass minimal instructions to local `openai-codex/*` model probes and surface provider error details when `infer model run` returns no text. Fixes #76464. Thanks @lilesjtu.
- Dependencies: override transitive `ip-address` to `10.2.0` so the runtime lockfile no longer includes the vulnerable `10.1.0` build flagged by Dependabot alert 109. Thanks @vincentkoc.
- Plugins/install: apply OpenClaw's npm security overrides inside managed external plugin npm roots so hoisted plugin dependencies inherit the host package hardening. Thanks @vincentkoc.
- Plugins/install: skip npm peer resolution in managed plugin roots so installing peer-based plugins such as Opik cannot pull a stale registry `openclaw` copy beside Codex/Discord/WhatsApp and trigger `ERESOLVE`. Thanks @vincentkoc.
- Plugins/uninstall: run managed npm cleanup even when a plugin package directory is already missing, preventing stale package manifests from reinstalling removed plugins on the next npm install.
- Feishu: hydrate missing native topic starter thread IDs before session routing so first turns and follow-ups stay in the same topic session. Fixes #78262. Thanks @joeyzenghuan.
- Memory Wiki: skip empty and whitespace-only source pages when refreshing generated Related blocks, preventing blank pages from being rewritten into Related-only stubs. Fixes #78121. Thanks @amknight.
- LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316.
- Telegram/Codex: keep message-tool-only progress drafts visible and render native Codex tool progress once per tool instead of duplicating item/tool draft lines. Fixes #75641. (#77949) Thanks @keshavbotagent.
- Telegram/sessions: gap-fill delivered embedded final replies into the session JSONL even when the runner trace is missing, so Telegram answers after tool calls do not vanish from the durable transcript. Fixes #77814. (#78426) Thanks @obviyus, @ChushulSuri, and @DougButdorf.
- Providers/xAI: stop sending OpenAI-style reasoning effort controls to native Grok Responses models, so `xai/grok-4.3` no longer fails live Docker/Gateway runs with `Invalid reasoning effort`.
- Providers/xAI: clamp the bundled xAI thinking profile to `off` so live Gateway runs cannot send unsupported reasoning levels to native Grok Responses models.
- Matrix/approvals: retry approval delivery up to 3 times with a short backoff so transient Matrix send failures do not strand pending approval prompts. (#78179) Thanks @Patrick-Erichsen.
- Cron/heartbeat: let restricted cron-triggered runs read their own status and current-job list metadata again, preventing heartbeat STATUS freshness checks from going stale while preserving self-remove-only mutation limits. Fixes #78208. Thanks @amknight.
- Discord/gateway: measure heartbeat ACK timeouts from the actual heartbeat send, preventing late initial heartbeats from triggering false reconnect loops while the channel is still awaiting readiness. Fixes #77668. (#78087) Thanks @bryce-d-greybeard and @NikolaFC.
- Channels/cron: ignore stale runtime conversation bindings that point at completed isolated cron run sessions, so follow-up DMs fall back to their normal route instead of reusing a closed cron task prompt. Fixes #78074. Thanks @amknight.
- Discord/guilds: route plain text control commands such as `/steer` through the normal authorization and mention gate instead of silently dropping them before an agent session can see them. Fixes #78080. Thanks @ramitrkar-hash.
- Control UI/Sessions: make the compaction count a compact `N Checkpoint(s)` disclosure and show expanded session-level details with modern checkpoint history cards across responsive table layouts. Thanks @BunsDev.
- Control UI/performance: keep chat and channel tabs responsive while history payloads and channel probes are slow, label partial channel status, and record slow chat/config render timings in the event log. Thanks @BunsDev.
- ACP: preserve streamed chunk boundaries in background-task progress summaries so CJK text, paths, URLs, and identifiers are no longer split with synthetic spaces. Fixes #78312. Thanks @amknight.
- Control UI/sessions: fire the documented `/new` command and lifecycle hooks only for explicit Control UI session creation, restoring session-memory and custom hook capture without changing SDK parent-session creates. Fixes #76957. Thanks @BunsDev.
- Exec approvals: fall back to a guarded copy when Windows rejects rename-overwrite for `exec-approvals.json`, while preserving symlink, hard-link, and owner-only permission safeguards. Fixes #77785. (#77907) Thanks @Alex-Alaniz and @MilleniumGenAI.
- Slack: preserve Socket Mode SDK error context and structured Slack API fields in reconnect logs, so startup failures no longer collapse to a bare `unknown error`.
- Agents/subagents: preserve the delegated task prompt when a spawned target agent uses `systemPromptOverride`, so `sessions_spawn(mode: "run")` child runs still see their assigned task. Fixes #77950. Thanks @amknight.
- iOS pairing: allow setup-code and manual `ws://` connects for private LAN and `.local` gateways while keeping Tailscale/public routes on `wss://`, and prefer explicit gateway passwords over stale bootstrap tokens in mixed-auth reconnects. Fixes #47887; carries forward #65185. Thanks @draix and @BunsDev.
- Node/Windows: fall back to the Startup-folder launcher when Spanish-localized `schtasks` reports `Acceso denegado`, matching the existing access-denied fallback path. Fixes #77993. Thanks @jackonedev.
- Plugins/diagnostics: make source-only TypeScript package warnings actionable by explaining that missing compiled runtime output is a publisher packaging issue and pointing users to update/reinstall or disable/uninstall the plugin. Fixes #77835. Thanks @googlerest.
- Control UI/chat: keep persisted assistant progress text visible when the same transcript turn also contains tool-use metadata, so chat.history reloads no longer make those replies vanish after the next user message. Fixes #77374. Thanks @BunsDev.
- Cron: repair persisted future `nextRunAtMs` values that no longer line up with the cron schedule, so daily timezone-aware jobs do not stay jumped to stale future dates. Fixes #77867. Thanks @hongfangsong.
- TUI: skip the generic CLI respawn wrapper for interactive launches, exit cleanly on terminal loss, and refuse to restore heartbeat sessions as the remembered chat session, preventing stale heartbeat history and orphaned `openclaw-tui` processes on first boot. Thanks @vincentkoc.
- Doctor/sessions: move heartbeat-poisoned default main session store entries to recovery keys and clear stale TUI restore pointers, so `doctor --fix` can repair instances already stuck on `agent:main:main` heartbeat history. Thanks @vincentkoc.
- Agents/context engines: keep hidden OpenClaw runtime-context custom messages out of context-engine assemble, afterTurn, and ingest hooks so transcript reconstruction plugins only see conversation messages. Thanks @vincentkoc.
- Agents/compaction: treat visible custom-message, bash, and branch-summary entries as real conversation anchors so safeguard mode does not write empty fallback summaries for cron and split-turn sessions with substantive tool work. Fixes #78300. Thanks @amknight.
- Network/runtime: avoid importing Undici's package dispatcher during no-proxy timeout bootstrap so external channel plugin fetch requests with explicit Content-Length keep working. Fixes #78007. Thanks @shakkernerd.
- Gateway/shutdown: cancel delayed post-ready maintenance during close and suppress maintenance/cron startup after quick restarts, preventing orphaned background timers. Thanks @vincentkoc.
- Agents/TTS: send media-bearing block replies directly when block streaming is off, so agent `tts` tool audio attached to a final text reply is delivered instead of being consumed before final Telegram/media delivery. Thanks @Conan-Scott.
- Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded.
- Control UI/sessions: show each session's agent runtime in the Sessions table and allow filtering by runtime labels, matching the Agents panel runtime wording. Thanks @vincentkoc.
- Discord/streaming: show live reasoning text in progress drafts instead of a bare `Reasoning` status line.
- Gateway/status: avoid marking fast repeated health/status samples as event-loop degraded from CPU/utilization alone until the Gateway has accumulated a sustained sampling window. Thanks @shakkernerd.
- Gateway/performance: reuse the current compatible plugin metadata snapshot across hot read-only status, channel, auth, skills, and embedded agent settings paths, avoiding repeated synchronous plugin metadata scans during Gateway activity. Fixes #77983. Thanks @shakkernerd.
- Plugins/update: keep installed official npm and ClawHub plugins such as Codex, Discord, WhatsApp, and diagnostics plugins synced during host updates even when disabled or previously exact-pinned, while preserving third-party plugin pins. Thanks @vincentkoc.
- Doctor/status: warn when `OPENCLAW_GATEWAY_TOKEN` would shadow a different active `gateway.auth.token` source for local CLI commands, while avoiding false positives when config points at the same env token. Fixes #74271. Thanks @yelog.
- Gateway/HTTP: avoid loading managed outgoing-image media handlers for unrelated requests, so disabled OpenAI-compatible routes return 404 without waiting on lazy media sidecars. Thanks @vincentkoc.
- Gateway/OpenAI-compatible: send the assistant role SSE chunk as soon as streaming chat-completion headers are accepted, so cold agent setup cannot leave `/v1/chat/completions` clients with a bodyless 200 response until their idle timeout fires.
- Agents/media: avoid direct generated-media completion fallback while the announce-agent run is still pending, so async video and music completions do not duplicate raw media messages. (#77754)
- WebChat/Codex media: stage Codex app-server generated local images into managed media before Gateway display, so Codex-home image paths no longer hit `LocalMediaAccessError` while keeping Codex home out of the display allowlist. Thanks @frankekn.
- Plugins/update: repair plugin-local `openclaw` peer links for all recorded npm plugins after any npm update mutates the shared managed npm tree, so targeted or batch updates cannot leave Codex, Discord, or Brave with pruned SDK imports. (#77787) Thanks @ProspectOre.
- Codex harness: honor `models.providers.openai-codex.models[].contextTokens` for native `openai/*` Codex runtime runs and `/status` context reporting, so subscription-backed Codex agents use the configured OAuth context cap without inflating past the runtime model window. Fixes #77858. Thanks @lilesjtu.
- Sessions cleanup: add `openclaw sessions cleanup --fix-dm-scope` so operators who return `session.dmScope` to `main` can dry-run and retire stale direct-DM session rows while preserving transcripts as deleted archives. Fixes #47561 and #45554. Thanks @BunsDev.
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd.
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd.
@@ -121,6 +324,7 @@ Docs: https://docs.openclaw.ai
- Hooks/session-memory: add collision suffixes to fallback memory filenames so repeated `/new` or `/reset` captures in the same minute do not overwrite the earlier session archive. Thanks @vincentkoc.
- Agents/config: remove the ambiguous legacy `main` agent dir helper from runtime paths; model, auth, gateway, bundled plugin, and test helpers now resolve default/session agent dirs through `agents.list`/agent-scope helpers while plugin SDK keeps a deprecated compatibility export.
- CLI/status: show the selected agent runtime/harness in `openclaw status` session rows so terminal status matches the `/status` runtime line. Thanks @vincentkoc.
- CLI/sessions: prune old unreferenced transcript, compaction checkpoint, and trajectory artifacts during normal `sessions cleanup`, so gateway restart or crash orphans do not accumulate indefinitely outside `sessions.json`. Fixes #77608. Thanks @slideshow-dingo.
- Doctor/Codex: repair legacy `openai-codex/*` routes in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel overrides, and stale session pins to canonical `openai/*`, selecting `agentRuntime.id: "codex"` only when the Codex plugin is installed, enabled, contributes the `codex` harness, and has usable OAuth; otherwise select `agentRuntime.id: "pi"`. Thanks @vincentkoc.
- Video generation: wait up to 20 minutes for slow fal/MiniMax queue-backed jobs, stop forwarding unsupported Google Veo generated-audio options, and normalize MiniMax `720P` requests to its supported `768P` resolution with the usual override warning/details instead of failing fallback.
@@ -137,6 +341,7 @@ Docs: https://docs.openclaw.ai
- CLI/update: make dev-channel preflight lint opt-in and constrained when enabled, so `openclaw update --channel dev` no longer walks back otherwise-good main commits when Ubuntu hosts OOM-kill or fail parallel oxlint shards. Thanks @vincentkoc.
- Google Meet: fork the caller's current agent transcript into agent-mode meeting consultant sessions, so Meet replies inherit the context from the tool call that joined the meeting.
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.
- Control UI/Sessions: hide disk-discovered unregistered-agent sessions by default and fall back from restored unconfigured agent session keys before chat refresh, preventing deleted-agent stores from reopening the wrong workspace. Fixes #41685. Thanks @BunsDev.
- Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model.
- Google Meet: stop advertising legacy `mode: "realtime"` to agents and config UIs, while keeping it as a hidden compatibility alias for `mode: "agent"`, so new joins use the STT -> OpenClaw agent -> TTS path instead of selecting the direct realtime voice fallback.
- Google Meet: add `chrome.audioBufferBytes` for generated command-pair SoX audio commands and lower the default buffer from SoX's 8192 bytes to 4096 bytes to reduce Chrome talk-back latency.
@@ -387,7 +592,22 @@ Docs: https://docs.openclaw.ai
- Agents/sessions: after embedded Pi runs, append assistant-visible reply text to session JSONL only when Pi did not already persist an equivalent tail assistant entry, without re-mirroring the user prompt Pi owns. Fixes #77823. (#77839) Thanks @neeravmakwana.
- Plugins/CLI: load the install-records ledger when listing channel-catalog entries, so npm-installed third-party channel plugins resolve through `openclaw channels login`/`channels add` instead of failing with `Unsupported channel`. (#77269) Thanks @pumpkinxing1.
- Memory wiki/Security: enforce session visibility on shared-memory `wiki_search` and `wiki_get` so sandboxed subagents cannot read transcript content from sibling or parent sessions. Fixes GHSA-72fw-cqh5-f324. Thanks @zsxsoft.
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Agents/compaction: disable Pi auto-compaction whenever OpenClaw effectively owns safeguard compaction, including provider-backed safeguard mode, so Pi and OpenClaw no longer fight over long-session compaction. Fixes #73003. (#73839) Thanks @bradhallett.
- Telegram/streaming: finalize text replies by stopping the edited stream message instead of sending a second answer bubble, so Telegram turns cannot duplicate the streamed final response. (#77947) Thanks @obviyus.
- web_search/Brave: fix provider selection when Brave is installed as an external plugin and `tools.web.search.provider: "brave"` is explicitly configured — a redundant provider re-resolution at startup could race and return an empty list, causing a spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and treating the explicitly configured provider as absent. Fixes #77676. Thanks @openperf.
- Doctor/plugins: discover doctor contracts from load-path channel plugins during `openclaw doctor --fix`, so plugin-owned legacy config repair runs before validation. (#77477) Thanks @jalehman.
- Dependencies: bump transitive `basic-ftp` to 5.3.1 so the runtime lockfile no longer includes the vulnerable 5.3.0 build flagged by the production dependency audit. (#78637) Thanks @sallyom.
- Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid `max_tokens` values. (#54392) Thanks @adzendo.
- Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` (default 60 minutes; same knob run-mode already uses for `archiveAtMs`) instead of a hardcoded 5-minute TTL, so `subagents list` and other registry-backed surfaces still show recently-completed runs and operators have one consistent retention knob across spawn modes. (#78263) Thanks @arniesaha.
- Plugins/channel setup: fix `setChannelRuntime` being silently dropped from non-bundled external plugin setup entries — external channel plugins that export `{ plugin, setChannelRuntime }` from their setup entry now have the runtime setter invoked, so the runtime initializer the provider polls for is set before the channel starts, preventing a poll timeout and gateway crash loop when the plugin opts into deferred startup loading. Fixes #77779. (#77799) Thanks @openperf.
- WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.
- WhatsApp: send captioned `MEDIA:` directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.
- Hooks/cron: log returned `/hooks/agent` isolated-run errors and failed cron jobs with cron diagnostic summaries, so rejected `payload.model` values are visible instead of looking like accepted-but-missing runs. Fixes #78597. (#78655) Thanks @kevinslin.
- Managed proxy/security: classify raw socket callsites and proxy runtime mutations in boundary checks so new direct egress or unmanaged proxy-state changes cannot land without explicit review. (#77126) Thanks @jesse-merhi.
- Channels/iMessage: surface the silent group-allowlist drop at default log level by emitting a one-time `warn` per account at monitor startup when `channels.imessage.groupPolicy: "allowlist"` is set without a `channels.imessage.groups` block, plus a one-time `warn` per `chat_id` when the runtime gate drops a specific group, naming the exact `channels.imessage.groups[...]` key to add to allow it. Fixes #78749. (#79190) Thanks @omarshahine.
- WhatsApp: stop Gateway-originated outbound echoes from advancing inbound activity in `openclaw channels status`, so outbound self-sends no longer look like handled inbound messages. Fixes #79056. (#79057) Thanks @ai-hpc and @bittoby.
- Gateway/nodes: preserve the live node registry session and invoke ownership when an older same-node WebSocket closes after reconnecting. (#78351) Thanks @samzong.
## 2026.5.3-1
@@ -413,6 +633,7 @@ Docs: https://docs.openclaw.ai
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
- Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys.
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.
- Agents/compaction: ignore pre-usage transcript metadata bytes when stale token snapshots estimate preflight compaction pressure, while still counting post-usage transcript tail pressure. Fixes #78604. Thanks @amknight.
- Discord/status: let explicit reaction tool calls opt into tracking subsequent tool progress on the reacted message with `trackToolCalls: true`, and use the shared tool display emoji table for status reactions.
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.

View File

@@ -14,6 +14,9 @@ Welcome to the lobster tank! 🦞
- **Peter Steinberger** - Benevolent Dictator
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
- **Frank Yang** - PR triage, Agents, Gateway, Channels
- GitHub: [@frankekn](https://github.com/frankekn) · X: [@frankekn](https://x.com/frankekn)
- **Shadow** - Discord subsystem, Discord admin, Clawhub, all community moderation
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shadowed](https://x.com/4shadowed)
@@ -26,7 +29,7 @@ Welcome to the lobster tank! 🦞
- **Ayaan Zaidi** - Telegram subsystem, Android app
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@obviyus](https://x.com/obviyus)
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
- **Tyler Yust** - Agents/subagents, cron, iMessage, macOS app
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
- **Mariano Belinky** - iOS app, Security
@@ -83,6 +86,9 @@ Welcome to the lobster tank! 🦞
- **Mason Huang** - Stability, Security, Speed
- GitHub: [@hxy91819](https://github.com/hxy91819) · X: [@chenjingtalk](https://x.com/chenjingtalk)
- **Maurice Niu** - ClawHub, Security, Stability, Data integrity
- GitHub: [@momothemage](https://github.com/momothemage) · X: [@MomoPsicasso](https://x.com/MomoPsicasso)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!

View File

@@ -97,9 +97,9 @@ RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do
# Stub it so local cross-arch builds still succeed.
RUN pnpm canvas:a2ui:bundle || \
(echo "A2UI bundle: creating stub (non-fatal)" && \
mkdir -p src/canvas-host/a2ui && \
echo "/* A2UI bundle unavailable in this build */" > src/canvas-host/a2ui/a2ui.bundle.js && \
echo "stub" > src/canvas-host/a2ui/.bundle.hash && \
mkdir -p extensions/canvas/src/host/a2ui && \
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
RUN pnpm build:docker
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)

View File

@@ -23,7 +23,7 @@ It answers you on the channels you already use. It can speak and listen on macOS
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Supported channels include: 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.
Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat.
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
@@ -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.14+**.
Runtime: **Node 24 (recommended) or Node 22.16+**.
```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.14+**.
Runtime: **Node 24 (recommended) or Node 22.16+**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
@@ -121,7 +121,7 @@ openclaw gateway --port 18789 --verbose
# Send a message
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)
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/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
```
@@ -146,7 +146,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat, macOS, iOS/Android.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).

View File

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

View File

@@ -2,6 +2,53 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.5.7</title>
<pubDate>Thu, 07 May 2026 22:36:27 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026050790</sparkle:version>
<sparkle:shortVersionString>2026.5.7</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.7</h2>
<h3>Fixes</h3>
<ul>
<li>Release/plugin publishing: retry transient ClawHub CLI dependency install failures, keep preview-passing plugins publishable when one preview cell flakes, and verify every expected ClawHub package version after publish so maintenance releases are faster to recover and less likely to hide partial plugin publishes.</li>
<li>OpenAI: support <code>openai/chat-latest</code> as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.</li>
<li>Cron CLI: include computed <code>status</code> in <code>cron list --json</code> and <code>cron show --json</code> output so external tooling can read disabled/running/ok/error/skipped/idle state without reimplementing cron status derivation. (#78701) Thanks @aweiker.</li>
<li>Channels CLI: make <code>openclaw channels list</code> channel-only, add <code>--all</code> for bundled and catalog channels, render installed/configured/enabled state, and move model auth/usage details to <code>openclaw models auth list</code>, <code>openclaw status</code>, and <code>openclaw models list</code>. (#78456) Thanks @sliverp.</li>
<li>Native commands: honor owner enforcement for native command handlers. (#78864) Thanks @pgondhi987.</li>
<li>Active Memory: require admin scope for global memory toggles. (#78863) Thanks @pgondhi987.</li>
<li>Gateway/sessions: clear cached skills snapshots during <code>/new</code> and <code>sessions.reset</code> so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.</li>
<li>Auto-reply: gate inline skill tool dispatch through before-tool-call authorization hooks. (#78517) Thanks @pgondhi987.</li>
<li>Tavily: resolve dedicated <code>tavily_search</code> and <code>tavily_extract</code> tool credentials from the active runtime config snapshot, so <code>exec</code> SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.</li>
<li>Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.</li>
<li>Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.</li>
<li>Discord/message: parse provider-prefixed targets like <code>discord:channel:<id></code> as channel sends instead of legacy Discord DM targets, so cross-channel agent <code>message(action="send")</code> calls no longer misroute channel IDs into misleading <code>Unknown Channel</code> failures. Fixes #78572.</li>
<li>Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid <code>max_tokens</code> values. (#54392) Thanks @adzendo.</li>
<li>Commands/BTW: show the <code>/btw</code> missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.</li>
<li>Cron/doctor: repair persisted cron jobs whose <code>payload.model</code> was stored as <code>"default"</code>, <code>"null"</code>, blank, or JSON <code>null</code> by removing the bad override during <code>openclaw doctor --fix</code> while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.</li>
<li>Telegram: honor <code>accessGroup:*</code> sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.</li>
<li>Agent delivery: report <code>deliverySucceeded=false</code> when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.</li>
<li>Cron/isolated runs: fail implicit announce delivery before model execution when <code>delivery.channel=last</code> has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.</li>
<li>Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.</li>
<li>Doctor/Codex OAuth: preserve working <code>openai-codex/*</code> PI routes during <code>doctor --fix</code> and recover 2026.5.5-rewritten <code>openai/*</code> GPT-5 routes when only Codex OAuth auth is available, so update repair does not break subscription-auth setups. Fixes #78407. Thanks @shakkernerd.</li>
<li>Telegram: keep the polling watchdog tied to <code>getUpdates</code> liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.</li>
<li>Agents/subagents: have completed session-mode subagent registry rows honor <code>agents.defaults.subagents.archiveAfterMinutes</code> instead of a hardcoded 5-minute TTL, so registry-backed surfaces keep one retention knob across spawn modes. (#78263) Thanks @arniesaha.</li>
<li>Plugins/channel setup: forward <code>setChannelRuntime</code> from non-bundled external plugin setup entries so deferred external channel runtime initializers are installed before startup polling. Fixes #77779. (#77799) Thanks @openperf.</li>
<li>Telegram: treat successful same-chat <code>message</code> tool outbound sends during an inbound Telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback. (#78685) Thanks @neeravmakwana.</li>
<li>Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared and bound channel hot-reload deferrals so stale task records cannot block Discord/Slack/Telegram reloads forever.</li>
<li>Discord/voice: audit Discord voice-channel permissions in <code>channels capabilities</code> and <code>channels status --probe</code>, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before <code>/vc join</code>.</li>
<li>Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add <code>voice.captureSilenceGraceMs</code> for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.</li>
<li>WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.</li>
<li>WhatsApp: send captioned <code>MEDIA:</code> directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.</li>
<li>Codex/approvals: in Codex approval modes, stop installing the pre-guardian native <code>PermissionRequest</code> hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember <code>allow-always</code> decisions for identical Codex native <code>PermissionRequest</code> payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.</li>
<li>Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with fallback signatures, accept legacy <code>__env__:VAR</code> custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.</li>
<li>Telegram/models: parse provider ids containing dots in <code>/models</code> callback buttons so <code>hf.co</code> model lists render as inline keyboard buttons. Fixes #38745.</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.5.7/OpenClaw-2026.5.7.zip" length="51130645" type="application/octet-stream" sparkle:edSignature="Zu+EzBGMRE1k7N4//L8HUxtUCPdO0ImrfDbgr2GrPMBrj7VGI1tOOl74gxNJoi/wfWvXz3fYVcBz2W/84ojuCw=="/>
</item>
<item>
<title>2026.5.2</title>
<pubDate>Sun, 03 May 2026 01:11:51 +0000</pubDate>
@@ -765,297 +812,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.29/OpenClaw-2026.4.29.zip" length="50896802" type="application/octet-stream" sparkle:edSignature="YfQ25zMGgDv8XvHbdlL/s0SMJXyu763l5ppnfjiKOjSyxZY9sfoLaoXthcctFQDXA8isR1EEb/EEausu+XkFCA=="/>
</item>
<item>
<title>2026.4.27</title>
<pubDate>Wed, 29 Apr 2026 23:53:26 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026042790</sparkle:version>
<sparkle:shortVersionString>2026.4.27</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.27</h2>
<h3>Changes</h3>
<ul>
<li>Sandbox/Docker: add opt-in <code>sandbox.docker.gpus</code> passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports <code>--gpus</code>. Fixes #57976; carries forward #58124. Thanks @cyan-ember.</li>
<li>iOS/Gateway: add an authenticated <code>node.presence.alive</code> protocol event and <code>node.list</code> last-seen fields so background iOS wakes can mark paired nodes recently alive without treating them as connected. Carries forward #63123. Thanks @ngutman.</li>
<li>Android: publish authenticated <code>node.presence.alive</code> events after node connect and background transitions so paired Android nodes retain durable last-seen metadata after disconnects. Carries forward #63123. Thanks @ngutman.</li>
<li>Gateway/chat: accept non-image attachments through <code>chat.send</code> by staging them as agent-readable media paths, while keeping unsupported RPC attachment paths explicit instead of silently dropping files. Fixes #48123. (#67572) Thanks @samzong.</li>
<li>Security/networking: add opt-in operator-managed outbound proxy routing (proxy.enabled + proxy.proxyUrl/OPENCLAW_PROXY_URL) with strict http:// forward-proxy validation, loopback-only Gateway bypass, and cleanup of proxy env/dispatcher state on exit. (#70044) Thanks @jesse-merhi and @joshavant.</li>
<li>Dependencies: refresh provider and tooling dependencies, including AWS SDK, PI runtime packages, AJV, Feishu SDK, Anthropic SDK, tokenjuice, and native TypeScript/oxlint tooling. Thanks @dependabot.</li>
<li>Matrix/QA: add live Matrix approval scenarios for exec metadata, chunked fallback, plugin approvals, deny reactions, thread targeting, and <code>target: "both"</code> delivery, with redacted artifacts preserving safe approval summaries. Thanks @gumadeiras.</li>
<li>Codex: add Computer Use setup for Codex-mode agents, including <code>/codex computer-use status/install</code>, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai.</li>
<li>Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy.</li>
<li>Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through <code>openclaw/plugin-sdk/channel-route</code>, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.</li>
<li>Docs/Codex: document how Codex Computer Use, direct <code>cua-driver mcp</code>, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.</li>
<li>Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with <code>streaming.preview.toolProgress: false</code> to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.</li>
<li>Plugins/models: wire manifest <code>modelCatalog.aliases</code> and <code>modelCatalog.suppressions</code> into model-catalog planning and built-in model suppression, with stale Spark and Qwen Coding Plan suppressions now declared in plugin manifests instead of runtime fallback hooks. Thanks @shakkernerd.</li>
<li>Plugin SDK/models: add a shared manifest-backed provider catalog builder and move Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Moonshot, DeepSeek, Tencent TokenHub, and StepFun provider catalogs onto their plugin manifest <code>modelCatalog</code> rows. Thanks @shakkernerd.</li>
<li>Plugin SDK/models: move BytePlus and Volcano Engine standard and plan-provider catalogs into plugin manifest <code>modelCatalog</code> rows and remove the now-unused Volcengine-family shared catalog SDK subpath. Thanks @shakkernerd.</li>
<li>CLI/models: move Fireworks and Together AI fixed provider catalogs into plugin manifest <code>modelCatalog</code> rows so provider-filtered listing can use manifest-backed static rows. Thanks @shakkernerd.</li>
<li>Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (<code>openclaw-plugin-yuanbao</code>) in the official channel catalog, contract suites, and community plugin docs, with a new <code>docs/channels/yuanbao.md</code> quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.</li>
<li>Channels/Yuanbao: add a channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.</li>
<li>Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C <code>stream_messages</code> streaming with a <code>StreamingController</code> lifecycle manager, unified <code>sendMedia</code> with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via <code>createEngineAdapters()</code>. (#70624) Thanks @cxyhhhhh.</li>
<li>Plugins/startup: migrate bundled plugin manifests to explicit <code>activation.onStartup</code> declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.</li>
<li>Plugins/startup: add an opt-in future-mode gate for disabling deprecated implicit startup sidecar loading while preserving explicit startup and narrower activation triggers. Thanks @shakkernerd.</li>
<li>Plugins/startup: add plugin compatibility warnings for deprecated implicit startup loading so authors can migrate to explicit <code>activation.onStartup</code> metadata. Thanks @shakkernerd.</li>
<li>Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.</li>
<li>Plugins/startup: add explicit <code>activation.onStartup</code> metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.</li>
<li>Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.</li>
<li>CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Chutes, Kilo, OpenAI, and OpenCode Go model catalogs in refreshable plugin manifests, keep broad <code>models list --all</code> on raw registry and supplement rows without runtime normalization, and avoid duplicate supplement resolution. Thanks @shakkernerd.</li>
<li>Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.</li>
<li>Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.</li>
<li>Plugin SDK/testing: move core-only channel contract fixtures under the channel contract test tree and retire the old <code>test/helpers/channels</code> bridge directory so plugin tests stay on focused SDK surfaces. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: expose native agent-runtime contract fixtures through <code>plugin-sdk/agent-runtime-test-contracts</code>, move sandbox config fixtures into the focused generic fixture subpath, and block extension tests from importing repo-only <code>test/helpers</code> bridges. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: expose generic module reload, bundled-path, Node builtin mock, channel pairing/envelope, HTTP server, temp-home, replay-policy, and live STT helpers through focused SDK test subpaths so extension tests no longer depend on repo-only helper bridges. Thanks @vincentkoc.</li>
<li>Plugin SDK: move maintained bundled channels off the deprecated <code>channel-config-schema-legacy</code> subpath, add an explicit bundled-channel schema SDK surface, and track both remaining legacy test/config compatibility barrels with dated removal windows. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: expose media provider capability assertions and provider HTTP mocks through focused SDK test subpaths, and retire the repo-only media-generation test helper bridge. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: promote bundled plugin/provider/channel contract helpers to focused SDK test subpaths and retire the repo-only <code>test/helpers/plugins</code> TypeScript bridge. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: expose generic channel action, setup, status, and directory contract helpers through <code>plugin-sdk/channel-test-helpers</code> so bundled extension tests no longer import repo-only channel helper bridges. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: add <code>plugin-sdk/channel-target-testing</code> for shared channel target-resolution cases, document channel reaction helpers on <code>plugin-sdk/channel-feedback</code>, and keep the old <code>plugin-sdk/test-utils</code> alias as compatibility-only. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: add a focused generic fixture subpath for CLI capture, sandbox, skill, agent-message, system-event, terminal, chunking, auth-token, and typed-case helpers. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: add focused plugin runtime and environment fixture subpaths so plugin tests can avoid the broad <code>plugin-sdk/testing</code> barrel for common setup helpers. Thanks @vincentkoc.</li>
<li>Plugin SDK/testing: add a focused <code>plugin-sdk/plugin-test-api</code> helper subpath and move bundled plugin registration tests off the repo-only plugin API bridge. Thanks @vincentkoc.</li>
<li>Plugin SDK: add generic host hooks for session state, next-turn context, trusted tool policy, UI descriptors, events, scheduler cleanup, and run-scoped plugin context. (#72287) Thanks @100yenadmin.</li>
<li>Plugin SDK/testing: expose provider catalog, wizard, registry, manifest, public-artifact, outbound, and TTS contract helpers through documented SDK testing seams so bundled plugin tests no longer import repo <code>src/**</code> internals. Thanks @vincentkoc.</li>
<li>Providers/DeepInfra: add a bundled DeepInfra provider with <code>DEEPINFRA_API_KEY</code> onboarding, dynamic OpenAI-compatible model discovery, image generation/editing, image/audio media understanding, TTS, text-to-video, memory embeddings, static catalog metadata, and provider-owned base URL policy. Carries forward #53805, #48088, #37576, #43896, #11533, and #2554. Thanks @ats3v.</li>
<li>Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Gateway/sessions: align <code>chat.history</code> and <code>sessions.list</code> thinking defaults with owning-agent and catalog-aware resolution so Control UI session defaults match backend runtime state. (#63418) Thanks @jpreagan.</li>
<li>Devices/pairing: recover array-shaped device and node pairing state files before persisting approvals, so UUID-keyed pending and paired entries no longer disappear after a malformed JSON store write. Fixes #63035. Thanks @sar618.</li>
<li>Gateway/auth: clear reused stale device tokens and stop reconnecting on device-token mismatch in the Control UI and Node gateway clients, avoiding rate-limit loops after scope-upgrade or token-rotation handoffs. Fixes #71609. Thanks @ricksayhi.</li>
<li>Gateway/approvals: treat duplicate same-decision approval resolves as idempotent during the resolved-entry grace window, including consumed <code>allow-once</code> approvals, while returning an explicit already-resolved error for conflicting repeats. Fixes #59162; refs #58479 and #65486. Thanks @wikithoughts, @sajazuniga7-coder, and @mjmai20682068-create.</li>
<li>Channels/Telegram: honor <code>approvals.exec/plugin.targets[].accountId</code> when routing native approvals across multi-bot Telegram accounts while preserving unscoped Telegram targets for any account. Fixes #69916. Thanks @joerod26.</li>
<li>Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.</li>
<li>Agents/exec: omit the internal session-resume fallback preface from successful async exec completion messages sent directly back to chat. Fixes #67181. Thanks @raistlin88.</li>
<li>Agents/media: register detached <code>video_generate</code> and <code>music_generate</code> tool run contexts until terminal status, so Discord-backed provider jobs stay live in <code>/tasks</code> instead of becoming <code>lost</code> when the parent chat run context disappears. Thanks @vincentkoc.</li>
<li>Agents/media: prefer OpenAI image and video providers when the default model uses the OpenAI Codex auth alias, so auto media generation no longer falls through to Fal before GPT Image or Sora. Thanks @vincentkoc.</li>
<li>Tasks/media: infer agent ownership for session-scoped task records so <code>/tasks</code> agent-local fallback includes session-backed <code>video_generate</code> and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc.</li>
<li>Agents/media: keep long-running <code>video_generate</code> and <code>music_generate</code> tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc.</li>
<li>CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so <code>openclaw status --all</code> no longer reports a live gateway as unreachable after <code>missing scope: operator.read</code>. Fixes #49180; supersedes #47981. Thanks @openjay.</li>
<li>CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.</li>
<li>Slack/Socket Mode: use a 15s Slack SDK pong timeout by default and add <code>channels.slack.socketMode.clientPingTimeout</code>, <code>serverPingTimeout</code>, and <code>pingPongLoggingEnabled</code> overrides so stale-websocket handling no longer depends on app-event health heuristics. Fixes #14248; refs #58519, #64009, and #63488. Thanks @shivasymbl and @freerk.</li>
<li>Slack/media: bound private file and forwarded attachment downloads with idle and total timeouts while preserving placeholder fallback, so stalled Slack <code>file_share</code> media no longer wedges inbound message handling. Fixes #61850. Thanks @bassboy2k.</li>
<li>Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.</li>
<li>Slack/auto-reply: keep fully consumed text reset triggers such as <code>new session</code> out of <code>BodyForAgent</code> after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.</li>
<li>Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.</li>
<li>Plugins/runtime deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
<li>Auto-reply: bound the post-run pending tool-result delivery drain with a progress-aware idle timeout, so a never-settling tool-result task no longer leaves the session active forever while slow healthy deliveries can keep draining. Fixes #53889; supersedes #64733 and #73434. Thanks @zijunl and @wujiaming88.</li>
<li>Gateway/startup: start chat channels without waiting for primary model prewarm, keeping model warmup bounded in the background so Slack and other channels come online promptly when provider discovery is slow. Supersedes #73420. Thanks @dorukardahan.</li>
<li>Gateway/install: carry env-backed config SecretRefs such as <code>channels.discord.token</code> into generated service environments when they are present only in the installing shell, while keeping gateway auth SecretRefs non-persisted. Fixes #67817; supersedes #73426. Thanks @wdimaculangan and @ztexydt-cqh.</li>
<li>Auto-reply/commands: stop bare <code>/reset</code> and <code>/new</code> after reset hooks acknowledge the command, so non-ACP channels no longer fall through into empty provider calls while <code>/reset <message></code> and <code>/new <message></code> still seed the next model turn. Fixes #73367 and #73412. Thanks @hoyanhan, @wenxu007, and @amdhelper.</li>
<li>Providers/DeepSeek: backfill DeepSeek V4 <code>reasoning_content</code> on plain assistant replay messages as well as tool-call turns, so thinking sessions with prior tool use no longer fail follow-up requests with missing reasoning content. Fixes #73417; refs #71372. Thanks @34262315716 and @Bartok9.</li>
<li>Agents/gateway tool: strip full config payloads from <code>config.patch</code> and <code>config.apply</code> tool responses while preserving direct RPC responses, so config-heavy sessions no longer replay large redacted configs into transcript history. Fixes #47610; supersedes #73439. Thanks @HanenVit and @juan-flores077.</li>
<li>Auto-reply: preserve voice-note media from silent turns while continuing to suppress text and non-voice media, so <code>NO_REPLY</code> TTS replies still deliver the requested audio bubble. (#73406) Thanks @zqchris.</li>
<li>Channels/Mattermost: stop enqueueing regular inbound posts as system events, so Mattermost user messages reach the model only as user-role inbound-envelope content instead of also appearing as <code>System: Mattermost message...</code> directives. Fixes #71795. Thanks @juan-flores077.</li>
<li>Agents/media: qualify bare <code>agents.defaults.imageModel</code> and <code>pdfModel</code> refs from unique configured image-capable providers, so Ollama vision models such as <code>moondream</code> and <code>qwen2.5vl:7b</code> do not fall through to the default provider. Fixes #38816; supersedes #73396. Thanks @alainasclaw and @vincentkoc.</li>
<li>Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski.</li>
<li>Skills: require explicit <code>skills.entries.coding-agent.enabled</code> before exposing the bundled coding-agent skill, so installs with Codex on PATH but no OpenAI auth do not silently offer Codex delegation. Fixes #73358. Thanks @LaFleurAdvertising and @Sanjays2402.</li>
<li>Plugins/startup: treat manifestless Claude bundles as valid installed-plugin registry entries instead of stale missing manifests, so workspace bundles no longer force repeated derived registry rebuilds or noisy <code>plugins.entries.workspace</code> warnings during Gateway startup. Fixes #73433. Thanks @AnneVoss.</li>
<li>Agents/subagents: preserve <code>sessions_yield</code> as a paused subagent state and ignore its wait text while freezing completion output, so parent sessions wait for the final post-compaction answer instead of receiving intermediate progress or <code>(no output)</code>. Fixes #73413. Thanks @Ask-sola.</li>
<li>Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.</li>
<li>Plugins/runtime deps: refresh bundled runtime mirrors without deleting active import trees, so config-triggered restarts do not see transient missing plugin files during registration. Thanks @shakkernerd.</li>
<li>Channels/LINE: persist inbound image, video, audio, and file downloads in <code>~/.openclaw/media/inbound/</code> instead of temporary files so agents can still read LINE media after <code>/tmp</code> cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.</li>
<li>CLI/plugins: keep bundled plugin installs out of <code>plugins.load.paths</code> while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.</li>
<li>CLI/plugins: scope <code>plugins inspect <id></code> runtime loading to the matched plugin so single-plugin inspection does not load every plugin before checking the target. Thanks @shakkernerd.</li>
<li>CLI/plugins: remove managed copied-path plugin directories during uninstall and plan uninstall from metadata instead of runtime-loading plugins, so plugin lifecycle commands avoid unnecessary bundled runtime-deps work. Thanks @shakkernerd.</li>
<li>Cron tool: infer the creating session's agentId for <code>cron.add</code> jobs when <code>agentId</code> is omitted or passed as undefined, keeping scheduled agentTurn jobs routed to the session agent; #40571 identified the guard bug and supplied the focused regression coverage. Thanks @ChanningYul.</li>
<li>Cron/Telegram: add <code>--thread-id</code> to <code>openclaw cron add</code> and <code>openclaw cron edit</code>, preserving Telegram forum topic delivery targets across scheduled announcements. Carries forward #51581, #60373, and #60890. Thanks @ChunHao-dev.</li>
<li>Cron/Telegram: preserve session-derived Telegram topic thread IDs when isolated cron delivery explicitly targets the parent chat, keeping bare chat targets in the active forum topic without leaking stale topics to other chats. Carries forward #64708. Thanks @addelh.</li>
<li>Memory/compaction: keep pre-compaction memory-flush prompts runtime-only so session transcripts and <code>chat.history</code> no longer expose them as normal user turns. Fixes #54408 and #58956; refs #43567. Thanks @markgong and @guoyuhang9.</li>
<li>Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger <code>RangeError: Maximum call stack size exceeded</code>. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.</li>
<li>Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on <code>reader.read()</code>. Refs #72965 and #73120. Thanks @wdeveloper16.</li>
<li>Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.</li>
<li>Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as <code>openclaw-sandbox:bookworm-slim</code>, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.</li>
<li>Control UI/WebChat: confirm toolbar New Session button resets before dispatching <code>/new</code> while leaving typed <code>/new</code> and <code>/reset</code> commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).</li>
<li>Agents/models: keep per-agent primary models strict when <code>fallbacks</code> is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.</li>
<li>Gateway/models: add <code>models.pricing.enabled</code> so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.</li>
<li>Gateway/startup: warn when legacy <code>CLAWDBOT_*</code> or <code>MOLTBOT_*</code> environment variables are still present, pointing users to <code>OPENCLAW_*</code> names instead of failing silently. Fixes #53482; carries forward #53667. Thanks @lndyzwdxhs.</li>
<li>Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale <code>OPENCLAW_GATEWAY_TOKEN</code> or <code>OPENCLAW_GATEWAY_PASSWORD</code> values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.</li>
<li>Doctor/state: require an interactive confirmation before archiving orphan transcript files, so <code>openclaw doctor --fix</code> no longer silently renames recoverable session history after upgrades regenerate <code>sessions.json</code>. Fixes #73106. Thanks @scottgl9.</li>
<li>Cron/Telegram: preserve explicit <code>:topic:</code> delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.</li>
<li>Build/runtime: write the runtime-postbuild stamp after <code>pnpm build</code> writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.</li>
<li>Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.</li>
<li>CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so <code>openclaw channels list</code> shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.</li>
<li>CLI/model probes: keep <code>infer model run --gateway</code> raw by skipping prior session transcript, bootstrap context, context-engine assembly, tools, and bundled MCP servers, so local backends can be tested without full agent-context overhead. Fixes #73308. Thanks @ScientificProgrammer.</li>
<li>CLI/image describe: pass <code>--prompt</code> and <code>--timeout-ms</code> through <code>infer image describe</code> and <code>describe-many</code>, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Addresses #63700. Thanks @cedricjanssens.</li>
<li>Providers/Ollama: reject long non-linguistic Kimi/GLM symbol runs as provider failures instead of storing them as successful visible assistant replies, so fallback or error handling can recover from garbled cloud output. Fixes #64262; refs #67019. Thanks @Kloz813 and @xiaomenger123.</li>
<li>CLI/model probes: reject empty or whitespace-only <code>infer model run --prompt</code> values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.</li>
<li>Gateway/media: route text-only <code>chat.send</code> image offloads through media-understanding fields so <code>agents.defaults.imageModel</code> can describe WebChat attachments instead of leaving only an opaque <code>media://inbound</code> marker. Fixes #72968. Thanks @vorajeeah.</li>
<li>Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.</li>
<li>Gateway/model pricing: skip plugin manifest discovery during background pricing refreshes when <code>plugins.enabled: false</code>, so disabled-plugin setups do not keep rebuilding plugin metadata from the Gateway hot path. Fixes #73291. Thanks @slideshow-dingo and @fishgills.</li>
<li>Ollama/thinking: validate <code>/think</code> commands against live Ollama catalog reasoning metadata and preserve explicit native <code>params.think</code>/<code>params.thinking</code>, so models whose <code>/api/show</code> capabilities include <code>thinking</code> expose <code>low</code>, <code>medium</code>, <code>high</code>, and <code>max</code> instead of being stuck on <code>off</code>. Fixes #73366. Thanks @cymise.</li>
<li>Gateway/sessions: remove automatic oversized <code>sessions.json</code> rotation backups, deprecate <code>session.maintenance.rotateBytes</code>, and teach <code>openclaw doctor --fix</code> to remove the ignored key so hot session writes no longer copy multi-MB stores. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
<li>Channels/Telegram: fail fast when Telegram rejects the startup <code>getMe</code> token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading <code>deleteWebhook</code> cleanup failures. Fixes #47674. Thanks @samaedan-arch.</li>
<li>ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.</li>
<li>CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep <code>--custom-image-input</code>/<code>--custom-text-input</code> overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974.</li>
<li>Models/OpenAI Codex: stop listing or resolving unsupported <code>openai-codex/gpt-5.4-mini</code> rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct <code>openai/gpt-5.4-mini</code> available. Fixes #73242. Thanks @0xCyda.</li>
<li>Plugin SDK: restore the root <code>stringEnum</code> and <code>optionalStringEnum</code> exports on both the published SDK entry and runtime root-alias bridge, so older external plugins can keep building and loading while migrating to focused SDK subpaths. Fixes #68279. Thanks @marzliak.</li>
<li>Plugin SDK: restore the root-alias bridge for <code>registerContextEngine</code> and expose missing legacy compat helpers <code>normalizeAccountId</code> and <code>resolvePreferredOpenClawTmpDir</code> so older external plugins such as <code>openclaw-weixin</code> can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85.</li>
<li>Auth profiles: make <code>openclaw doctor --fix</code> migrate legacy flat <code>auth-profiles.json</code> files such as <code>{ "ollama-windows": { "apiKey": "ollama-local" } }</code> to canonical provider default API-key profiles with a backup, so custom Ollama/OpenAI-compatible providers recover cleanly after upgrading. Fixes #59629; supersedes #59642. Thanks @Xsanders555 and @Linux2010.</li>
<li>Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.</li>
<li>Feishu/inbound files: recover CJK filenames from plain <code>Content-Disposition: filename=</code> download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.</li>
<li>Channels/Telegram: normalize accidental full <code>/bot<TOKEN></code> Telegram <code>apiRoot</code> values at runtime and teach <code>openclaw doctor --fix</code> to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris.</li>
<li>Zalo Personal: persist refreshed <code>zca-js</code> session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa.</li>
<li>Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so <code>createSubsystemLogger().info/warn/error</code> output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev.</li>
<li>Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints <code>openclaw-unknown-*</code> directories or loops on <code>ENOTEMPTY</code>. Fixes #72956. (#73205) Thanks @SymbolStar.</li>
<li>Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh.</li>
<li>Agents/failover: seed non-claude-cli fallback prompts with Claude Code session context when a claude-cli attempt fails, so fallback models do not restart cold after billing or quota failover. (#72069) Thanks @stainlu.</li>
<li>Agents/CLI runner: transfer bundle-MCP tempDir cleanup from the per-turn runner finally to the Claude live-session lifecycle, so persistent Claude CLI sessions keep their <code>--mcp-config</code> directory until the live subprocess closes. Fixes #73244. Thanks @edwin-rivera-dev.</li>
<li>Gateway/nodes: allow Windows companion nodes to use safe declared commands such as canvas, camera list, location, device info, and screen snapshot by default while keeping dangerous media commands opt-in. (#71884) Thanks @shanselman.</li>
<li>Agents/cron: clarify agent-tool and CLI cron timezone guidance so supplied <code>tz</code> values use local wall-clock cron fields and omitted cron <code>tz</code> falls back to the Gateway host local timezone. Fixes #53669; carries forward #46177. (#73372) Thanks @chen-zhang-cs-code and @maranello-o.</li>
<li>Providers/Qwen: allow explicitly configured <code>qwen/qwen3.6-plus</code> to resolve on Qwen Coding Plan endpoints while keeping the built-in catalog from advertising it there. Fixes #63654; carries forward #63987. Thanks @jepson-liu.</li>
<li>Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so <code>deleteWebhook</code> IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.</li>
<li>Gateway/agents: accept heartbeat, cron, and webhook as internal channel hints for agent runs so <code>sessions_spawn</code> works from non-delivery parent sessions while unknown channel hints still fail closed. Fixes #73237. Thanks @KeWang0622.</li>
<li>Gateway/models: merge explicit <code>models.providers.*.models</code> rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.</li>
<li>Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.</li>
<li>Gateway/Docker: keep config-triggered restarts in-process inside containers instead of spawning a detached child and exiting PID 1 cleanly, so Docker Swarm and other on-failure supervisors do not leave the service stuck at 0/1 replicas. Fixes #73178. Thanks @du-nguyen-IT007.</li>
<li>CLI/tasks: ship the task-registry control runtime in npm packages so <code>openclaw tasks cancel</code> can load ACP/subagent cancellation helpers from published builds. Fixes #68997. Thanks @1OAKDesign.</li>
<li>Channels/Telegram: preserve unsent generated media after partial reply streaming has already delivered the text, so <code>image_generate</code> outputs still reach Telegram as photos instead of being dropped from the final payload. Fixes #73253. Thanks @mlaihk.</li>
<li>Memory-core/dreaming: cap detached Dream Diary narrative subagents across cron sweeps so multi-workspace dreaming no longer fans out unbounded subagent sessions, lock contention, and cascading narrative timeouts. Fixes #73198. (#73287) Thanks @KeWang0622.</li>
<li>CLI/agents: close local one-shot Claude live stdio sessions and bundled MCP loopback resources after embedded <code>openclaw agent --local</code> runs, while keeping gateway-owned MCP loopback cleanup internal to the Gateway. Thanks @frankekn.</li>
<li>Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp.</li>
<li>Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live <code>npx</code> adapter resolution. Fixes #73202. Thanks @joerod26.</li>
<li>Memory/compaction: let pre-compaction memory flush use an exact <code>agents.defaults.compaction.memoryFlush.model</code> override such as <code>ollama/qwen3:8b</code> without inheriting the active session fallback chain, so local housekeeping can avoid paid conversation models. Fixes #53772. Thanks @limen96.</li>
<li>macOS/update: stop managed Gateway services before package replacement and keep LaunchAgent service secrets out of world-readable plist metadata by loading them from owner-only env files. Fixes #72996. Thanks @Mathewb7.</li>
<li>Google Meet: keep observe-only Chrome joins and setup checks from requiring BlackHole or audio bridge commands, avoid granting or selecting the microphone in observe-only mode, and make <code>test_speech</code> report fresh realtime output-byte verification instead of only confirming a queued utterance. Refs #72478. Thanks @DougButdorf.</li>
<li>Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.</li>
<li>Control UI/models: request the configured Gateway model-list view so dashboards with only <code>models.providers.*.models</code> show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw.</li>
<li>CLI/models: keep default-model and allowlist pickers on explicit <code>models.providers.*.models</code> entries when <code>models.mode</code> is <code>replace</code> instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg.</li>
<li>Media/security: tighten media-understanding MIME sanitization so parameterized MIME values stay end-anchored and malformed whitespace or suffix payloads are rejected before file-context handling. Fixes #9795; carries forward #68225 with related review/test context from #61016/#68456. Thanks @ymaxgit, @bluesky6868, and @shamsulalam1114.</li>
<li>Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip <code>InteractionEventListener</code> listener timeouts. Fixes #73204. Thanks @slideshow-dingo.</li>
<li>Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.</li>
<li>Models/fallbacks: record first-class <code>model.fallback_step</code> trajectory events with from/to models, failure detail, chain position, and final outcome so support exports preserve the primary model failure even when a later fallback also fails. Fixes #71744. Thanks @nikolaykazakovvs-ux.</li>
<li>Gateway/agents: block agent <code>exec</code> from launching interactive <code>openclaw channels login</code> flows and abort active agent runs after invalid-config recovery restores last-known-good config, preventing known channel-login and reload paths from wedging replies. Refs #72338. Thanks @midhunmonachan.</li>
<li>Gateway/diagnostics: emit payload-free liveness warnings with event-loop delay, event-loop utilization, CPU-core ratio, active-session counts, and OTEL warning metrics/spans so live-but-stalled Gateways capture CPU-spin context in stability bundles and telemetry. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
<li>Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc.</li>
<li>Heartbeat/models: show heartbeat model bleed guidance on context-overflow resets when the last runtime model matches configured <code>heartbeat.model</code>, so smaller local heartbeat models point users to <code>isolatedSession</code> or <code>lightContext</code> instead of only compaction-buffer tuning. Fixes #67314. Thanks @Knightmare6890.</li>
<li>Subagents/models: persist <code>sessions_spawn.model</code> and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99.</li>
<li>Channels/Telegram: keep webhook-mode local listeners alive and retry Telegram <code>setWebhook</code> registration after recoverable startup network failures, so transient Bot API timeouts no longer leave reverse proxies pointing at a closed listener. Fixes #71834. Thanks @jinon86.</li>
<li>Agents/ACPX: bundle the Codex ACP adapter and launch it from the isolated <code>CODEX_HOME</code> wrapper before falling back to npm, so Codex ACP startup no longer depends on live <code>npx</code> resolution or the stale <code>@zed-industries/codex-acp@^0.11.1</code> range. Fixes #72037; refs #73202. Thanks @jasonftl, @sazora, and @joerod26.</li>
<li>Agents/ACPX: register the embedded ACP backend at Gateway startup through a lightweight ACP backend SDK path and without importing the heavy ACPX runtime until an ACP session or explicit startup probe needs it, reducing baseline Gateway RSS. Thanks @vincentkoc.</li>
<li>CLI/update: keep restart health polling when the restarted Gateway is reachable but has not reported its version yet, so macOS service restarts do not fail early with <code>actual unavailable</code>. Thanks @ProspectOre.</li>
<li>Backup: skip installed plugin <code>extensions/*/node_modules</code> dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang.</li>
<li>Cron/models: fail isolated cron runs closed when an explicit <code>payload.model</code> is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang.</li>
<li>Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William.</li>
<li>Talk Mode: resolve <code>messages.tts.providers.<id>.apiKey</code> through the active runtime snapshot for <code>talk.config</code>, so Talk overlays can discover SecretRef-backed speech providers without falling back to local speech. Fixes #73109. (#73111) Thanks @omarshahine.</li>
<li>Memory/Ollama: resolve <code>memorySearch.provider</code> custom provider ids through their configured <code>models.providers.<id>.api</code> owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as <code>ollama-5080</code> without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.</li>
<li>CLI/memory: skip eager context-window warmup for <code>openclaw memory</code> commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.</li>
<li>CLI/Telegram: route Telegram <code>message send</code> and poll actions through the running Gateway when available, so packaged installs use the staged <code>grammy</code> runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.</li>
<li>Plugins/runtime deps: prepare staged bundled plugin dependencies before loading packaged public surfaces, so OpenClaw's Telegram runtime/test facade loads resolve <code>grammy</code> from the managed runtime-deps stage without copying dependencies into the global package root. Refs #73140. Thanks @oalansilva.</li>
<li>Agents/exec: emit <code>(no output)</code> for silent exec update and node-host result blocks so Anthropic-compatible providers no longer reject empty tool-result text after quiet commands. Fixes #73117. Thanks @pfrederiksen and @Sanjays2402.</li>
<li>Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead.</li>
<li>Gateway/config: let config reload continue in degraded mode when invalidity is scoped to plugin entries, so incompatible plugin configs can be skipped and the Gateway restart can still pick up the rest of the config after rollbacks. Fixes #73131. Thanks @Adam-Researchh.</li>
<li>Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to <code>openclaw-lark</code>. Fixes #56794. Thanks @wuji-tech-dev.</li>
<li>CLI/status: show skipped fast-path memory checks as <code>not checked</code> and report active custom memory plugin runtime status from <code>status --json --all</code> without requiring built-in <code>agents.defaults.memorySearch</code>, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.</li>
<li>Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.</li>
<li>Discord/group chats: keep group/channel replies private by default unless the agent explicitly uses the message tool, so always-on rooms can lurk without leaking automatic final, block, preview, or status-reaction output; <code>messages.groupChat.visibleReplies: "automatic"</code> restores legacy auto-posting. (#73046) Thanks @scoootscooob.</li>
<li>Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh.</li>
<li>Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers.</li>
<li>Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts.</li>
<li>Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts.</li>
<li>Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin <code>embedding.apiKey</code>, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai.</li>
<li>CLI/parents: invoking <code>openclaw <parent></code> (memory, channels, plugins, approvals, devices, cron, mcp) without a subcommand now prints the parent's help and exits <code>0</code>, matching <code><parent> --help</code> and the existing <code>agents</code> / <code>sessions</code> defaults so shell <code>&&</code> chains and pnpm wrappers no longer surface a misleading <code>ELIFECYCLE Command failed with exit code 1.</code> line. Fixes #73077. Thanks @hclsys.</li>
<li>Plugins/hooks: time out never-settling <code>agent_end</code> observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099.</li>
<li>Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every <code>config.get</code>/<code>config.schema</code>, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb.</li>
<li>Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending <code>encoding_format</code>, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial.</li>
<li>Plugins/install: run dependency installs with npm error-level logging instead of silent mode so failed plugin or hook installs surface actionable npm errors such as EUNSUPPORTEDPROTOCOL instead of <code>npm install failed:</code> with no detail. (#73093) Thanks @sanctrl.</li>
<li>Memory/LanceDB: bound memory recall embedding queries with a new <code>recallMaxChars</code> setting, prefer the latest user message over channel prompt metadata during auto-recall, and document the knob so small Ollama embedding models avoid context-length failures. Fixes #56780. Thanks @rungmc357 and @zak-collaborator.</li>
<li>CLI/skills: resolve workspace-backed skills commands from <code>--agent</code>, then the current agent workspace, before falling back to the default agent, so multi-agent ClawHub installs, updates, and status checks stay scoped to the active workspace. Fixes #56161; carries forward #72726. Thanks @langbowang and @luyao618.</li>
<li>Plugin SDK: fall back from partial bundled plugin directory overrides to package source public surfaces while preserving <code>OPENCLAW_DISABLE_BUNDLED_PLUGINS</code> as a hard disable. (#72817) Thanks @serkonyc.</li>
<li>Agents/ACPX: stop forwarding Codex ACP timeout config controls that Codex rejects while preserving OpenClaw's run-timeout watchdog for ACP subagents. Fixes #73052. Thanks @pfrederiksen and @richa65.</li>
<li>Memory Core: stream fallback vector search scoring with a bounded top-K result set so large indexes do not materialize every chunk embedding when sqlite-vec is unavailable. (#73069) Thanks @parkertoddbrooks.</li>
<li>Memory Core: stream embedding-cache seeding during safe reindex so large local caches do not materialize every row into the V8 heap before the atomic rebuild. (#73067) Thanks @parkertoddbrooks.</li>
<li>Memory/Ollama: add <code>memorySearch.remote.nonBatchConcurrency</code> for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys.</li>
<li>macOS app: update Peekaboo, ElevenLabsKit, and MLX TTS helper dependencies, make canvas file watching and config/exec-approval state writes reliable under concurrent app/test activity, and keep the app plus helper builds warning-free. Thanks @Blaizzy.</li>
<li>iOS app: refresh SwiftPM/XcodeGen source hygiene, make app, extension, watch, and curated shared Swift files pass the prebuild SwiftFormat and SwiftLint checks, move relay registration off deprecated StoreKit receipt APIs, and keep simulator builds and logic tests warning-free. Thanks @ngutman.</li>
<li>Agents/models: keep <code>models.json</code> readiness and provider-hook caches warm across repeated agent and subagent model resolution while preserving external <code>models.json</code> invalidation, reducing repeated provider-plugin loads on slower ARM64 hosts. Fixes #73075. Thanks @jochen.</li>
<li>Docs/tools: clarify that <code>tools.profile: "messaging"</code> is intentionally narrow and that <code>tools.profile: "full"</code> is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit.</li>
<li>Control UI/Agents: redact tool-call args, partial/final results, derived exec output, and configured custom secret patterns before streaming tool events to the Control UI, so tool output cannot expose provider or channel credentials. Fixes #72283. (#72319) Thanks @volcano303 and @BunsDev.</li>
<li>Agents/sessions: keep <code>sessions_history</code> recall redaction enabled even when general log redaction is disabled, and clarify that safety-boundary UI/tool/diagnostic payloads still redact independently of <code>logging.redactSensitive</code>. Carries forward #72319. Thanks @volcano303 and @BunsDev.</li>
<li>Providers/Codex: pass agent and workspace directories into provider stream wrappers so Codex native <code>web_search</code> activation can evaluate the correct auth context, and smoke-test the built status-message runtime by resolving the emitted bundle name. Carries forward #67843; refs #65909. Thanks @neilofneils404.</li>
<li>Cron/models: keep <code>payload.model</code> as a per-job primary that can use configured fallbacks, while still letting <code>payload.fallbacks: []</code> make cron runs strict and avoid hidden agent-primary retries. Refs #73023. Thanks @pavelyortho-cyber.</li>
<li>Models/fallbacks: treat user-selected session models as exact choices, so <code>/model ollama/...</code> and model-picker switches fail visibly when the selected provider is unreachable instead of answering from an unrelated configured fallback. Fixes #73023. Thanks @pavelyortho-cyber.</li>
<li>Codex harness: keep ChatGPT subscription app-server runs from inheriting <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code>, and fall back to <code>CODEX_API_KEY</code> / <code>OPENAI_API_KEY</code> app-server login only when no Codex account is available. Fixes #73057. Thanks @holgergruenhagen and @pashpashpash.</li>
<li>CLI/model probes: fail local <code>infer model run</code> probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber.</li>
<li>CLI/Ollama: run local <code>infer model run</code> through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native <code>/api/chat</code> request. Fixes #72851. Thanks @TotalRes2020.</li>
<li>Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk.</li>
<li>Daemon/service: only emit hard-coded version-manager paths such as <code>~/.volta/bin</code>, <code>~/.asdf/shims</code>, <code>~/.bun/bin</code>, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so <code>openclaw doctor</code> no longer flags <code>gateway.path.non-minimal</code> against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.</li>
<li>CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place <code>pnpm build</code> updates are visible to the next <code>openclaw</code> CLI invocation. Fixes #73037. Thanks @LouisGameDev.</li>
<li>Agents/group chat: keep silent-allowed empty and reasoning-only turns on the <code>NO_REPLY</code> path without injecting visible-answer retry prompts, and clarify the group prompt so agents use the exact silent token instead of prose. Thanks @vincentkoc.</li>
<li>Agents/group chat: move <code>NO_REPLY</code> mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.</li>
<li>Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request <code>reasoning.encrypted_content</code> on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required <code>rs_*</code> state beside <code>msg_*</code> items. Fixes #73053. Thanks @odb36777.</li>
<li>Gateway/startup: treat <code>plugins.enabled=false</code> as an early plugin fast path, skipping plugin auto-enable discovery, gateway plugin lookup/runtime-dependency staging, and stale-plugin cleanup warnings while preserving channel blocker warnings. (#73041) Thanks @WuKongAI-CMU.</li>
<li>Channels/commands: make generated <code>/dock-*</code> commands switch the active session reply route through <code>session.identityLinks</code> instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.</li>
<li>Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.</li>
<li>Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no <code>gateway_start</code> hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.</li>
<li>Gateway/channels: start bundled channel accounts with a lightweight <code>runtimeContexts</code> surface instead of importing the full reply/routing/session channel runtime before <code>startAccount</code>, so Discord, Telegram, Slack, Matrix, and QQBot startup no longer block on unrelated channel helper graphs. Refs #72846 and #72960. Thanks @mrz1836, @RayWoo, and @rollingshmily.</li>
<li>Gateway/supervisor: exit cleanly when a supervised restart finds an existing healthy gateway and bound retries when the existing gateway stays unhealthy, so stale lock contention cannot loop indefinitely. Refs #72846. Thanks @azgardtek.</li>
<li>Gateway/startup: scope primary-model provider discovery during channel prewarm to the configured provider owner and add split startup trace timings, so boot avoids staging unrelated bundled provider dependencies while setup discovery remains broad. Fixes #73002. Thanks @Schnup03.</li>
<li>Plugins/runtime deps: declare retained staged bundled plugin dependencies in the npm staging manifest while installing only newly missing packages, so Gateway restarts avoid reinstalling the full retained dependency set when one runtime dependency is absent. Fixes #73055. Thanks @GCorp2026.</li>
<li>CLI/status: keep default <code>openclaw status</code> off the heavyweight security audit, plugin compatibility, and memory-vector probes while still showing configured Telegram channels through setup metadata, so routine health checks stay fast and no longer render an empty Channels table. Fixes #72993. Thanks @comick1.</li>
<li>Channels/Telegram: send a best-effort native typing cue immediately after an inbound message is accepted, so slow pre-dispatch turns show Telegram liveness before queueing, compaction, model, or tool work starts. Fixes #63759. Thanks @alessandropcostabr.</li>
<li>Channels/Telegram: stop native approval startup auth failures from retrying every second, while still waiting through retryable Gateway auth handoffs, so Telegram approval setup problems no longer create a reconnect/log loop during channel startup. Refs #72846 and #72867. Thanks @kiranvk-2011 and @porly1985.</li>
<li>Channels/Microsoft Teams: unwrap staged CommonJS JWT runtime dependencies before Bot Connector token validation so inbound Teams messages no longer 401 after the bundled runtime-deps move. Fixes #73026. Thanks @kbrown10000.</li>
<li>Gateway/auth: allow local direct callers in trusted-proxy mode to use the configured gateway password as an internal fallback while keeping token fallback rejected. Fixes #17761. Thanks @dashed, @vincentkoc, and @jetd1.</li>
<li>Gateway/auth: add explicit <code>trustedProxy.allowLoopback</code> support for same-host loopback reverse proxies while keeping loopback trusted-proxy auth fail-closed by default and preserving required-header and allowlist checks. Fixes #59167; carries forward #63379. Thanks @Matir, @jeremyakers, and @mrosmarin.</li>
<li>Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.</li>
<li>Cron: accept <code>delivery.threadId</code> in Gateway cron add/update schemas so scheduled announce delivery can target Telegram forum topics and other threaded channel destinations through the documented delivery path. Fixes #73017. Thanks @coachsootz.</li>
<li>Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss <code>chokidar</code> or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.</li>
<li>Plugins/runtime deps: reuse unchanged bundled plugin runtime mirrors instead of rebuilding plugin trees on every load, cutting avoidable writes and restart/reconnect I/O on slow storage. Fixes #72933. Thanks @jasonftl.</li>
<li>Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409.</li>
<li>Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.</li>
<li>CLI/message: resolve targeted <code>openclaw message</code> channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.</li>
<li>Plugins/startup: parse strict JSON plugin manifests with native JSON first and keep JSON5 as the compatibility fallback, reducing manifest registry CPU during Gateway boot and CLI startup. Fixes #73011. Thanks @jasonftl.</li>
<li>CLI/models: keep route-first <code>models status --json</code> stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.</li>
<li>Gateway/runtime: keep dirty-tree status calls from rebuilding live <code>dist</code>, clear stale task and restart state across in-process restarts, retry transient Discord lazy imports, and let channel startup continue after slow model warmup so browser, Discord, and voice-call sidecars come online. Thanks @vincentkoc.</li>
<li>Security/CodeQL: replace file SecretRef id gateway schema regex validation with segment-aligned predicates and set empty permissions on release summary/backfill jobs so the narrowed CodeQL profile stays clean. Thanks @vincentkoc.</li>
<li>Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future <code>updatedAt</code> values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.</li>
<li>Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.</li>
<li>Sessions: remove trajectory runtime and pointer sidecars when session maintenance prunes, caps, or disk-evicts their owning session, while preserving sidecars still referenced by live rows. Fixes #73000. Thanks @jared-rebel.</li>
<li>Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136.</li>
<li>Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.</li>
<li>Media understanding: reject malformed MIME values with trailing junk while preserving standard parameter tails before enrichment uses them. (#72914) Thanks @volcano303.</li>
<li>WebChat: keep bare <code>/new</code> and <code>/reset</code> prompts from producing empty transcript text by inserting the hidden session marker when the visible tail is blank. (#72863) Thanks @mahopan.</li>
<li>CLI/update: explain completion-cache refresh timeouts with manual refresh guidance instead of surfacing a raw low-level timeout. Fixes #72842. (#72850) Thanks @iot2edge.</li>
<li>Memory-core/dreaming: give narrative generation a 60-second timeout so slower local or remote models can finish instead of timing out at 15 seconds. Fixes #72837. (#72852) Thanks @RayWoo.</li>
<li>Plugins/hooks: inject each plugin's resolved config into internal hook event context without mutating the shared event object. (#72888) Thanks @jalapeno777.</li>
<li>Agents/ACP: pass the resolved ACP agent directory into media understanding so per-agent media caches and config are used for ACP-dispatched image turns. (#72832) Thanks @luyao618.</li>
<li>Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit at valid UTF-8 boundaries. (#72809) Thanks @luyao618.</li>
<li>Feishu: treat groups explicitly configured under channels.feishu.groups as admitted even when groupAllowFrom is empty, while preserving groupPolicy: "disabled" as a hard group block and keeping groups.\* wildcard defaults non-admitting. Fixes #67687. (#72789) Thanks @MoerAI.</li>
<li>Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.</li>
<li>WebChat: read <code>chat.history</code> from active transcript branches, drop stale streamed assistant tails once final history catches up, and coalesce duplicate in-flight Control UI submits, so rewritten prompts, completed replies, and rapid send events no longer render or process twice. Fixes #72975, #72963, and #72974. Thanks @dmagdici, @lhtpluto, and @Benjamin5281999.</li>
<li>WebChat/TTS: persist automatic final-mode TTS audio as a supplemental audio-only transcript update instead of adding a second assistant message with the same visible text. Fixes #72830. Thanks @lhtpluto.</li>
<li>Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as <code>tsserver</code> do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.</li>
<li>Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.</li>
<li>Logging: write validated diagnostic trace context as top-level <code>traceId</code>, <code>spanId</code>, <code>parentSpanId</code>, and <code>traceFlags</code> fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.</li>
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
<li>Providers/Ollama: honor <code>/api/show</code> capabilities when registering local models so non-tool Ollama models no longer receive the agent tool surface, and keep native Ollama thinking opt-in instead of enabling it by default. Fixes #64710 and duplicate #65343. Thanks @yuan-b, @netherby, @xilopaint, and @Diyforfun2026.</li>
<li>Control UI/Agents: remount the Overview model controls when switching agents so the primary-model picker cannot retain stale per-agent selection. Fixes #39392; carries forward #39401, notes the duplicate #39495 approach, and keeps #46275/#54724 broader stabilization out of scope. Thanks @daijunyi002, @SergioChan, @aworki, and @wsyjh8.</li>
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
<li>Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @vincentkoc.</li>
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>. Thanks @vincentkoc.</li>
<li>Gateway/plugins: resolve <code>gateway_start</code> cron hooks from live Gateway runtime state before the legacy deps fallback, so memory-core dreaming cron reconciliation keeps working on installs where <code>deps.cron</code> is not populated during service startup. Fixes #72835. Thanks @RayWoo.</li>
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
<li>Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.</li>
<li>Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.</li>
<li>Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale <code>plugins list</code> entries. Thanks @vincentkoc.</li>
<li>Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @vincentkoc.</li>
<li>Plugins: fail <code>plugins update</code> when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @vincentkoc.</li>
<li>WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.</li>
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
<li>Gateway/chat: preserve repeated boundary characters while merging assistant chat stream deltas, including repeated digits, CJK characters, and markdown/table tokens. Fixes #63769; carries forward #63994 and #65457. Thanks @yon950905 and @mohuaxiao.</li>
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.</li>
<li>WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.</li>
<li>Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.</li>
<li>TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in <code>tts.voice.preferAudioFileFormat</code> channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses <code>file-type</code> and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.</li>
<li>Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.</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.27/OpenClaw-2026.4.27.zip" length="50595360" type="application/octet-stream" sparkle:edSignature="X8DQNQNWVcvtpYLkhZcsKNpnA78ycyzgGlZaG0XBY1GIph3oZNUIpAszGGocJVqTK7+F89Au5ZPb60mOqJQ6DQ=="/>
</item>
</channel>
</rss>

View File

@@ -285,7 +285,7 @@ Common failure quick-fixes:
- `pairing required` before tests start:
- approve pending device pairing (`openclaw devices approve --latest`) and rerun.
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure gateway canvas host is running and reachable, keep the app on the **Screen** tab. The app will auto-refresh canvas capability once; if it still fails, reconnect app and rerun.
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:
- app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active.

View File

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

View File

@@ -36,6 +36,7 @@ import ai.openclaw.app.node.Quad
import ai.openclaw.app.node.SmsHandler
import ai.openclaw.app.node.SmsManager
import ai.openclaw.app.node.SystemHandler
import ai.openclaw.app.node.TalkHandler
import ai.openclaw.app.node.asObjectOrNull
import ai.openclaw.app.node.asStringOrNull
import ai.openclaw.app.node.invokeErrorFromThrowable
@@ -205,6 +206,16 @@ class NodeRuntime(
deviceHandler = deviceHandler,
notificationsHandler = notificationsHandler,
systemHandler = systemHandler,
talkHandler =
object : TalkHandler {
override suspend fun handlePttStart(paramsJson: String?): GatewaySession.InvokeResult = handleTalkPttStart()
override suspend fun handlePttStop(paramsJson: String?): GatewaySession.InvokeResult = handleTalkPttStop()
override suspend fun handlePttCancel(paramsJson: String?): GatewaySession.InvokeResult = handleTalkPttCancel()
override suspend fun handlePttOnce(paramsJson: String?): GatewaySession.InvokeResult = handleTalkPttOnce()
},
photosHandler = photosHandler,
contactsHandler = contactsHandler,
calendarHandler = calendarHandler,
@@ -222,13 +233,13 @@ class NodeRuntime(
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
debugBuild = { BuildConfig.DEBUG },
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
onCanvasA2uiPush = {
_canvasA2uiHydrated.value = true
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
},
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
@@ -881,6 +892,80 @@ class NodeRuntime(
setVoiceCaptureMode(if (value) VoiceCaptureMode.TalkMode else VoiceCaptureMode.Off)
}
private suspend fun handleTalkPttStart(): GatewaySession.InvokeResult =
runPreparedTalkPttCommand {
val payload = talkMode.beginPushToTalk()
GatewaySession.InvokeResult.ok(payload.toJson())
}
private suspend fun handleTalkPttStop(): GatewaySession.InvokeResult =
runTalkPttCommand {
val payload = talkMode.endPushToTalk()
finishTalkCaptureIfIdle()
GatewaySession.InvokeResult.ok(payload.toJson())
}
private suspend fun handleTalkPttCancel(): GatewaySession.InvokeResult =
runTalkPttCommand {
val payload = talkMode.cancelPushToTalk()
finishTalkCaptureIfIdle()
GatewaySession.InvokeResult.ok(payload.toJson())
}
private suspend fun handleTalkPttOnce(): GatewaySession.InvokeResult =
runPreparedTalkPttCommand {
val payload = talkMode.runPushToTalkOnce()
finishTalkCaptureIfIdle()
GatewaySession.InvokeResult.ok(payload.toJson())
}
private suspend fun runPreparedTalkPttCommand(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult =
runTalkPttCommand {
prepareTalkCapture()
try {
block()
} catch (err: Throwable) {
cleanupFailedTalkCapture()
throw err
}
}
private suspend fun runTalkPttCommand(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult =
try {
block()
} catch (err: Throwable) {
val (code, message) = invokeErrorFromThrowable(err)
GatewaySession.InvokeResult.error(code = code, message = message)
}
private suspend fun prepareTalkCapture() {
if (!hasRecordAudioPermission()) {
throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
}
micCapture.setMicEnabled(false)
stopVoicePlayback()
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.TalkMode)
talkMode.ttsOnAllResponses = true
talkMode.setPlaybackEnabled(speakerEnabled.value)
talkMode.ensureChatSubscribed()
externalAudioCaptureActive.value = true
}
private suspend fun cleanupFailedTalkCapture() {
runCatching { talkMode.cancelPushToTalk() }
talkMode.ttsOnAllResponses = false
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
externalAudioCaptureActive.value = false
}
private fun finishTalkCaptureIfIdle() {
if (!talkMode.isEnabled.value && !talkMode.isListening.value && !talkMode.isSpeaking.value) {
talkMode.ttsOnAllResponses = false
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
externalAudioCaptureActive.value = false
}
}
val speakerEnabled: StateFlow<Boolean>
get() = prefs.speakerEnabled

View File

@@ -278,14 +278,13 @@ class GatewayDiscovery(
return legacyHostAddress(resolved)
}
private fun legacyHostAddress(resolved: NsdServiceInfo): String? {
return try {
private fun legacyHostAddress(resolved: NsdServiceInfo): String? =
try {
val host = NsdServiceInfo::class.java.getMethod("getHost").invoke(resolved) as? InetAddress
host?.hostAddress
} catch (_: Throwable) {
null
}
}
private fun publish() {
_gateways.value =
@@ -529,20 +528,20 @@ class GatewayDiscovery(
val cm = connectivity ?: return null
// Prefer VPN (Tailscale) when present; otherwise use the active network.
trackedNetworks(cm).firstOrNull { n ->
val caps = cm.getNetworkCapabilities(n) ?: return@firstOrNull false
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}?.let { return it }
trackedNetworks(cm)
.firstOrNull { n ->
val caps = cm.getNetworkCapabilities(n) ?: return@firstOrNull false
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}?.let { return it }
return cm.activeNetwork
}
private fun trackedNetworks(cm: ConnectivityManager): List<Network> {
return buildList {
private fun trackedNetworks(cm: ConnectivityManager): List<Network> =
buildList {
cm.activeNetwork?.let(::add)
addAll(availableNetworks)
}.distinct()
}
private fun createDirectResolver(): Resolver? {
val cm = connectivity ?: return null

View File

@@ -1,3 +1,3 @@
package ai.openclaw.app.gateway
const val GATEWAY_PROTOCOL_VERSION = 3
const val GATEWAY_PROTOCOL_VERSION = 4

View File

@@ -135,7 +135,7 @@ class GatewaySession(
private val writeLock = Mutex()
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
@Volatile private var canvasHostUrl: String? = null
@Volatile private var pluginSurfaceUrls: Map<String, String> = emptyMap()
@Volatile private var mainSessionKey: String? = null
@@ -185,7 +185,7 @@ class GatewaySession(
scope.launch(Dispatchers.IO) {
job?.cancelAndJoin()
job = null
canvasHostUrl = null
pluginSurfaceUrls = emptyMap()
mainSessionKey = null
onDisconnected("Offline")
}
@@ -196,7 +196,20 @@ class GatewaySession(
currentConnection?.closeQuietly()
}
fun currentCanvasHostUrl(): String? = canvasHostUrl
fun currentCanvasHostUrl(): String? = pluginSurfaceUrls["canvas"]
suspend fun refreshCanvasHostUrl(timeoutMs: Long = 8_000): String? {
val refreshed =
refreshPluginSurfaceUrl(
method = "node.pluginSurface.refresh",
params = buildJsonObject { put("surface", JsonPrimitive("canvas")) },
timeoutMs = timeoutMs,
)
if (!refreshed.isNullOrBlank()) {
pluginSurfaceUrls = pluginSurfaceUrls + ("canvas" to refreshed)
}
return refreshed
}
fun currentMainSessionKey(): String? = mainSessionKey
@@ -218,6 +231,28 @@ class GatewaySession(
}
}
private suspend fun refreshPluginSurfaceUrl(
method: String,
params: JsonElement?,
timeoutMs: Long,
): String? {
val conn = currentConnection ?: return null
return try {
val res = conn.request(method, params, timeoutMs)
if (!res.ok) return null
val obj = res.payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() } ?: return null
val raw =
obj["pluginSurfaceUrls"]
.asObjectOrNull()
?.get("canvas")
.asStringOrNull()
normalizeCanvasHostUrl(raw, conn.endpoint, isTlsConnection = conn.tls != null)
} catch (err: Throwable) {
Log.d("OpenClawGateway", "$method failed: ${err.message ?: err::class.java.simpleName}")
null
}
}
suspend fun sendNodeEventDetailed(
event: String,
payloadJson: String?,
@@ -280,52 +315,6 @@ class GatewaySession(
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
}
suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean {
val conn = currentConnection ?: return false
val response =
try {
conn.request(
"node.canvas.capability.refresh",
params = buildJsonObject {},
timeoutMs = timeoutMs,
)
} catch (err: Throwable) {
Log.w("OpenClawGateway", "node.canvas.capability.refresh failed: ${err.message ?: err::class.java.simpleName}")
return false
}
if (!response.ok) {
val err = response.error
Log.w(
"OpenClawGateway",
"node.canvas.capability.refresh rejected: ${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}",
)
return false
}
val payloadObj = response.payloadJson?.let(::parseJsonOrNull)?.asObjectOrNull()
val refreshedCapability =
payloadObj
?.get("canvasCapability")
.asStringOrNull()
?.trim()
.orEmpty()
if (refreshedCapability.isEmpty()) {
Log.w("OpenClawGateway", "node.canvas.capability.refresh missing canvasCapability")
return false
}
val scopedCanvasHostUrl = canvasHostUrl?.trim().orEmpty()
if (scopedCanvasHostUrl.isEmpty()) {
Log.w("OpenClawGateway", "node.canvas.capability.refresh missing local canvasHostUrl")
return false
}
val refreshedUrl = replaceCanvasCapabilityInScopedHostUrl(scopedCanvasHostUrl, refreshedCapability)
if (refreshedUrl == null) {
Log.w("OpenClawGateway", "node.canvas.capability.refresh unable to rewrite scoped canvas URL")
return false
}
canvasHostUrl = refreshedUrl
return true
}
private data class RpcResponse(
val id: String,
val ok: Boolean,
@@ -334,12 +323,12 @@ class GatewaySession(
)
private inner class Connection(
private val endpoint: GatewayEndpoint,
val endpoint: GatewayEndpoint,
private val token: String?,
private val bootstrapToken: String?,
private val password: String?,
private val options: GatewayConnectOptions,
private val tls: GatewayTlsParams?,
val tls: GatewayTlsParams?,
) {
private val connectDeferred = CompletableDeferred<Unit>()
private val closedDeferred = CompletableDeferred<Unit>()
@@ -615,8 +604,13 @@ class GatewaySession(
}
}
}
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null)
val rawPluginSurfaceUrls = obj["pluginSurfaceUrls"].asObjectOrNull()
val normalizedPluginSurfaceUrls =
rawPluginSurfaceUrls?.mapNotNull { (surface, value) ->
normalizeCanvasHostUrl(value.asStringOrNull(), endpoint, isTlsConnection = tls != null)
?.let { normalized -> surface to normalized }
} ?: emptyList()
pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap()
val sessionDefaults =
obj["snapshot"]
.asObjectOrNull()
@@ -910,7 +904,7 @@ class GatewaySession(
conn.awaitClose()
} finally {
currentConnection = null
canvasHostUrl = null
pluginSurfaceUrls = emptyMap()
mainSessionKey = null
}
}
@@ -1133,22 +1127,6 @@ private fun parseJsonOrNull(payload: String): JsonElement? {
}
}
internal fun replaceCanvasCapabilityInScopedHostUrl(
scopedUrl: String,
capability: String,
): String? {
val marker = "/__openclaw__/cap/"
val markerStart = scopedUrl.indexOf(marker)
if (markerStart < 0) return null
val capabilityStart = markerStart + marker.length
val slashEnd = scopedUrl.indexOf("/", capabilityStart).takeIf { it >= 0 }
val queryEnd = scopedUrl.indexOf("?", capabilityStart).takeIf { it >= 0 }
val fragmentEnd = scopedUrl.indexOf("#", capabilityStart).takeIf { it >= 0 }
val capabilityEnd = listOfNotNull(slashEnd, queryEnd, fragmentEnd).minOrNull() ?: scopedUrl.length
if (capabilityEnd <= capabilityStart) return null
return scopedUrl.substring(0, capabilityStart) + capability + scopedUrl.substring(capabilityEnd)
}
internal fun resolveInvokeResultAckTimeoutMs(invokeTimeoutMs: Long?): Long {
val normalized = invokeTimeoutMs?.takeIf { it > 0L } ?: 15_000L
return normalized.coerceIn(15_000L, 120_000L)

View File

@@ -14,6 +14,7 @@ import ai.openclaw.app.protocol.OpenClawNotificationsCommand
import ai.openclaw.app.protocol.OpenClawPhotosCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawSystemCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
data class NodeRuntimeFlags(
val cameraEnabled: Boolean,
@@ -81,6 +82,7 @@ object InvokeCommandRegistry {
name = OpenClawCapability.VoiceWake.rawValue,
availability = NodeCapabilityAvailability.VoiceWakeEnabled,
),
NodeCapabilitySpec(name = OpenClawCapability.Talk.rawValue),
NodeCapabilitySpec(
name = OpenClawCapability.Location.rawValue,
availability = NodeCapabilityAvailability.LocationEnabled,
@@ -135,6 +137,18 @@ object InvokeCommandRegistry {
InvokeCommandSpec(
name = OpenClawSystemCommand.Notify.rawValue,
),
InvokeCommandSpec(
name = OpenClawTalkCommand.PttStart.rawValue,
),
InvokeCommandSpec(
name = OpenClawTalkCommand.PttStop.rawValue,
),
InvokeCommandSpec(
name = OpenClawTalkCommand.PttCancel.rawValue,
),
InvokeCommandSpec(
name = OpenClawTalkCommand.PttOnce.rawValue,
),
InvokeCommandSpec(
name = OpenClawCameraCommand.List.rawValue,
requiresForeground = true,

View File

@@ -13,6 +13,7 @@ import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawNotificationsCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawSystemCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
internal enum class SmsSearchAvailabilityReason {
Available,
@@ -59,6 +60,7 @@ class InvokeDispatcher(
private val deviceHandler: DeviceHandler,
private val notificationsHandler: NotificationsHandler,
private val systemHandler: SystemHandler,
private val talkHandler: TalkHandler,
private val photosHandler: PhotosHandler,
private val contactsHandler: ContactsHandler,
private val calendarHandler: CalendarHandler,
@@ -76,9 +78,9 @@ class InvokeDispatcher(
private val smsTelephonyAvailable: () -> Boolean,
private val callLogAvailable: () -> Boolean,
private val debugBuild: () -> Boolean,
private val refreshNodeCanvasCapability: suspend () -> Boolean,
private val onCanvasA2uiPush: () -> Unit,
private val onCanvasA2uiReset: () -> Unit,
private val refreshCanvasHostUrl: suspend () -> String?,
private val motionActivityAvailable: () -> Boolean,
private val motionPedometerAvailable: () -> Boolean,
) {
@@ -188,6 +190,12 @@ class InvokeDispatcher(
// System command
OpenClawSystemCommand.Notify.rawValue -> systemHandler.handleSystemNotify(paramsJson)
// Talk commands
OpenClawTalkCommand.PttStart.rawValue -> talkHandler.handlePttStart(paramsJson)
OpenClawTalkCommand.PttStop.rawValue -> talkHandler.handlePttStop(paramsJson)
OpenClawTalkCommand.PttCancel.rawValue -> talkHandler.handlePttCancel(paramsJson)
OpenClawTalkCommand.PttOnce.rawValue -> talkHandler.handlePttOnce(paramsJson)
// Photos command
ai.openclaw.app.protocol.OpenClawPhotosCommand.Latest.rawValue ->
photosHandler.handlePhotosLatest(
@@ -223,23 +231,15 @@ class InvokeDispatcher(
private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult {
var a2uiUrl =
a2uiHandler.resolveA2uiHostUrl()
?: refreshCanvasHostUrl().let { a2uiHandler.resolveA2uiHostUrl() }
?: return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
)
val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl)
if (!readyOnFirstCheck) {
if (!refreshNodeCanvasCapability()) {
return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_UNAVAILABLE",
message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
)
}
a2uiUrl = a2uiHandler.resolveA2uiHostUrl()
?: return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
)
refreshCanvasHostUrl()
a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: a2uiUrl
if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) {
return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_UNAVAILABLE",
@@ -336,3 +336,13 @@ class InvokeDispatcher(
}
}
}
interface TalkHandler {
suspend fun handlePttStart(paramsJson: String?): GatewaySession.InvokeResult
suspend fun handlePttStop(paramsJson: String?): GatewaySession.InvokeResult
suspend fun handlePttCancel(paramsJson: String?): GatewaySession.InvokeResult
suspend fun handlePttOnce(paramsJson: String?): GatewaySession.InvokeResult
}

View File

@@ -7,6 +7,7 @@ enum class OpenClawCapability(
Camera("camera"),
Sms("sms"),
VoiceWake("voiceWake"),
Talk("talk"),
Location("location"),
Device("device"),
Notifications("notifications"),
@@ -71,6 +72,20 @@ enum class OpenClawSmsCommand(
}
}
enum class OpenClawTalkCommand(
val rawValue: String,
) {
PttStart("talk.ptt.start"),
PttStop("talk.ptt.stop"),
PttCancel("talk.ptt.cancel"),
PttOnce("talk.ptt.once"),
;
companion object {
const val NamespacePrefix: String = "talk."
}
}
enum class OpenClawLocationCommand(
val rawValue: String,
) {

View File

@@ -0,0 +1,45 @@
package ai.openclaw.app.voice
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
internal object ChatEventText {
fun assistantTextFromPayload(payload: JsonObject): String? = assistantTextFromMessage(payload["message"])
fun assistantTextFromMessage(messageEl: JsonElement?): String? {
val message = messageEl.asObjectOrNull() ?: return null
val role = message["role"].asStringOrNull()
if (role != null && role != "assistant") return null
return textFromContent(message["content"])
}
private fun textFromContent(content: JsonElement?): String? =
when (content) {
is JsonPrimitive -> content.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
is JsonArray ->
content
.mapNotNull(::textFromContentPart)
.filter { it.isNotEmpty() }
.joinToString("\n")
.takeIf { it.isNotBlank() }
else -> null
}
private fun textFromContentPart(part: JsonElement): String? {
part
.asStringOrNull()
?.trim()
?.takeIf { it.isNotEmpty() }
?.let { return it }
val obj = part.asObjectOrNull() ?: return null
val type = obj["type"].asStringOrNull()
if (type != null && type != "text") return null
return obj["text"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
}
}
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
private fun JsonElement?.asStringOrNull(): String? = (this as? JsonPrimitive)?.takeIf { it.isString }?.content

View File

@@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import java.util.UUID
@@ -596,20 +595,7 @@ class MicCaptureManager(
PackageManager.PERMISSION_GRANTED
)
private fun parseAssistantText(payload: JsonObject): String? {
val message = payload["message"].asObjectOrNull() ?: return null
if (message["role"].asStringOrNull() != "assistant") return null
val content = message["content"] as? JsonArray ?: return null
val parts =
content.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
if (obj["type"].asStringOrNull() != "text") return@mapNotNull null
obj["text"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
}
if (parts.isEmpty()) return null
return parts.joinToString("\n")
}
private fun parseAssistantText(payload: JsonObject): String? = ChatEventText.assistantTextFromPayload(payload)
private val listener =
object : RecognitionListener {

View File

@@ -12,20 +12,26 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
internal interface TalkAudioPlaying {
suspend fun play(audio: TalkSpeakAudio)
fun stop()
}
internal class TalkAudioPlayer(
private val context: Context,
) {
) : TalkAudioPlaying {
private val lock = Any()
private var active: ActivePlayback? = null
suspend fun play(audio: TalkSpeakAudio) {
override suspend fun play(audio: TalkSpeakAudio) {
when (val mode = resolvePlaybackMode(audio)) {
is TalkPlaybackMode.Pcm -> playPcm(audio.bytes, mode.sampleRate)
is TalkPlaybackMode.Compressed -> playCompressed(audio.bytes, mode.fileExtension)
}
}
fun stop() {
override fun stop() {
synchronized(lock) {
active?.cancel()
active = null

View File

@@ -41,7 +41,28 @@ import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.coroutineContext
class TalkModeManager(
data class TalkPttStartPayload(
val captureId: String,
) {
fun toJson(): String = """{"captureId":"$captureId"}"""
}
data class TalkPttStopPayload(
val captureId: String,
val transcript: String?,
val status: String,
) {
fun toJson(): String =
buildJsonObject {
put("captureId", JsonPrimitive(captureId))
if (transcript != null) {
put("transcript", JsonPrimitive(transcript))
}
put("status", JsonPrimitive(status))
}.toString()
}
class TalkModeManager internal constructor(
private val context: Context,
private val scope: CoroutineScope,
private val session: GatewaySession,
@@ -49,6 +70,8 @@ class TalkModeManager(
private val isConnected: () -> Boolean,
private val onBeforeSpeak: suspend () -> Unit = {},
private val onAfterSpeak: suspend () -> Unit = {},
private val talkSpeakClient: TalkSpeechSynthesizing = TalkSpeakClient(session = session),
private val talkAudioPlayer: TalkAudioPlaying = TalkAudioPlayer(context),
) {
companion object {
private const val tag = "TalkMode"
@@ -60,9 +83,6 @@ class TalkModeManager(
private val mainHandler = Handler(Looper.getMainLooper())
private val json = Json { ignoreUnknownKeys = true }
private val talkSpeakClient = TalkSpeakClient(session = session, json = json)
private val talkAudioPlayer = TalkAudioPlayer(context)
private val _isEnabled = MutableStateFlow(false)
val isEnabled: StateFlow<Boolean> = _isEnabled
@@ -82,6 +102,10 @@ class TalkModeManager(
private var restartJob: Job? = null
private var stopRequested = false
private var listeningMode = false
private var activePttCaptureId: String? = null
private var pttAutoStopEnabled = false
private var pttTimeoutJob: Job? = null
private var pttCompletion: CompletableDeferred<TalkPttStopPayload>? = null
private var silenceJob: Job? = null
private var silenceWindowMs = TalkDefaults.defaultSilenceTimeoutMs
@@ -156,6 +180,127 @@ class TalkModeManager(
}
}
suspend fun beginPushToTalk(): TalkPttStartPayload {
if (!isConnected()) {
_statusText.value = "Gateway not connected"
throw IllegalStateException("UNAVAILABLE: Gateway not connected")
}
activePttCaptureId?.let { return TalkPttStartPayload(captureId = it) }
stopSpeaking(resetInterrupt = false)
pttTimeoutJob?.cancel()
pttTimeoutJob = null
pttAutoStopEnabled = false
pttCompletion = null
silenceJob?.cancel()
silenceJob = null
listeningMode = false
finalizeInFlight = false
stopRequested = false
lastTranscript = ""
lastHeardAtMs = null
val micOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
if (!micOk) {
_statusText.value = "Microphone permission required"
throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
}
if (!SpeechRecognizer.isRecognitionAvailable(context)) {
_statusText.value = "Speech recognizer unavailable"
throw IllegalStateException("UNAVAILABLE: Speech recognizer unavailable")
}
val captureId = UUID.randomUUID().toString()
activePttCaptureId = captureId
withContext(Dispatchers.Main) {
recognizer?.cancel()
recognizer?.destroy()
recognizer = SpeechRecognizer.createSpeechRecognizer(context).also { it.setRecognitionListener(listener) }
startListeningInternal(markListening = true)
}
_statusText.value = "Listening (PTT)"
return TalkPttStartPayload(captureId = captureId)
}
suspend fun endPushToTalk(): TalkPttStopPayload {
val captureId = activePttCaptureId ?: UUID.randomUUID().toString()
if (activePttCaptureId == null) {
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = null, status = "idle"))
}
clearPushToTalkRecognition()
val transcript = lastTranscript.trim()
lastTranscript = ""
lastHeardAtMs = null
if (transcript.isEmpty()) {
_statusText.value = if (_isEnabled.value) "Listening" else "Ready"
if (_isEnabled.value) {
start()
}
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = null, status = "empty"))
}
if (!isConnected()) {
_statusText.value = "Gateway not connected"
if (_isEnabled.value) {
start()
}
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = transcript, status = "offline"))
}
_statusText.value = "Thinking…"
scope.launch {
finalizeTranscript(transcript)
}
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = transcript, status = "queued"))
}
suspend fun cancelPushToTalk(): TalkPttStopPayload {
val captureId = activePttCaptureId ?: UUID.randomUUID().toString()
if (activePttCaptureId == null) {
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = null, status = "idle"))
}
clearPushToTalkRecognition()
lastTranscript = ""
lastHeardAtMs = null
_statusText.value = if (_isEnabled.value) "Listening" else "Ready"
if (_isEnabled.value) {
start()
}
return finishPushToTalk(TalkPttStopPayload(captureId = captureId, transcript = null, status = "cancelled"))
}
suspend fun runPushToTalkOnce(maxDurationMs: Long = 12_000L): TalkPttStopPayload {
if (pttCompletion != null) {
cancelPushToTalk()
}
if (activePttCaptureId != null) {
return TalkPttStopPayload(
captureId = activePttCaptureId ?: UUID.randomUUID().toString(),
transcript = null,
status = "busy",
)
}
beginPushToTalk()
val completion = CompletableDeferred<TalkPttStopPayload>()
pttCompletion = completion
pttAutoStopEnabled = true
startSilenceMonitor()
pttTimeoutJob =
scope.launch {
delay(maxDurationMs)
if (pttAutoStopEnabled && activePttCaptureId != null) {
endPushToTalk()
}
}
return completion.await()
}
/**
* Speak a wake-word command through TalkMode's full pipeline:
* chat.send → wait for final → read assistant text → TTS.
@@ -335,6 +480,12 @@ class TalkModeManager(
stopRequested = true
finalizeInFlight = false
listeningMode = false
activePttCaptureId = null
pttAutoStopEnabled = false
pttCompletion?.cancel()
pttCompletion = null
pttTimeoutJob?.cancel()
pttTimeoutJob = null
restartJob?.cancel()
restartJob = null
silenceJob?.cancel()
@@ -434,7 +585,7 @@ class TalkModeManager(
silenceJob?.cancel()
silenceJob =
scope.launch {
while (_isEnabled.value) {
while (_isEnabled.value || pttAutoStopEnabled) {
delay(200)
checkSilence()
}
@@ -448,6 +599,12 @@ class TalkModeManager(
val lastHeard = lastHeardAtMs ?: return
val elapsed = SystemClock.elapsedRealtime() - lastHeard
if (elapsed < silenceWindowMs) return
if (activePttCaptureId != null) {
if (pttAutoStopEnabled) {
scope.launch { endPushToTalk() }
}
return
}
if (finalizeInFlight) return
finalizeInFlight = true
scope.launch {
@@ -525,6 +682,27 @@ class TalkModeManager(
}
}
private suspend fun clearPushToTalkRecognition() {
pttTimeoutJob?.cancel()
pttTimeoutJob = null
pttAutoStopEnabled = false
activePttCaptureId = null
_isListening.value = false
listeningMode = false
clearListenWatchdog()
withContext(Dispatchers.Main) {
recognizer?.cancel()
recognizer?.destroy()
recognizer = null
}
}
private fun finishPushToTalk(payload: TalkPttStopPayload): TalkPttStopPayload {
pttCompletion?.complete(payload)
pttCompletion = null
return payload
}
private suspend fun subscribeChatIfNeeded(
session: GatewaySession,
sessionKey: String,
@@ -656,20 +834,7 @@ class TalkModeManager(
}
}
private fun extractTextFromChatEventMessage(messageEl: JsonElement?): String? {
val msg = messageEl?.asObjectOrNull() ?: return null
val content = msg["content"] as? JsonArray ?: return null
return content
.mapNotNull { entry ->
entry
.asObjectOrNull()
?.get("text")
?.asStringOrNull()
?.trim()
}.filter { it.isNotEmpty() }
.joinToString("\n")
.takeIf { it.isNotBlank() }
}
private fun extractTextFromChatEventMessage(messageEl: JsonElement?): String? = ChatEventText.assistantTextFromMessage(messageEl)
private suspend fun waitForAssistantText(
session: GatewaySession,
@@ -729,17 +894,16 @@ class TalkModeManager(
_lastAssistantText.value = cleaned
ensurePlaybackActive(playbackToken)
_statusText.value = "Speaking"
_isSpeaking.value = true
_statusText.value = "Generating voice"
_isSpeaking.value = false
lastSpokenText = cleaned
ensureInterruptListener()
requestAudioFocusForTts()
try {
val started = SystemClock.elapsedRealtime()
when (val result = talkSpeakClient.synthesize(text = cleaned, directive = directive)) {
is TalkSpeakResult.Success -> {
ensurePlaybackActive(playbackToken)
markAudioPlaybackStarting(playbackToken)
talkAudioPlayer.play(result.audio)
ensurePlaybackActive(playbackToken)
Log.d(tag, "talk.speak ok durMs=${SystemClock.elapsedRealtime() - started}")
@@ -789,8 +953,6 @@ class TalkModeManager(
shouldResumeAfterSpeak = true
onBeforeSpeak()
ensurePlaybackActive(playbackToken)
_isSpeaking.value = true
_statusText.value = "Speaking…"
block()
} finally {
synchronized(ttsJobLock) {
@@ -888,6 +1050,7 @@ class TalkModeManager(
}
},
)
markAudioPlaybackStarting(playbackToken)
val result = engine.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
if (result != TextToSpeech.SUCCESS) {
throw IllegalStateException("TextToSpeech start failed")
@@ -905,6 +1068,14 @@ class TalkModeManager(
}
}
private fun markAudioPlaybackStarting(playbackToken: Long) {
ensurePlaybackActive(playbackToken)
_statusText.value = "Speaking…"
_isSpeaking.value = true
ensureInterruptListener()
requestAudioFocusForTts()
}
fun stopTts() {
stopSpeaking(resetInterrupt = true)
_isSpeaking.value = false

View File

@@ -28,12 +28,19 @@ internal sealed interface TalkSpeakResult {
) : TalkSpeakResult
}
internal interface TalkSpeechSynthesizing {
suspend fun synthesize(
text: String,
directive: TalkDirective?,
): TalkSpeakResult
}
internal class TalkSpeakClient(
private val session: GatewaySession? = null,
private val json: Json = Json { ignoreUnknownKeys = true },
private val requestDetailed: (suspend (String, String, Long) -> GatewaySession.RpcResult)? = null,
) {
suspend fun synthesize(
) : TalkSpeechSynthesizing {
override suspend fun synthesize(
text: String,
directive: TalkDirective?,
): TalkSpeakResult {

View File

@@ -6,6 +6,11 @@ import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
import ai.openclaw.app.node.InvokeDispatcher
import ai.openclaw.app.protocol.OpenClawTalkCommand
import ai.openclaw.app.voice.TalkModeManager
import android.Manifest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -15,6 +20,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import java.lang.reflect.Field
import java.util.UUID
@@ -221,6 +227,23 @@ class GatewayBootstrapAuthTest {
assertNull(authStore.loadToken(deviceId, "operator"))
}
@Test
fun talkPttStart_cleansPreparedCaptureWhenBeginFails() =
runBlocking {
val app = RuntimeEnvironment.getApplication()
shadowOf(app).grantPermissions(Manifest.permission.RECORD_AUDIO)
val runtime = NodeRuntime(app)
val dispatcher = readField<InvokeDispatcher>(runtime, "invokeDispatcher")
val result = dispatcher.handleInvoke(OpenClawTalkCommand.PttStart.rawValue, null)
assertEquals("UNAVAILABLE", result.error?.code)
assertEquals(VoiceCaptureMode.Off, runtime.voiceCaptureMode.value)
assertFalse(readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value)
val talkMode = readField<Lazy<TalkModeManager>>(runtime, "talkMode\$delegate").value
assertFalse(talkMode.ttsOnAllResponses)
}
private fun waitForGatewayTrustPrompt(runtime: NodeRuntime): NodeRuntime.GatewayTrustPrompt {
repeat(50) {
runtime.pendingGatewayTrust.value?.let { return it }

View File

@@ -476,56 +476,6 @@ class GatewaySessionInvokeTest {
)
}
@Test
fun refreshNodeCanvasCapability_sendsObjectParamsAndUpdatesScopedUrl() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val refreshRequestParams = CompletableDeferred<String?>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, frame ->
when (method) {
"connect" -> {
webSocket.send(connectResponseFrame(id, canvasHostUrl = "http://127.0.0.1/__openclaw__/cap/old-cap"))
}
"node.canvas.capability.refresh" -> {
if (!refreshRequestParams.isCompleted) {
refreshRequestParams.complete(frame["params"]?.toString())
}
webSocket.send(
"""{"type":"res","id":"$id","ok":true,"payload":{"canvasCapability":"new-cap"}}""",
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(harness.session, server.port)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val refreshed = harness.session.refreshNodeCanvasCapability(timeoutMs = TEST_TIMEOUT_MS)
val refreshParamsJson = withTimeout(TEST_TIMEOUT_MS) { refreshRequestParams.await() }
assertEquals(true, refreshed)
assertEquals("{}", refreshParamsJson)
assertEquals(
"http://127.0.0.1:${server.port}/__openclaw__/cap/new-cap",
harness.session.currentCanvasHostUrl(),
)
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun sendNodeEventDetailed_sendsPresenceAlivePayloadAndReturnsStructuredResponse() =
runBlocking {
@@ -778,12 +728,17 @@ class GatewaySessionInvokeTest {
private fun connectResponseFrame(
id: String,
canvasHostUrl: String? = null,
pluginSurfaceUrls: Map<String, String> = emptyMap(),
authJson: String? = null,
): String {
val canvas = canvasHostUrl?.let { "\"canvasHostUrl\":\"$it\"," } ?: ""
val surfaces =
pluginSurfaceUrls.entries
.joinToString(",") { (key, value) -> """"$key":"$value"""" }
.takeIf { it.isNotEmpty() }
?.let { """"pluginSurfaceUrls":{$it},""" }
?: ""
val auth = authJson?.let { "\"auth\":$it," } ?: ""
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
return """{"type":"res","id":"$id","ok":true,"payload":{$surfaces$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
}
private fun startGatewayServer(

View File

@@ -39,26 +39,4 @@ class GatewaySessionInvokeTimeoutTest {
assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(121_000L))
assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(Long.MAX_VALUE))
}
@Test
fun replaceCanvasCapabilityInScopedHostUrl_rewritesTerminalCapabilitySegment() {
assertEquals(
"http://127.0.0.1:18789/__openclaw__/cap/new-token",
replaceCanvasCapabilityInScopedHostUrl(
"http://127.0.0.1:18789/__openclaw__/cap/old-token",
"new-token",
),
)
}
@Test
fun replaceCanvasCapabilityInScopedHostUrl_rewritesWhenQueryAndFragmentPresent() {
assertEquals(
"http://127.0.0.1:18789/__openclaw__/cap/new-token?a=1#frag",
replaceCanvasCapabilityInScopedHostUrl(
"http://127.0.0.1:18789/__openclaw__/cap/old-token?a=1#frag",
"new-token",
),
)
}
}

View File

@@ -12,6 +12,7 @@ import ai.openclaw.app.protocol.OpenClawNotificationsCommand
import ai.openclaw.app.protocol.OpenClawPhotosCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawSystemCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -26,6 +27,7 @@ class InvokeCommandRegistryTest {
OpenClawCapability.Device.rawValue,
OpenClawCapability.Notifications.rawValue,
OpenClawCapability.System.rawValue,
OpenClawCapability.Talk.rawValue,
OpenClawCapability.Photos.rawValue,
OpenClawCapability.Contacts.rawValue,
OpenClawCapability.Calendar.rawValue,
@@ -50,6 +52,10 @@ class InvokeCommandRegistryTest {
OpenClawNotificationsCommand.List.rawValue,
OpenClawNotificationsCommand.Actions.rawValue,
OpenClawSystemCommand.Notify.rawValue,
OpenClawTalkCommand.PttStart.rawValue,
OpenClawTalkCommand.PttStop.rawValue,
OpenClawTalkCommand.PttCancel.rawValue,
OpenClawTalkCommand.PttOnce.rawValue,
OpenClawPhotosCommand.Latest.rawValue,
OpenClawContactsCommand.Search.rawValue,
OpenClawContactsCommand.Add.rawValue,

View File

@@ -1,11 +1,13 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
import android.content.Context
import android.content.pm.PackageManager
import kotlinx.coroutines.flow.MutableStateFlow
@@ -208,6 +210,27 @@ class InvokeDispatcherTest {
assertEquals("INVALID_REQUEST: unknown command", result.error?.message)
}
@Test
fun handleInvoke_routesTalkPttCommands() =
runTest {
val talk = InvokeDispatcherFakeTalkHandler()
val dispatcher = newDispatcher(talkHandler = talk)
val start = dispatcher.handleInvoke(OpenClawTalkCommand.PttStart.rawValue, null)
val stop = dispatcher.handleInvoke(OpenClawTalkCommand.PttStop.rawValue, null)
val cancel = dispatcher.handleInvoke(OpenClawTalkCommand.PttCancel.rawValue, null)
val once = dispatcher.handleInvoke(OpenClawTalkCommand.PttOnce.rawValue, null)
assertEquals("""{"captureId":"start"}""", start.payloadJson)
assertEquals("""{"status":"stop"}""", stop.payloadJson)
assertEquals("""{"status":"cancel"}""", cancel.payloadJson)
assertEquals("""{"status":"once"}""", once.payloadJson)
assertEquals(
listOf("start", "stop", "cancel", "once"),
talk.calls,
)
}
private fun newDispatcher(
cameraEnabled: Boolean = false,
locationEnabled: Boolean = false,
@@ -219,6 +242,7 @@ class InvokeDispatcherTest {
debugBuild: Boolean = false,
motionActivityAvailable: Boolean = false,
motionPedometerAvailable: Boolean = false,
talkHandler: TalkHandler = InvokeDispatcherFakeTalkHandler(),
): InvokeDispatcher {
val appContext = RuntimeEnvironment.getApplication()
shadowOf(appContext.packageManager).setSystemFeature(PackageManager.FEATURE_TELEPHONY, smsTelephonyAvailable)
@@ -238,6 +262,7 @@ class InvokeDispatcherTest {
stateProvider = InvokeDispatcherFakeNotificationsStateProvider(),
),
systemHandler = SystemHandler.forTesting(InvokeDispatcherFakeSystemNotificationPoster()),
talkHandler = talkHandler,
photosHandler = PhotosHandler.forTesting(appContext, InvokeDispatcherFakePhotosDataSource()),
contactsHandler = ContactsHandler.forTesting(appContext, InvokeDispatcherFakeContactsDataSource()),
calendarHandler = CalendarHandler.forTesting(appContext, InvokeDispatcherFakeCalendarDataSource()),
@@ -261,9 +286,9 @@ class InvokeDispatcherTest {
smsTelephonyAvailable = { smsTelephonyAvailable },
callLogAvailable = { callLogAvailable },
debugBuild = { debugBuild },
refreshNodeCanvasCapability = { false },
onCanvasA2uiPush = {},
onCanvasA2uiReset = {},
refreshCanvasHostUrl = { null },
motionActivityAvailable = { motionActivityAvailable },
motionPedometerAvailable = { motionPedometerAvailable },
)
@@ -312,6 +337,30 @@ private class InvokeDispatcherFakeSystemNotificationPoster : SystemNotificationP
override fun post(request: SystemNotifyRequest) = Unit
}
private class InvokeDispatcherFakeTalkHandler : TalkHandler {
val calls = mutableListOf<String>()
override suspend fun handlePttStart(paramsJson: String?): GatewaySession.InvokeResult {
calls.add("start")
return GatewaySession.InvokeResult.ok("""{"captureId":"start"}""")
}
override suspend fun handlePttStop(paramsJson: String?): GatewaySession.InvokeResult {
calls.add("stop")
return GatewaySession.InvokeResult.ok("""{"status":"stop"}""")
}
override suspend fun handlePttCancel(paramsJson: String?): GatewaySession.InvokeResult {
calls.add("cancel")
return GatewaySession.InvokeResult.ok("""{"status":"cancel"}""")
}
override suspend fun handlePttOnce(paramsJson: String?): GatewaySession.InvokeResult {
calls.add("once")
return GatewaySession.InvokeResult.ok("""{"status":"once"}""")
}
}
private class InvokeDispatcherFakePhotosDataSource : PhotosDataSource {
override fun hasPermission(context: Context): Boolean = true

View File

@@ -25,6 +25,7 @@ class OpenClawProtocolConstantsTest {
assertEquals("canvas", OpenClawCapability.Canvas.rawValue)
assertEquals("camera", OpenClawCapability.Camera.rawValue)
assertEquals("voiceWake", OpenClawCapability.VoiceWake.rawValue)
assertEquals("talk", OpenClawCapability.Talk.rawValue)
assertEquals("location", OpenClawCapability.Location.rawValue)
assertEquals("sms", OpenClawCapability.Sms.rawValue)
assertEquals("device", OpenClawCapability.Device.rawValue)
@@ -92,6 +93,14 @@ class OpenClawProtocolConstantsTest {
assertEquals("sms.search", OpenClawSmsCommand.Search.rawValue)
}
@Test
fun talkCommandsUseStableStrings() {
assertEquals("talk.ptt.start", OpenClawTalkCommand.PttStart.rawValue)
assertEquals("talk.ptt.stop", OpenClawTalkCommand.PttStop.rawValue)
assertEquals("talk.ptt.cancel", OpenClawTalkCommand.PttCancel.rawValue)
assertEquals("talk.ptt.once", OpenClawTalkCommand.PttOnce.rawValue)
}
@Test
fun callLogCommandsUseStableStrings() {
assertEquals("callLog.search", OpenClawCallLogCommand.Search.rawValue)

View File

@@ -0,0 +1,69 @@
package ai.openclaw.app.voice
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class ChatEventTextTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun extractsAssistantTextParts() {
val payload =
payload(
"""
{
"message": {
"role": "assistant",
"content": [
{ "type": "text", "text": "hello" },
{ "type": "text", "text": "world" }
]
}
}
""",
)
assertEquals("hello\nworld", ChatEventText.assistantTextFromPayload(payload))
}
@Test
fun extractsPlainStringContent() {
val payload =
payload(
"""
{
"message": {
"role": "assistant",
"content": "plain reply"
}
}
""",
)
assertEquals("plain reply", ChatEventText.assistantTextFromPayload(payload))
}
@Test
fun ignoresUserMessages() {
val payload =
payload(
"""
{
"message": {
"role": "user",
"content": [
{ "type": "text", "text": "do not speak" }
]
}
}
""",
)
assertNull(ChatEventText.assistantTextFromPayload(payload))
}
private fun payload(source: String): JsonObject = json.parseToJsonElement(source.trimIndent()) as JsonObject
}

View File

@@ -9,7 +9,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -78,7 +81,54 @@ class TalkModeManagerTest {
assertEquals(1L, playbackGeneration(manager).get())
}
private fun createManager(): TalkModeManager {
@Test
fun nonPendingUserFinalDoesNotUseAllResponseTts() {
val manager = createManager()
manager.ttsOnAllResponses = true
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-user", text = "do not speak", role = "user"))
assertEquals(0L, playbackGeneration(manager).get())
}
@Test
fun textReadyDoesNotEnterSpeakingUntilAudioPlaybackStarts() =
runTest {
val talkSpeakClient = FakeTalkSpeechSynthesizer()
val talkAudioPlayer = FakeTalkAudioPlayer()
val manager = createManager(talkSpeakClient = talkSpeakClient, talkAudioPlayer = talkAudioPlayer)
val job = launch { manager.speakAssistantReply("hello") }
talkSpeakClient.requested.await()
assertEquals("Generating voice…", manager.statusText.value)
assertFalse(manager.isSpeaking.value)
talkSpeakClient.result.complete(
TalkSpeakResult.Success(
TalkSpeakAudio(
bytes = byteArrayOf(1, 2, 3),
provider = "test",
outputFormat = "mp3_44100_128",
voiceCompatible = true,
mimeType = "audio/mpeg",
fileExtension = ".mp3",
),
),
)
talkAudioPlayer.started.await()
assertEquals("Speaking…", manager.statusText.value)
assertTrue(manager.isSpeaking.value)
talkAudioPlayer.finished.complete(Unit)
job.join()
}
private fun createManager(
talkSpeakClient: TalkSpeechSynthesizing = TalkSpeakClient(),
talkAudioPlayer: TalkAudioPlaying? = null,
): TalkModeManager {
val app = RuntimeEnvironment.getApplication()
val sessionJob = SupervisorJob()
val session =
@@ -96,6 +146,8 @@ class TalkModeManagerTest {
session = session,
supportsChatSubscribe = false,
isConnected = { true },
talkSpeakClient = talkSpeakClient,
talkAudioPlayer = talkAudioPlayer ?: TalkAudioPlayer(app),
)
}
@@ -124,6 +176,7 @@ class TalkModeManagerTest {
private fun chatFinalPayload(
runId: String,
text: String,
role: String = "assistant",
): String =
"""
{
@@ -131,7 +184,7 @@ class TalkModeManagerTest {
"sessionKey": "main",
"state": "final",
"message": {
"role": "assistant",
"role": "$role",
"content": [
{ "type": "text", "text": "$text" }
]
@@ -140,6 +193,34 @@ class TalkModeManagerTest {
""".trimIndent()
}
private class FakeTalkSpeechSynthesizer : TalkSpeechSynthesizing {
val requested = CompletableDeferred<Unit>()
val result = CompletableDeferred<TalkSpeakResult>()
override suspend fun synthesize(
text: String,
directive: TalkDirective?,
): TalkSpeakResult {
requested.complete(Unit)
return result.await()
}
}
private class FakeTalkAudioPlayer : TalkAudioPlaying {
val started = CompletableDeferred<Unit>()
val finished = CompletableDeferred<Unit>()
var stopped = false
override suspend fun play(audio: TalkSpeakAudio) {
started.complete(Unit)
finished.await()
}
override fun stop() {
stopped = true
}
}
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
override fun loadEntry(
deviceId: String,

View File

@@ -1,5 +1,9 @@
# OpenClaw iOS Changelog
## 2026.5.6 - 2026-05-06
Maintenance update for the current OpenClaw development release.
## 2026.5.5 - 2026-05-05
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.5.5
OPENCLAW_MARKETING_VERSION = 2026.5.5
OPENCLAW_IOS_VERSION = 2026.5.6
OPENCLAW_MARKETING_VERSION = 2026.5.6
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -689,7 +689,7 @@ final class GatewayConnectionController {
}
private func shouldRequireTLS(host: String) -> Bool {
!Self.isLoopbackHost(host)
!LoopbackHost.isLocalNetworkHost(host)
}
private func shouldForceTLS(host: String) -> Bool {
@@ -698,51 +698,6 @@ final class GatewayConnectionController {
return trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.")
}
private static func isLoopbackHost(_ rawHost: String) -> Bool {
var host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !host.isEmpty else { return false }
if host.hasPrefix("[") && host.hasSuffix("]") {
host.removeFirst()
host.removeLast()
}
if host.hasSuffix(".") {
host.removeLast()
}
if let zoneIndex = host.firstIndex(of: "%") {
host = String(host[..<zoneIndex])
}
if host.isEmpty { return false }
if host == "localhost" || host == "0.0.0.0" || host == "::" {
return true
}
return Self.isLoopbackIPv4(host) || Self.isLoopbackIPv6(host)
}
private static func isLoopbackIPv4(_ host: String) -> Bool {
var addr = in_addr()
let parsed = host.withCString { inet_pton(AF_INET, $0, &addr) == 1 }
guard parsed else { return false }
let value = UInt32(bigEndian: addr.s_addr)
let firstOctet = UInt8((value >> 24) & 0xFF)
return firstOctet == 127
}
private static func isLoopbackIPv6(_ host: String) -> Bool {
var addr = in6_addr()
let parsed = host.withCString { inet_pton(AF_INET6, $0, &addr) == 1 }
guard parsed else { return false }
return withUnsafeBytes(of: &addr) { rawBytes in
let bytes = rawBytes.bindMemory(to: UInt8.self)
let isV6Loopback = bytes[0..<15].allSatisfy { $0 == 0 } && bytes[15] == 1
if isV6Loopback { return true }
let isMappedV4 = bytes[0..<10].allSatisfy { $0 == 0 } && bytes[10] == 0xFF && bytes[11] == 0xFF
return isMappedV4 && bytes[12] == 127
}
}
private func manualStableID(host: String, port: Int) -> String {
"manual|\(host.lowercased())|\(port)"
}
@@ -821,6 +776,7 @@ final class GatewayConnectionController {
if locationMode != .off { caps.append(OpenClawCapability.location.rawValue) }
caps.append(OpenClawCapability.device.rawValue)
caps.append(OpenClawCapability.talk.rawValue)
if WatchMessagingService.isSupportedOnDevice() {
caps.append(OpenClawCapability.watch.rawValue)
}

View File

@@ -63,10 +63,9 @@ extension NodeAppModel {
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready(initialUrl)
}
// First render can fail when scoped capability rotates between reconnects.
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return .hostUnavailable }
guard let refreshedUrl = await self.resolveA2UIHostURL() else { return .hostUnavailable }
guard let refreshedUrl = await self.resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: true) else {
return .hostUnavailable
}
self.screen.navigate(to: refreshedUrl, trustA2UIActions: true)
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready(refreshedUrl)
@@ -79,19 +78,19 @@ extension NodeAppModel {
self.screen.showDefaultCanvas()
}
private func resolveA2UIHostURLWithCapabilityRefresh() async -> String? {
if let url = await self.resolveA2UIHostURL() {
return url
private func resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
if !forceRefresh, let current = await self.resolveA2UIHostURL() {
return current
}
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil }
_ = await self.gatewaySession.refreshCanvasHostUrl()
return await self.resolveA2UIHostURL()
}
private func resolveCanvasHostURLWithCapabilityRefresh() async -> String? {
if let url = await self.resolveCanvasHostURL() {
return url
private func resolveCanvasHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
if !forceRefresh, let current = await self.resolveCanvasHostURL() {
return current
}
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil }
_ = await self.gatewaySession.refreshCanvasHostUrl()
return await self.resolveCanvasHostURL()
}

View File

@@ -800,11 +800,11 @@ final class TalkModeManager: NSObject {
}
}
let completion = await self.waitForChatCompletion(runId: runId, gateway: gateway, timeoutSeconds: 120)
if completion == .timeout {
if completion.state == .timeout {
self.logger.warning(
"chat completion timeout runId=\(runId, privacy: .public); attempting history fallback")
GatewayDiagnostics.log("talk: chat completion timeout runId=\(runId)")
} else if completion == .aborted {
} else if completion.state == .aborted {
self.statusText = "Aborted"
self.logger.warning("chat completion aborted runId=\(runId, privacy: .public)")
GatewayDiagnostics.log("talk: chat completion aborted runId=\(runId)")
@@ -812,7 +812,7 @@ final class TalkModeManager: NSObject {
await self.finishIncrementalSpeech()
await self.start()
return
} else if completion == .error {
} else if completion.state == .error {
self.statusText = "Chat error"
self.logger.warning("chat completion error runId=\(runId, privacy: .public)")
GatewayDiagnostics.log("talk: chat completion error runId=\(runId)")
@@ -822,16 +822,19 @@ final class TalkModeManager: NSObject {
return
}
var assistantText = try await self.waitForAssistantText(
gateway: gateway,
since: startedAt,
timeoutSeconds: completion == .final ? 12 : 25)
var assistantText = completion.assistantText
if assistantText == nil, shouldIncremental {
let fallback = self.incrementalSpeechBuffer.latestText
if !fallback.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
assistantText = fallback
}
}
if assistantText == nil {
assistantText = try await self.waitForAssistantTextFromHistory(
gateway: gateway,
since: startedAt,
timeoutSeconds: completion.state == .final ? 12 : 25)
}
guard let assistantText else {
self.statusText = "No reply"
self.logger.warning("assistant text timeout runId=\(runId, privacy: .public)")
@@ -898,6 +901,11 @@ final class TalkModeManager: NSObject {
}
}
private struct ChatCompletionResult {
var state: ChatCompletionState
var assistantText: String?
}
private func sendChat(_ message: String, gateway: GatewayNodeSession) async throws -> String {
struct SendResponse: Decodable { let runId: String }
let payload: [String: Any] = [
@@ -922,40 +930,51 @@ final class TalkModeManager: NSObject {
private func waitForChatCompletion(
runId: String,
gateway: GatewayNodeSession,
timeoutSeconds: Int = 120) async -> ChatCompletionState
timeoutSeconds: Int = 120) async -> ChatCompletionResult
{
let stream = await gateway.subscribeServerEvents(bufferingNewest: 200)
return await withTaskGroup(of: ChatCompletionState.self) { group in
return await withTaskGroup(of: ChatCompletionResult.self) { group in
group.addTask { [runId] in
var latestAssistantText: String?
for await evt in stream {
if Task.isCancelled { return .timeout }
if Task.isCancelled {
return ChatCompletionResult(state: .timeout, assistantText: latestAssistantText)
}
guard evt.event == "chat", let payload = evt.payload else { continue }
guard let chatEvent = try? GatewayPayloadDecoding.decode(payload, as: ChatEvent.self) else {
guard let chatEvent = try? GatewayPayloadDecoding.decode(
payload,
as: OpenClawChatEventPayload.self)
else {
continue
}
guard chatEvent.runid == runId else { continue }
if let state = chatEvent.state.value as? String {
switch state {
case "final": return .final
case "aborted": return .aborted
case "error": return .error
default: break
}
guard chatEvent.runId == runId else { continue }
if let text = OpenClawChatEventText.assistantText(from: chatEvent) {
latestAssistantText = text
}
switch chatEvent.state {
case "final":
return ChatCompletionResult(state: .final, assistantText: latestAssistantText)
case "aborted":
return ChatCompletionResult(state: .aborted, assistantText: nil)
case "error":
return ChatCompletionResult(state: .error, assistantText: nil)
default:
break
}
}
return .timeout
return ChatCompletionResult(state: .timeout, assistantText: latestAssistantText)
}
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds) * 1_000_000_000)
return .timeout
return ChatCompletionResult(state: .timeout, assistantText: nil)
}
let result = await group.next() ?? .timeout
let result = await group.next() ?? ChatCompletionResult(state: .timeout, assistantText: nil)
group.cancelAll()
return result
}
}
private func waitForAssistantText(
private func waitForAssistantTextFromHistory(
gateway: GatewayNodeSession,
since: Double,
timeoutSeconds: Int) async throws -> String?

View File

@@ -101,6 +101,20 @@ private func agentAction(
#expect(DeepLinkParser.parse(url) == nil)
}
@Test func parseGatewayLinkAllowsPrivateLanWs() {
let url = URL(
string: "openclaw://gateway?host=openclaw.local&port=18789&tls=0&token=abc")!
#expect(
DeepLinkParser.parse(url) == .gateway(
.init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: nil,
token: "abc",
password: nil)))
}
@Test func parseGatewayLinkRejectsInsecurePrefixBypassHost() {
let url = URL(
string: "openclaw://gateway?host=127.attacker.example&port=18789&tls=0&token=abc")!
@@ -162,6 +176,25 @@ private func agentAction(
password: nil))
}
@Test func parseGatewaySetupCodeAllowsPrivateLanWs() {
let payload = #"{"url":"ws://openclaw.local:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func parseGatewaySetupCodeRejectsTailnetPlaintextWs() {
let payload = #"{"url":"ws://gateway.tailnet.ts.net:18789","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload))
#expect(link == nil)
}
@Test func parseGatewaySetupInputParsesFullCopiedSetupMessage() {
let payload = #"{"url":"wss://gateway.example.com","bootstrapToken":"tok"}"#
let link = GatewayConnectDeepLink.fromSetupInput("""

View File

@@ -36,6 +36,7 @@ import UIKit
#expect(caps.contains(OpenClawCapability.camera.rawValue))
#expect(caps.contains(OpenClawCapability.location.rawValue))
#expect(caps.contains(OpenClawCapability.voiceWake.rawValue))
#expect(caps.contains(OpenClawCapability.talk.rawValue))
}
}

View File

@@ -107,8 +107,9 @@ import Testing
let controller = makeController()
#expect(controller._test_resolveManualUseTLS(host: "gateway.example.com", useTLS: false) == true)
#expect(controller._test_resolveManualUseTLS(host: "openclaw.local", useTLS: false) == true)
#expect(controller._test_resolveManualUseTLS(host: "127.attacker.example", useTLS: false) == true)
#expect(controller._test_resolveManualUseTLS(host: "gateway.ts.net", useTLS: false) == true)
#expect(controller._test_resolveManualUseTLS(host: "100.64.0.9", useTLS: false) == true)
#expect(controller._test_resolveManualUseTLS(host: "localhost", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "127.0.0.1", useTLS: false) == false)
@@ -118,6 +119,17 @@ import Testing
#expect(controller._test_resolveManualUseTLS(host: "0.0.0.0", useTLS: false) == false)
}
@Test @MainActor func manualConnectionsAllowPrivateLanPlaintext() async {
let controller = makeController()
#expect(controller._test_resolveManualUseTLS(host: "openclaw.local", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "192.168.1.20", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "10.0.0.5", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "172.16.1.5", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "169.254.1.5", useTLS: false) == false)
#expect(controller._test_resolveManualUseTLS(host: "fd00::1", useTLS: false) == false)
}
@Test @MainActor func manualDefaultPortUses443OnlyForTailnetTLSHosts() async {
let controller = makeController()

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.5"
"version": "2026.5.6"
}

View File

@@ -152,15 +152,17 @@ final class CanvasManager {
private func handleGatewayPush(_ push: GatewayPush) {
guard case let .snapshot(snapshot) = push else { return }
let raw = snapshot.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let raw =
(snapshot.pluginsurfaceurls?["canvas"]?.value as? String)?
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
if raw.isEmpty {
Self.logger.debug("canvas host url missing in gateway snapshot")
Self.logger.debug("canvas plugin surface URL missing in gateway snapshot")
} else {
Self.logger.debug("canvas host url snapshot=\(raw, privacy: .public)")
Self.logger.debug("canvas plugin surface URL snapshot=\(raw, privacy: .public)")
}
let a2uiUrl = Self.resolveA2UIHostUrl(from: raw)
if a2uiUrl == nil, !raw.isEmpty {
Self.logger.debug("canvas host url invalid; cannot resolve A2UI")
Self.logger.debug("canvas plugin surface URL invalid; cannot resolve A2UI")
}
guard let controller = self.panelController else {
if a2uiUrl != nil {
@@ -197,7 +199,7 @@ final class CanvasManager {
}
private func resolveA2UIHostUrl() async -> String? {
let raw = await GatewayConnection.shared.canvasHostUrl()
let raw = await GatewayConnection.shared.canvasPluginSurfaceUrl()
return Self.resolveA2UIHostUrl(from: raw)
}

View File

@@ -43,7 +43,8 @@ enum ExecApprovalEvaluator {
let allowAlwaysPatterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: command,
cwd: cwd,
env: env)
env: env,
rawCommand: allowlistRawCommand)
let allowlistMatches = security == .allowlist
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
: []

View File

@@ -27,7 +27,7 @@ struct ExecCommandResolution {
{
// Allowlist resolution must follow actual argv execution for wrappers.
// `rawCommand` is caller-supplied display text and may be canonicalized.
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
let shell = ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand)
if shell.isWrapper {
// Fail closed when env modifiers precede a shell wrapper. This mirrors
// system-run binding behavior where such invocations must stay bound to
@@ -68,7 +68,8 @@ struct ExecCommandResolution {
static func resolveAllowAlwaysPatterns(
command: [String],
cwd: String?,
env: [String: String]?) -> [String]
env: [String: String]?,
rawCommand: String? = nil) -> [String]
{
var patterns: [String] = []
var seen = Set<String>()
@@ -76,6 +77,7 @@ struct ExecCommandResolution {
command: command,
cwd: cwd,
env: env,
rawCommand: rawCommand,
depth: 0,
patterns: &patterns,
seen: &seen)
@@ -152,6 +154,7 @@ struct ExecCommandResolution {
command: [String],
cwd: String?,
env: [String: String]?,
rawCommand: String?,
depth: Int,
patterns: inout [String],
seen: inout Set<String>)
@@ -162,13 +165,19 @@ struct ExecCommandResolution {
if let token0 = command.first?.trimmingCharacters(in: .whitespacesAndNewlines),
ExecCommandToken.basenameLower(token0) == "env",
let envUnwrapped = ExecEnvInvocationUnwrapper.unwrap(command),
!envUnwrapped.isEmpty
let envUnwrapped = ExecEnvInvocationUnwrapper.unwrapWithMetadata(command),
!envUnwrapped.command.isEmpty
{
if envUnwrapped.usesModifiers,
self.isAllowlistShellWrapper(command: envUnwrapped.command, rawCommand: rawCommand)
{
return
}
self.collectAllowAlwaysPatterns(
command: envUnwrapped,
command: envUnwrapped.command,
cwd: cwd,
env: env,
rawCommand: rawCommand,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
@@ -180,13 +189,14 @@ struct ExecCommandResolution {
command: shellMultiplexer,
cwd: cwd,
env: env,
rawCommand: rawCommand,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
return
}
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: nil)
let shell = ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand)
if shell.isWrapper {
guard let shellCommand = shell.command,
let segments = self.splitShellCommandChain(shellCommand)
@@ -202,6 +212,7 @@ struct ExecCommandResolution {
command: tokens,
cwd: cwd,
env: env,
rawCommand: nil,
depth: depth + 1,
patterns: &patterns,
seen: &seen)
@@ -218,6 +229,10 @@ struct ExecCommandResolution {
patterns.append(pattern)
}
private static func isAllowlistShellWrapper(command: [String], rawCommand: String?) -> Bool {
ExecShellWrapperParser.extractForAllowlist(command: command, rawCommand: rawCommand).isWrapper
}
private static func unwrapShellMultiplexerInvocation(_ argv: [String]) -> [String]? {
guard let token0 = argv.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token0.isEmpty else {
return nil

View File

@@ -0,0 +1,278 @@
import Foundation
enum ExecInlineCommandParser {
struct Match {
let tokenIndex: Int
let inlineCommand: String?
let valueTokenOffset: Int
init(tokenIndex: Int, inlineCommand: String?, valueTokenOffset: Int = 1) {
self.tokenIndex = tokenIndex
self.inlineCommand = inlineCommand
self.valueTokenOffset = valueTokenOffset
}
}
private struct CombinedCommandFlag {
let attachedCommand: String?
let separateValueCount: Int
}
private static let posixShellOptionsWithSeparateValues = Set([
"--init-file",
"--rcfile",
"-O",
"-o",
"+O",
"+o",
])
static func hasPosixInteractiveStartupBeforeInlineCommand(
_ argv: [String],
flags: Set<String>) -> Bool
{
var idx = 1
var sawInteractiveMode = false
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if token == "--" {
return false
}
if self.isPosixInteractiveModeOption(token) {
sawInteractiveMode = true
}
if flags.contains(token) || self.isCombinedCommandFlag(token) {
return sawInteractiveMode
}
if !token.hasPrefix("-"), !token.hasPrefix("+") {
return false
}
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
if combinedValueCount > 0 {
idx += 1 + combinedValueCount
continue
}
if self.consumesSeparateValue(token) {
idx += 2
continue
}
idx += 1
}
return false
}
static func hasPosixLoginStartupBeforeInlineCommand(
_ argv: [String],
flags: Set<String>) -> Bool
{
var idx = 1
var sawLoginMode = false
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if token == "--" {
return false
}
if token == "--login" || self.isPosixShortOption(token, containing: "l") {
sawLoginMode = true
}
if flags.contains(token) || self.isCombinedCommandFlag(token) {
return sawLoginMode
}
if !token.hasPrefix("-"), !token.hasPrefix("+") {
return false
}
let combinedValueCount = self.combinedSeparateValueOptionCount(token)
if combinedValueCount > 0 {
idx += 1 + combinedValueCount
continue
}
if self.consumesSeparateValue(token) {
idx += 2
continue
}
idx += 1
}
return false
}
static func hasFishInitCommandOption(_ argv: [String]) -> Bool {
var idx = 1
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if token == "--" {
return false
}
if token == "-C" || token == "--init-command" {
return true
}
if token.hasPrefix("-C"), token != "-C" {
return true
}
if token.hasPrefix("--init-command=") {
return true
}
if !token.hasPrefix("-"), !token.hasPrefix("+") {
return false
}
idx += 1
}
return false
}
static func hasFishAttachedCommandOption(_ argv: [String]) -> Bool {
var idx = 1
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if token == "--" {
return false
}
if token.hasPrefix("-c"), token != "-c" {
return true
}
if !token.hasPrefix("-"), !token.hasPrefix("+") {
return false
}
idx += 1
}
return false
}
static func findMatch(
_ argv: [String],
flags: Set<String>,
allowCombinedC: Bool) -> Match?
{
var idx = 1
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if token == "--" {
break
}
let comparableToken = allowCombinedC ? token : token.lowercased()
if flags.contains(comparableToken) {
return Match(tokenIndex: idx, inlineCommand: nil)
}
if allowCombinedC, let combined = self.parseCombinedCommandFlag(token) {
if let attachedCommand = combined.attachedCommand {
return Match(tokenIndex: idx, inlineCommand: attachedCommand, valueTokenOffset: 0)
}
return Match(
tokenIndex: idx,
inlineCommand: nil,
valueTokenOffset: 1 + combined.separateValueCount)
}
if allowCombinedC, !token.hasPrefix("-"), !token.hasPrefix("+") {
break
}
let combinedValueCount = allowCombinedC ? self.combinedSeparateValueOptionCount(token) : 0
if combinedValueCount > 0 {
idx += 1 + combinedValueCount
continue
}
if allowCombinedC, self.consumesSeparateValue(token) {
idx += 2
continue
}
idx += 1
}
return nil
}
static func extractInlineCommand(
_ argv: [String],
flags: Set<String>,
allowCombinedC: Bool) -> String?
{
guard let match = self.findMatch(argv, flags: flags, allowCombinedC: allowCombinedC) else {
return nil
}
if let inlineCommand = match.inlineCommand {
return inlineCommand
}
let nextIndex = match.tokenIndex + match.valueTokenOffset
let payload = nextIndex < argv.count
? argv[nextIndex].trimmingCharacters(in: .whitespacesAndNewlines)
: ""
return payload.isEmpty ? nil : payload
}
private static func isCombinedCommandFlag(_ token: String) -> Bool {
self.parseCombinedCommandFlag(token) != nil
}
private static func parseCombinedCommandFlag(_ token: String) -> CombinedCommandFlag? {
let chars = Array(token)
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
return nil
}
let optionChars = Array(chars.dropFirst())
guard let commandFlagIndex = optionChars.firstIndex(of: "c") else {
return nil
}
if optionChars.contains("-") {
return nil
}
let suffix = String(optionChars.dropFirst(commandFlagIndex + 1))
if !suffix.isEmpty,
suffix.range(of: #"[^A-Za-z]"#, options: .regularExpression) != nil
{
return CombinedCommandFlag(attachedCommand: suffix, separateValueCount: 0)
}
let separateValueCount = optionChars.reduce(0) { count, char in
count + ((char == "o" || char == "O") ? 1 : 0)
}
return CombinedCommandFlag(attachedCommand: nil, separateValueCount: separateValueCount)
}
private static func combinedSeparateValueOptionCount(_ token: String) -> Int {
let chars = Array(token)
guard chars.count >= 2, chars[0] == "-" || chars[0] == "+", chars[1] != "-" else {
return 0
}
if chars.dropFirst().contains("-") {
return 0
}
return chars.dropFirst().reduce(0) { count, char in
count + ((char == "o" || char == "O") ? 1 : 0)
}
}
private static func consumesSeparateValue(_ token: String) -> Bool {
self.posixShellOptionsWithSeparateValues.contains(token)
}
private static func isPosixInteractiveModeOption(_ token: String) -> Bool {
token == "--interactive" || self.isPosixShortOption(token, containing: "i")
}
private static func isPosixShortOption(_ token: String, containing option: Character) -> Bool {
let chars = Array(token)
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
return false
}
if chars.dropFirst().contains("-") {
return false
}
return chars.dropFirst().contains(option)
}
}

View File

@@ -6,9 +6,10 @@ enum ExecShellWrapperParser {
let command: String?
static let notWrapper = ParsedShellWrapper(isWrapper: false, command: nil)
static let blockedWrapper = ParsedShellWrapper(isWrapper: true, command: nil)
}
private enum Kind {
private enum Kind: Equatable {
case posix
case cmd
case powershell
@@ -27,14 +28,34 @@ enum ExecShellWrapperParser {
WrapperSpec(kind: .cmd, names: ["cmd.exe", "cmd"]),
WrapperSpec(kind: .powershell, names: ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]),
]
private static let loginStartupShellNames = Set(["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"])
static func extract(command: [String], rawCommand: String?) -> ParsedShellWrapper {
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
return self.extract(command: command, preferredRaw: preferredRaw, depth: 0)
return self.extract(
command: command,
preferredRaw: preferredRaw,
failClosedOnStartupWrappers: false,
depth: 0)
}
private static func extract(command: [String], preferredRaw: String?, depth: Int) -> ParsedShellWrapper {
static func extractForAllowlist(command: [String], rawCommand: String?) -> ParsedShellWrapper {
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
return self.extract(
command: command,
preferredRaw: preferredRaw,
failClosedOnStartupWrappers: true,
depth: 0)
}
private static func extract(
command: [String],
preferredRaw: String?,
failClosedOnStartupWrappers: Bool,
depth: Int) -> ParsedShellWrapper
{
guard depth < ExecEnvInvocationUnwrapper.maxWrapperDepth else {
return .notWrapper
}
@@ -47,19 +68,96 @@ enum ExecShellWrapperParser {
guard let unwrapped = ExecEnvInvocationUnwrapper.unwrap(command) else {
return .notWrapper
}
return self.extract(command: unwrapped, preferredRaw: preferredRaw, depth: depth + 1)
return self.extract(
command: unwrapped,
preferredRaw: preferredRaw,
failClosedOnStartupWrappers: failClosedOnStartupWrappers,
depth: depth + 1)
}
guard let spec = self.wrapperSpecs.first(where: { $0.names.contains(base0) }) else {
return .notWrapper
}
if spec.kind == .posix,
base0 == "fish",
ExecInlineCommandParser.hasFishAttachedCommandOption(command)
{
return .blockedWrapper
}
let includeLegacyLoginInlineForm = failClosedOnStartupWrappers &&
!self.legacyLoginInlinePayloadMatchesRaw(
command: command,
spec: spec,
base0: base0,
preferredRaw: preferredRaw)
if self.startupWrapperRequiresFullArgv(
command: command,
spec: spec,
base0: base0,
includeLegacyLoginInlineForm: includeLegacyLoginInlineForm)
{
return .blockedWrapper
}
guard let payload = self.extractPayload(command: command, spec: spec) else {
return .notWrapper
}
let normalized = preferredRaw ?? payload
let normalized = failClosedOnStartupWrappers ? payload : preferredRaw ?? payload
return ParsedShellWrapper(isWrapper: true, command: normalized)
}
private static func startupWrapperRequiresFullArgv(
command: [String],
spec: WrapperSpec,
base0: String,
includeLegacyLoginInlineForm: Bool) -> Bool
{
guard spec.kind == .posix else {
return false
}
if base0 == "fish",
ExecInlineCommandParser.hasFishInitCommandOption(command)
{
return true
}
if self.loginStartupShellNames.contains(base0),
ExecInlineCommandParser.hasPosixLoginStartupBeforeInlineCommand(
command,
flags: self.posixInlineFlags)
{
return includeLegacyLoginInlineForm || !self.isLegacyShLoginInlineForm(command, base0: base0)
}
return ExecInlineCommandParser.hasPosixInteractiveStartupBeforeInlineCommand(
command,
flags: self.posixInlineFlags)
}
private static func isLegacyLoginInlineForm(_ command: [String]) -> Bool {
guard command.count > 1 else {
return false
}
return command[1].trimmingCharacters(in: .whitespacesAndNewlines) == "-lc"
}
private static func isLegacyShLoginInlineForm(_ command: [String], base0: String) -> Bool {
base0 == "sh" && self.isLegacyLoginInlineForm(command)
}
private static func legacyLoginInlinePayloadMatchesRaw(
command: [String],
spec: WrapperSpec,
base0: String,
preferredRaw: String?) -> Bool
{
guard let preferredRaw,
base0 == "sh",
self.isLegacyLoginInlineForm(command),
let payload = self.extractPayload(command: command, spec: spec)
else {
return false
}
return payload == preferredRaw.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func extractPayload(command: [String], spec: WrapperSpec) -> String? {
switch spec.kind {
case .posix:
@@ -72,12 +170,10 @@ enum ExecShellWrapperParser {
}
private static func extractPosixInlineCommand(_ command: [String]) -> String? {
let flag = command.count > 1 ? command[1].trimmingCharacters(in: .whitespacesAndNewlines) : ""
guard self.posixInlineFlags.contains(flag.lowercased()) else {
return nil
}
let payload = command.count > 2 ? command[2].trimmingCharacters(in: .whitespacesAndNewlines) : ""
return payload.isEmpty ? nil : payload
ExecInlineCommandParser.extractInlineCommand(
command,
flags: self.posixInlineFlags,
allowCombinedC: true)
}
private static func extractCmdInlineCommand(_ command: [String]) -> String? {
@@ -97,10 +193,10 @@ enum ExecShellWrapperParser {
if token.isEmpty { continue }
if token == "--" { break }
if self.powershellInlineFlags.contains(token) {
let payload = idx + 1 < command.count
? command[idx + 1].trimmingCharacters(in: .whitespacesAndNewlines)
: ""
return payload.isEmpty ? nil : payload
return ExecInlineCommandParser.extractInlineCommand(
command,
flags: self.powershellInlineFlags,
allowCombinedC: false)
}
}
return nil

View File

@@ -326,40 +326,12 @@ enum ExecSystemRunCommandValidator {
return current
}
private struct InlineCommandTokenMatch {
var tokenIndex: Int
var inlineCommand: String?
}
private static func findInlineCommandTokenMatch(
_ argv: [String],
flags: Set<String>,
allowCombinedC: Bool) -> InlineCommandTokenMatch?
allowCombinedC: Bool) -> ExecInlineCommandParser.Match?
{
var idx = 1
while idx < argv.count {
let token = argv[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
let lower = token.lowercased()
if lower == "--" {
break
}
if flags.contains(lower) {
return InlineCommandTokenMatch(tokenIndex: idx, inlineCommand: nil)
}
if allowCombinedC, let inlineOffset = self.combinedCommandInlineOffset(token) {
let inline = String(token.dropFirst(inlineOffset))
.trimmingCharacters(in: .whitespacesAndNewlines)
return InlineCommandTokenMatch(
tokenIndex: idx,
inlineCommand: inline.isEmpty ? nil : inline)
}
idx += 1
}
return nil
ExecInlineCommandParser.findMatch(argv, flags: flags, allowCombinedC: allowCombinedC)
}
private static func resolveInlineCommandTokenIndex(
@@ -373,24 +345,10 @@ enum ExecSystemRunCommandValidator {
if match.inlineCommand != nil {
return match.tokenIndex
}
let nextIndex = match.tokenIndex + 1
let nextIndex = match.tokenIndex + match.valueTokenOffset
return nextIndex < argv.count ? nextIndex : nil
}
private static func combinedCommandInlineOffset(_ token: String) -> Int? {
let chars = Array(token.lowercased())
guard chars.count >= 2, chars[0] == "-", chars[1] != "-" else {
return nil
}
if chars.dropFirst().contains("-") {
return nil
}
guard let commandIndex = chars.firstIndex(of: "c"), commandIndex > 0 else {
return nil
}
return commandIndex + 1
}
private static func extractShellInlinePayload(
_ argv: [String],
normalizedWrapper: String) -> String?
@@ -421,7 +379,7 @@ enum ExecSystemRunCommandValidator {
if let inlineCommand = match.inlineCommand {
return inlineCommand
}
let nextIndex = match.tokenIndex + 1
let nextIndex = match.tokenIndex + match.valueTokenOffset
return self.trimmedNonEmpty(nextIndex < argv.count ? argv[nextIndex] : nil)
}

View File

@@ -311,9 +311,10 @@ actor GatewayConnection {
self.lastSnapshot = nil
}
func canvasHostUrl() async -> String? {
func canvasPluginSurfaceUrl() async -> String? {
guard let snapshot = self.lastSnapshot else { return nil }
let trimmed = snapshot.canvashosturl?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
let raw = snapshot.pluginsurfaceurls?["canvas"]?.value as? String
let trimmed = raw?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}

View File

@@ -8,10 +8,18 @@ final class MacNodeModeCoordinator {
private let logger = Logger(subsystem: "ai.openclaw", category: "mac-node")
private var task: Task<Void, Never>?
private let runtime = MacNodeRuntime()
private let session = GatewayNodeSession()
private let runtime: MacNodeRuntime
private let session: GatewayNodeSession
private var autoRepairedTLSFingerprintsByStoreKey: [String: String] = [:]
private init() {
let session = GatewayNodeSession()
self.session = session
self.runtime = MacNodeRuntime(
canvasSurfaceUrl: { await session.currentCanvasHostUrl() },
refreshCanvasSurfaceUrl: { await session.refreshCanvasHostUrl() })
}
func start() {
guard self.task == nil else { return }
self.task = Task { [weak self] in

View File

@@ -7,6 +7,8 @@ actor MacNodeRuntime {
private let cameraCapture = CameraCaptureService()
private let makeMainActorServices: () async -> any MacNodeRuntimeMainActorServices
private let browserProxyRequest: @Sendable (String?) async throws -> String
private let canvasSurfaceUrl: @Sendable () async -> String?
private let refreshCanvasSurfaceUrl: @Sendable () async -> String?
private var cachedMainActorServices: (any MacNodeRuntimeMainActorServices)?
private var mainSessionKey: String = "main"
private var eventSender: (@Sendable (String, String?) async -> Void)?
@@ -17,10 +19,16 @@ actor MacNodeRuntime {
},
browserProxyRequest: @escaping @Sendable (String?) async throws -> String = { paramsJSON in
try await MacNodeBrowserProxy.shared.request(paramsJSON: paramsJSON)
})
},
canvasSurfaceUrl: @escaping @Sendable () async -> String? = {
await GatewayConnection.shared.canvasPluginSurfaceUrl()
},
refreshCanvasSurfaceUrl: @escaping @Sendable () async -> String? = { nil })
{
self.makeMainActorServices = makeMainActorServices
self.browserProxyRequest = browserProxyRequest
self.canvasSurfaceUrl = canvasSurfaceUrl
self.refreshCanvasSurfaceUrl = refreshCanvasSurfaceUrl
}
func updateMainSessionKey(_ sessionKey: String) {
@@ -441,7 +449,7 @@ actor MacNodeRuntime {
private func ensureA2UIHost() async throws {
if await self.isA2UIReady() { return }
guard let a2uiUrl = await self.resolveA2UIHostUrl() else {
guard let a2uiUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh() else {
throw NSError(domain: "Canvas", code: 30, userInfo: [
NSLocalizedDescriptionKey: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
])
@@ -451,18 +459,35 @@ actor MacNodeRuntime {
try CanvasManager.shared.show(sessionKey: sessionKey, path: a2uiUrl)
}
if await self.isA2UIReady(poll: true) { return }
if let refreshedUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true) {
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: sessionKey, path: refreshedUrl)
}
if await self.isA2UIReady(poll: true) { return }
}
throw NSError(domain: "Canvas", code: 31, userInfo: [
NSLocalizedDescriptionKey: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
])
}
private func resolveA2UIHostUrl() async -> String? {
guard let raw = await GatewayConnection.shared.canvasHostUrl() else { return nil }
Self.resolveA2UIHostUrl(from: await self.canvasSurfaceUrl())
}
private static func resolveA2UIHostUrl(from raw: String?) -> String? {
guard let raw else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let baseUrl = URL(string: trimmed) else { return nil }
return baseUrl.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=macos"
}
func resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
if !forceRefresh, let current = await self.resolveA2UIHostUrl() {
return current
}
return Self.resolveA2UIHostUrl(from: await self.refreshCanvasSurfaceUrl())
}
private func isA2UIReady(poll: Bool = false) async -> Bool {
let deadline = poll ? Date().addingTimeInterval(6.0) : Date()
while true {

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.5.5</string>
<string>2026.5.6</string>
<key>CFBundleVersion</key>
<string>2026050500</string>
<string>2026050600</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -395,10 +395,18 @@ actor TalkModeRuntime {
"talk chat.send ok runId=\(response.runId, privacy: .public) " +
"session=\(sessionKey, privacy: .public)")
guard let assistantText = await self.waitForAssistantText(
var assistantText = await self.waitForAssistantEventText(
sessionKey: sessionKey,
since: startedAt,
runId: response.runId,
timeoutSeconds: 45)
if assistantText == nil {
self.logger.warning("talk assistant event text missing; using history fallback")
assistantText = await self.waitForAssistantTextFromHistory(
sessionKey: sessionKey,
since: startedAt,
timeoutSeconds: 12)
}
guard let assistantText
else {
self.logger.warning("talk assistant text missing after timeout")
await self.startListening()
@@ -439,7 +447,67 @@ actor TalkModeRuntime {
return TalkPromptBuilder.build(transcript: transcript, interruptedAtSeconds: interrupted)
}
private func waitForAssistantText(
private func waitForAssistantEventText(
sessionKey: String,
runId: String,
timeoutSeconds: Int) async -> String?
{
let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200)
return await withTaskGroup(of: String?.self) { group in
group.addTask { [runId, sessionKey] in
var latestText: String?
for await push in stream {
if Task.isCancelled { return latestText }
guard case let .event(evt) = push else { continue }
guard evt.event == "chat", let payload = evt.payload else { continue }
guard let chatEvent = try? GatewayPayloadDecoding.decode(
payload,
as: OpenClawChatEventPayload.self)
else {
continue
}
guard chatEvent.runId == runId else { continue }
if let eventSessionKey = chatEvent.sessionKey,
!Self.matchesSessionKey(eventSessionKey, sessionKey)
{
continue
}
if let text = OpenClawChatEventText.assistantText(from: chatEvent) {
latestText = text
}
switch chatEvent.state {
case "final":
return latestText
case "aborted", "error":
return nil
default:
break
}
}
return latestText
}
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds) * 1_000_000_000)
return nil
}
guard let result = await group.next() else {
group.cancelAll()
return nil
}
group.cancelAll()
return result
}
}
private static func matchesSessionKey(_ incoming: String, _ current: String) -> Bool {
let incoming = incoming.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let current = current.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if incoming == current { return true }
return (incoming == "agent:main:main" && current == "main") ||
(incoming == "main" && current == "agent:main:main")
}
private func waitForAssistantTextFromHistory(
sessionKey: String,
since: Double,
timeoutSeconds: Int) async -> String?
@@ -1111,7 +1179,10 @@ extension TalkModeRuntime {
} else {
self.ttsLogger
.info(
"talk provider \(parsed.activeProvider, privacy: .public) uses gateway talk.speak with system voice fallback")
"""
talk provider \(parsed.activeProvider, privacy: .public) uses gateway talk.speak \
with system voice fallback
""")
}
return parsed
} catch {

View File

@@ -63,8 +63,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey()
let defaults = decoded.defaults.map {
OpenClawChatSessionsDefaults(
modelProvider: $0.modelProvider,
model: $0.model,
contextTokens: $0.contextTokens,
thinkingLevels: $0.thinkingLevels,
thinkingOptions: $0.thinkingOptions,
thinkingDefault: $0.thinkingDefault,
mainSessionKey: mainSessionKey)
} ?? OpenClawChatSessionsDefaults(
model: nil,

File diff suppressed because it is too large Load Diff

View File

@@ -111,7 +111,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist splits shell chains`() {
let command = ["/bin/sh", "-lc", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
let command = ["/bin/sh", "-c", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test",
@@ -122,9 +122,109 @@ struct ExecAllowlistTests {
#expect(resolutions[1].executableName == "touch")
}
@Test func `resolve for allowlist splits posix combined c flag payloads`() {
for command in [
["/bin/bash", "-xc", "/usr/bin/printf safe_marker"],
["/bin/bash", "-ec", "/usr/bin/printf safe_marker"],
["/bin/bash", "-euxc", "/usr/bin/printf safe_marker"],
["/bin/bash", "-cx", "/usr/bin/printf safe_marker"],
["/bin/bash", "-O", "extglob", "-xc", "/usr/bin/printf safe_marker"],
["/bin/bash", "-co", "vi", "/usr/bin/printf safe_marker"],
["/bin/bash", "-oc", "vi", "/usr/bin/printf safe_marker"],
["/bin/bash", "-cO", "extglob", "/usr/bin/printf safe_marker"],
["/bin/bash", "-xo", "vi", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "-xO", "extglob", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "+xo", "vi", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "--rcfile", "/tmp/rc", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "--init-file=/tmp/rc", "-c", "/usr/bin/printf safe_marker"],
] {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
#expect(resolutions[0].executableName == "printf")
}
}
@Test func `resolve for allowlist treats c after posix shell operand as direct exec`() {
for command in [
["/bin/bash", "./script.sh", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "-x", "-C", "echo ok", "-c", "/usr/bin/printf safe_marker"],
] {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: "/tmp",
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/bin/bash")
#expect(resolutions[0].executableName == "bash")
}
}
@Test func `resolve for allowlist fails closed for interactive posix shell wrappers`() {
for command in [
["/bin/bash", "-i", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "-ic", "/usr/bin/printf safe_marker"],
["/bin/bash", "--rcfile", "/tmp/payload.sh", "-i", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--interactive", "-c", "/usr/bin/printf safe_marker"],
] {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
}
@Test func `resolve for allowlist fails closed for login shell wrappers`() {
for command in [
["/bin/bash", "-l", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "--login", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "-xlc", "/usr/bin/printf safe_marker"],
["/bin/dash", "-lc", "/usr/bin/printf safe_marker"],
["ash", "-lc", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "-l", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--login", "-c", "/usr/bin/printf safe_marker"],
["/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
["/bin/sh", "-x", "-lc", "/usr/bin/printf safe_marker"],
["/usr/bin/env", "/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
] {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
}
@Test func `resolve for allowlist fails closed for fish init command wrappers`() {
for command in [
["/usr/bin/fish", "--init-command=/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--init-command", "/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "-C", "/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "-C/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--init-command", "-c; /tmp/payload.fish", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "-C", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "-c/tmp/payload.fish", "/usr/bin/printf safe_marker"],
] {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
}
@Test func `resolve for allowlist uses wrapper argv payload even with canonical raw command`() {
let command = ["/bin/sh", "-lc", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
let canonicalRaw = "/bin/sh -lc \"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test\""
let command = ["/bin/sh", "-c", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
let canonicalRaw = "/bin/sh -c \"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test\""
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: canonicalRaw,
@@ -135,6 +235,25 @@ struct ExecAllowlistTests {
#expect(resolutions[1].executableName == "touch")
}
@Test func `resolve for allowlist preserves generated sh lc raw payload binding`() {
let command = ["/bin/sh", "-lc", "/usr/bin/printf safe_marker"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "/usr/bin/printf safe_marker",
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
#expect(resolutions[0].executableName == "printf")
let rawlessResolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(rawlessResolutions.isEmpty)
}
@Test func `resolve for allowlist fails closed for env modified shell wrappers`() {
let command = ["/usr/bin/env", "BASH_ENV=/tmp/payload.sh", "bash", "-lc", "echo allowlisted"]
let canonicalRaw = "/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc \"echo allowlisted\""
@@ -158,7 +277,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist keeps quoted operators in single segment`() {
let command = ["/bin/sh", "-lc", "echo \"a && b\""]
let command = ["/bin/sh", "-c", "echo \"a && b\""]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo \"a && b\"",
@@ -169,7 +288,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist fails closed on command substitution`() {
let command = ["/bin/sh", "-lc", "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)"]
let command = ["/bin/sh", "-c", "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo $(/usr/bin/touch /tmp/openclaw-allowlist-test-subst)",
@@ -179,7 +298,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist fails closed on quoted command substitution`() {
let command = ["/bin/sh", "-lc", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
let command = ["/bin/sh", "-c", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\"",
@@ -189,7 +308,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist fails closed on line-continued command substitution`() {
let command = ["/bin/sh", "-lc", "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)"]
let command = ["/bin/sh", "-c", "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-line-cont-subst)",
@@ -201,7 +320,7 @@ struct ExecAllowlistTests {
@Test func `resolve for allowlist fails closed on chained line-continued command substitution`() {
let command = [
"/bin/sh",
"-lc",
"-c",
"echo ok && $\\\n(/usr/bin/touch /tmp/openclaw-allowlist-test-chained-line-cont-subst)",
]
let resolutions = ExecCommandResolution.resolveForAllowlist(
@@ -213,7 +332,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist fails closed on quoted backticks`() {
let command = ["/bin/sh", "-lc", "echo \"ok `/usr/bin/id`\""]
let command = ["/bin/sh", "-c", "echo \"ok `/usr/bin/id`\""]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo \"ok `/usr/bin/id`\"",
@@ -226,7 +345,7 @@ struct ExecAllowlistTests {
let fixtures = try Self.loadShellParserParityCases()
for fixture in fixtures {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: ["/bin/sh", "-lc", fixture.command],
command: ["/bin/sh", "-c", fixture.command],
rawCommand: fixture.command,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
@@ -276,7 +395,7 @@ struct ExecAllowlistTests {
let command = [
"/usr/bin/env",
"/bin/sh",
"-lc",
"-c",
"echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test",
]
let resolutions = ExecCommandResolution.resolveForAllowlist(
@@ -290,7 +409,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist unwraps env dispatch wrappers inside shell segments`() {
let command = ["/bin/sh", "-lc", "env /usr/bin/touch /tmp/openclaw-allowlist-test"]
let command = ["/bin/sh", "-c", "env /usr/bin/touch /tmp/openclaw-allowlist-test"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "env /usr/bin/touch /tmp/openclaw-allowlist-test",
@@ -302,7 +421,7 @@ struct ExecAllowlistTests {
}
@Test func `resolve for allowlist preserves env assignments inside shell segments`() {
let command = ["/bin/sh", "-lc", "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test"]
let command = ["/bin/sh", "-c", "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "env FOO=bar /usr/bin/touch /tmp/openclaw-allowlist-test",
@@ -326,8 +445,8 @@ struct ExecAllowlistTests {
}
@Test func `approval evaluator resolves shell payload from canonical wrapper text`() async {
let command = ["/bin/sh", "-lc", "/usr/bin/printf ok"]
let rawCommand = "/bin/sh -lc \"/usr/bin/printf ok\""
let command = ["/bin/sh", "-c", "/usr/bin/printf ok"]
let rawCommand = "/bin/sh -c \"/usr/bin/printf ok\""
let evaluation = await ExecApprovalEvaluator.evaluate(
command: command,
rawCommand: rawCommand,
@@ -350,6 +469,32 @@ struct ExecAllowlistTests {
#expect(patterns == ["/usr/bin/printf"])
}
@Test func `allow always patterns fail closed for env modified shell wrappers`() {
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: [
"/usr/bin/env",
"BASH_ENV=/tmp/payload.sh",
"/bin/sh",
"-lc",
"/usr/bin/printf ok",
],
cwd: nil,
env: ["PATH": "/usr/bin:/bin"],
rawCommand: "/usr/bin/printf ok")
#expect(patterns.isEmpty)
}
@Test func `allow always patterns preserve generated sh lc raw payload binding`() {
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: ["/bin/sh", "-lc", "/usr/bin/printf safe_marker"],
cwd: nil,
env: ["PATH": "/usr/bin:/bin"],
rawCommand: "/usr/bin/printf safe_marker")
#expect(patterns == ["/usr/bin/printf"])
}
@Test func `match all requires every segment to match`() {
let first = ExecCommandResolution(
rawExecutable: "echo",

View File

@@ -85,6 +85,48 @@ struct ExecSystemRunCommandValidatorTests {
}
}
@Test func `fish attached c command requires canonical raw command binding`() {
let command = ["/usr/bin/fish", "-c/tmp/payload.fish", "/usr/bin/printf safe_marker"]
let result = ExecSystemRunCommandValidator.resolve(
command: command,
rawCommand: "/usr/bin/printf safe_marker")
switch result {
case .ok:
Issue.record("expected rawCommand mismatch for attached fish command payload")
case let .invalid(message):
#expect(message.contains("rawCommand does not match command"))
}
}
@Test func `startup shell wrappers require canonical raw command binding`() {
for command in [
["/bin/bash", "-lc", "/usr/bin/printf safe_marker"],
["/bin/bash", "--rcfile", "/tmp/payload.sh", "-i", "-c", "/usr/bin/printf safe_marker"],
["/bin/bash", "--login", "-c", "/usr/bin/printf safe_marker"],
["/usr/bin/fish", "--init-command=/tmp/payload.fish", "-c", "/usr/bin/printf safe_marker"],
] {
let legacy = ExecSystemRunCommandValidator.resolve(
command: command,
rawCommand: "/usr/bin/printf safe_marker")
switch legacy {
case .ok:
Issue.record("expected rawCommand mismatch for startup shell wrapper")
case let .invalid(message):
#expect(message.contains("rawCommand does not match command"))
}
let canonicalRaw = ExecCommandFormatter.displayString(for: command)
let canonical = ExecSystemRunCommandValidator.resolve(command: command, rawCommand: canonicalRaw)
switch canonical {
case let .ok(resolved):
#expect(resolved.displayCommand == canonicalRaw)
case let .invalid(message):
Issue.record("unexpected invalid result for canonical raw command: \(message)")
}
}
}
private static func loadContractCases() throws -> [SystemRunCommandContractCase] {
let fixtureURL = try self.findContractFixtureURL()
let data = try Data(contentsOf: fixtureURL)

View File

@@ -22,7 +22,7 @@ struct MacGatewayChatTransportMappingTests {
server: [:],
features: [:],
snapshot: snapshot,
canvashosturl: nil,
pluginsurfaceurls: nil,
auth: [:],
policy: [:])

View File

@@ -5,6 +5,15 @@ import Testing
@testable import OpenClaw
struct MacNodeRuntimeTests {
actor CanvasRefreshProbe {
private(set) var calls = 0
func refresh() -> String? {
self.calls += 1
return "http://127.0.0.1:18789/refreshed"
}
}
@Test func `handle invoke rejects unknown command`() async {
let runtime = MacNodeRuntime()
let response = await runtime.handleInvoke(
@@ -12,6 +21,21 @@ struct MacNodeRuntimeTests {
#expect(response.ok == false)
}
@Test func `A2UI host capability refresh uses injected node session refresher`() async {
let probe = CanvasRefreshProbe()
let runtime = MacNodeRuntime(
canvasSurfaceUrl: { "http://127.0.0.1:18789/current" },
refreshCanvasSurfaceUrl: { await probe.refresh() })
let current = await runtime.resolveA2UIHostUrlWithCapabilityRefresh()
#expect(current == "http://127.0.0.1:18789/current/__openclaw__/a2ui/?platform=macos")
#expect(await probe.calls == 0)
let refreshed = await runtime.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true)
#expect(refreshed == "http://127.0.0.1:18789/refreshed/__openclaw__/a2ui/?platform=macos")
#expect(await probe.calls == 1)
}
@Test func `handle invoke rejects empty system run`() async throws {
let runtime = MacNodeRuntime()
let params = OpenClawSystemRunParams(command: [])

View File

@@ -9,8 +9,6 @@ import UniformTypeIdentifiers
@MainActor
struct OpenClawChatComposer: View {
private static let menuThinkingLevels = ["off", "low", "medium", "high"]
@Bindable var viewModel: OpenClawChatViewModel
let style: OpenClawChatView.Style
let showsSessionSwitcher: Bool
@@ -95,12 +93,8 @@ struct OpenClawChatComposer: View {
get: { self.viewModel.thinkingLevel },
set: { next in self.viewModel.selectThinkingLevel(next) }))
{
Text("Off").tag("off")
Text("Low").tag("low")
Text("Medium").tag("medium")
Text("High").tag("high")
if !Self.menuThinkingLevels.contains(self.viewModel.thinkingLevel) {
Text(self.viewModel.thinkingLevel.capitalized).tag(self.viewModel.thinkingLevel)
ForEach(self.viewModel.thinkingLevelOptions) { option in
Text(option.label).tag(option.id)
}
}
.labelsHidden()

View File

@@ -0,0 +1,78 @@
import OpenClawKit
public enum OpenClawChatEventText {
public static func assistantText(from event: OpenClawChatEventPayload) -> String? {
self.assistantText(fromMessage: event.message)
}
public static func assistantText(fromMessage message: AnyCodable?) -> String? {
guard let message else { return nil }
return self.assistantText(fromValue: message.value)
}
private static func assistantText(fromValue value: Any) -> String? {
if let text = value as? String {
return self.trimmed(text)
}
guard let object = self.dictionary(from: value) else { return nil }
if let role = self.stringValue(object["role"])?.trimmingCharacters(in: .whitespacesAndNewlines),
!role.isEmpty,
role.lowercased() != "assistant"
{
return nil
}
guard let content = object["content"] else { return nil }
return self.textContent(from: content)
}
private static func textContent(from value: Any) -> String? {
if let text = value as? String {
return self.trimmed(text)
}
let parts: [String] = if let array = value as? [AnyCodable] {
array.compactMap { self.textContentPart(from: $0.value) }
} else if let array = value as? [Any] {
array.compactMap { self.textContentPart(from: $0) }
} else {
self.textContentPart(from: value).map { [$0] } ?? []
}
return self.trimmed(parts.joined(separator: "\n"))
}
private static func textContentPart(from value: Any) -> String? {
if let text = value as? String {
return self.trimmed(text)
}
guard let object = self.dictionary(from: value) else { return nil }
return self.trimmed(self.stringValue(object["text"]) ?? "")
}
private static func dictionary(from value: Any) -> [String: Any]? {
if let dict = value as? [String: AnyCodable] {
return dict.mapValues(\.value)
}
if let dict = value as? [String: Any] {
return dict
}
return nil
}
private static func stringValue(_ value: Any?) -> String? {
if let string = value as? String {
return string
}
if let wrapped = value as? AnyCodable {
return self.stringValue(wrapped.value)
}
return nil
}
private static func trimmed(_ text: String) -> String? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}

View File

@@ -1,5 +1,15 @@
import Foundation
public struct OpenClawChatThinkingLevelOption: Codable, Identifiable, Sendable, Hashable {
public let id: String
public let label: String
public init(id: String, label: String) {
self.id = id
self.label = label
}
}
public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable {
public var id: String {
self.selectionID
@@ -34,13 +44,29 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable
}
public struct OpenClawChatSessionsDefaults: Codable, Sendable {
public let modelProvider: String?
public let model: String?
public let contextTokens: Int?
public let thinkingLevels: [OpenClawChatThinkingLevelOption]?
public let thinkingOptions: [String]?
public let thinkingDefault: String?
public let mainSessionKey: String?
public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) {
public init(
modelProvider: String? = nil,
model: String?,
contextTokens: Int?,
thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil,
thinkingOptions: [String]? = nil,
thinkingDefault: String? = nil,
mainSessionKey: String? = nil)
{
self.modelProvider = modelProvider
self.model = model
self.contextTokens = contextTokens
self.thinkingLevels = thinkingLevels
self.thinkingOptions = thinkingOptions
self.thinkingDefault = thinkingDefault
self.mainSessionKey = mainSessionKey
}
}
@@ -72,6 +98,57 @@ public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashabl
public let modelProvider: String?
public let model: String?
public let contextTokens: Int?
public let thinkingLevels: [OpenClawChatThinkingLevelOption]?
public let thinkingOptions: [String]?
public let thinkingDefault: String?
public init(
key: String,
kind: String?,
displayName: String?,
surface: String?,
subject: String?,
room: String?,
space: String?,
updatedAt: Double?,
sessionId: String?,
systemSent: Bool?,
abortedLastRun: Bool?,
thinkingLevel: String?,
verboseLevel: String?,
inputTokens: Int?,
outputTokens: Int?,
totalTokens: Int?,
modelProvider: String?,
model: String?,
contextTokens: Int?,
thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil,
thinkingOptions: [String]? = nil,
thinkingDefault: String? = nil)
{
self.key = key
self.kind = kind
self.displayName = displayName
self.surface = surface
self.subject = subject
self.room = room
self.space = space
self.updatedAt = updatedAt
self.sessionId = sessionId
self.systemSent = systemSent
self.abortedLastRun = abortedLastRun
self.thinkingLevel = thinkingLevel
self.verboseLevel = verboseLevel
self.inputTokens = inputTokens
self.outputTokens = outputTokens
self.totalTokens = totalTokens
self.modelProvider = modelProvider
self.model = model
self.contextTokens = contextTokens
self.thinkingLevels = thinkingLevels
self.thinkingOptions = thinkingOptions
self.thinkingDefault = thinkingDefault
}
}
public struct OpenClawChatSessionsListResponse: Codable, Sendable {

View File

@@ -21,6 +21,7 @@ public final class OpenClawChatViewModel {
public private(set) var messages: [OpenClawChatMessage] = []
public var input: String = ""
public private(set) var thinkingLevel: String
public private(set) var thinkingLevelOptions: [OpenClawChatThinkingLevelOption]
public private(set) var modelSelectionID: String = "__default__"
public private(set) var modelChoices: [OpenClawChatModelChoice] = []
public private(set) var isLoading = false
@@ -83,7 +84,11 @@ public final class OpenClawChatViewModel {
self.sessionKey = sessionKey
self.transport = transport
let normalizedThinkingLevel = Self.normalizedThinkingLevel(initialThinkingLevel)
self.thinkingLevel = normalizedThinkingLevel ?? "off"
let initialResolvedThinkingLevel = normalizedThinkingLevel ?? "off"
self.thinkingLevel = initialResolvedThinkingLevel
self.thinkingLevelOptions = Self.withCurrentThinkingOption(
Self.baseThinkingLevelOptions,
current: initialResolvedThinkingLevel)
self.prefersExplicitThinkingLevel = normalizedThinkingLevel != nil
self.onThinkingLevelChanged = onThinkingLevelChanged
@@ -198,6 +203,14 @@ public final class OpenClawChatViewModel {
return "Default: \(self.modelLabel(for: defaultModelID))"
}
private static let baseThinkingLevelOptions: [OpenClawChatThinkingLevelOption] = [
OpenClawChatThinkingLevelOption(id: "off", label: "off"),
OpenClawChatThinkingLevelOption(id: "minimal", label: "minimal"),
OpenClawChatThinkingLevelOption(id: "low", label: "low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "medium"),
OpenClawChatThinkingLevelOption(id: "high", label: "high"),
]
public func addAttachments(urls: [URL]) {
Task { await self.loadAttachments(urls: urls) }
}
@@ -243,6 +256,7 @@ public final class OpenClawChatViewModel {
{
self.thinkingLevel = level
}
self.syncThinkingLevelOptions()
await self.pollHealthIfNeeded(force: true)
await self.fetchSessions(limit: 50)
await self.fetchModels()
@@ -594,6 +608,7 @@ public final class OpenClawChatViewModel {
self.sessions = res.sessions
self.sessionDefaults = res.defaults
self.syncSelectedModel()
self.syncThinkingLevelOptions()
} catch {
// Best-effort.
}
@@ -675,6 +690,8 @@ public final class OpenClawChatViewModel {
let sessionKey = self.sessionKey
self.thinkingLevel = next
self.syncThinkingLevelOptions()
self.updateCurrentSessionThinkingLevel(next, sessionKey: sessionKey)
self.onThinkingLevelChanged?(next)
self.nextThinkingSelectionRequestID &+= 1
let requestID = self.nextThinkingSelectionRequestID
@@ -770,6 +787,99 @@ public final class OpenClawChatViewModel {
}
}
private func syncThinkingLevelOptions() {
let currentSession = self.sessions.first(where: { $0.key == self.sessionKey })
var options = self.resolvedThinkingLevelOptions(for: currentSession)
if let current = Self.normalizedThinkingLevel(self.thinkingLevel) {
options = Self.withCurrentThinkingOption(options, current: current)
}
self.thinkingLevelOptions = options
}
private func resolvedThinkingLevelOptions(
for currentSession: OpenClawChatSessionEntry?) -> [OpenClawChatThinkingLevelOption]
{
if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty {
return levels
}
let defaultsMatch = currentSession.map {
Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults)
} ?? true
if defaultsMatch,
let levels = Self.normalizedThinkingLevelOptions(self.sessionDefaults?.thinkingLevels),
!levels.isEmpty
{
return levels
}
if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty {
return options
}
if defaultsMatch,
let options = Self.thinkingOptions(from: self.sessionDefaults?.thinkingOptions),
!options.isEmpty
{
return options
}
return Self.baseThinkingLevelOptions
}
private static func sessionModelMatchesDefaults(
_ session: OpenClawChatSessionEntry,
defaults: OpenClawChatSessionsDefaults?) -> Bool
{
let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider
let modelMatches = session.model == nil || session.model == defaults?.model
return providerMatches && modelMatches
}
private static func normalizedThinkingLevelOptions(
_ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]?
{
guard let levels else { return nil }
return Self.dedupedThinkingOptions(
levels.compactMap { level in
guard let id = Self.normalizedThinkingLevel(level.id) else { return nil }
let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines)
return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label)
})
}
private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? {
guard let labels else { return nil }
return Self.dedupedThinkingOptions(
labels.compactMap { label in
guard let id = Self.normalizedThinkingLevel(label) else { return nil }
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed)
})
}
private static func withCurrentThinkingOption(
_ options: [OpenClawChatThinkingLevelOption],
current: String) -> [OpenClawChatThinkingLevelOption]
{
guard !options.contains(where: { $0.id == current }) else { return options }
return options + [OpenClawChatThinkingLevelOption(id: current, label: current)]
}
private static func dedupedThinkingOptions(
_ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption]
{
var result: [OpenClawChatThinkingLevelOption] = []
var seen = Set<String>()
for option in options {
guard !option.id.isEmpty, !seen.contains(option.id) else { continue }
seen.insert(option.id)
result.append(option)
}
return result
}
private func placeholderSession(key: String) -> OpenClawChatSessionEntry {
OpenClawChatSessionEntry(
key: key,
@@ -858,6 +968,9 @@ public final class OpenClawChatViewModel {
modelProvider: resolved.modelProvider,
sessionKey: sessionKey,
syncSelection: syncSelection)
if sessionKey == self.sessionKey {
self.syncThinkingLevelOptions()
}
}
private func resolvedSessionModelIdentity(forSelectionID selectionID: String)
@@ -885,6 +998,34 @@ public final class OpenClawChatViewModel {
return "\(provider)/\(modelID)"
}
private func updateCurrentSessionThinkingLevel(_ thinkingLevel: String?, sessionKey: String) {
guard let index = self.sessions.firstIndex(where: { $0.key == sessionKey }) else { return }
let current = self.sessions[index]
self.sessions[index] = OpenClawChatSessionEntry(
key: current.key,
kind: current.kind,
displayName: current.displayName,
surface: current.surface,
subject: current.subject,
room: current.room,
space: current.space,
updatedAt: current.updatedAt,
sessionId: current.sessionId,
systemSent: current.systemSent,
abortedLastRun: current.abortedLastRun,
thinkingLevel: thinkingLevel,
verboseLevel: current.verboseLevel,
inputTokens: current.inputTokens,
outputTokens: current.outputTokens,
totalTokens: current.totalTokens,
modelProvider: current.modelProvider,
model: current.model,
contextTokens: current.contextTokens,
thinkingLevels: current.thinkingLevels,
thinkingOptions: current.thinkingOptions,
thinkingDefault: current.thinkingDefault)
}
private func updateCurrentSessionModel(
modelID: String?,
modelProvider: String?,
@@ -1084,6 +1225,7 @@ public final class OpenClawChatViewModel {
let level = Self.normalizedThinkingLevel(payload.thinkingLevel)
{
self.thinkingLevel = level
self.syncThinkingLevelOptions()
}
} catch {
chatUILogger.error("refresh history failed \(error.localizedDescription, privacy: .public)")
@@ -1195,9 +1337,33 @@ public final class OpenClawChatViewModel {
private static func normalizedThinkingLevel(_ level: String?) -> String? {
guard let level else { return nil }
let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(trimmed) else {
return nil
guard !trimmed.isEmpty else { return nil }
let collapsed = trimmed.replacingOccurrences(
of: "[\\s_-]+",
with: "",
options: .regularExpression)
switch collapsed {
case "adaptive", "auto":
return "adaptive"
case "max":
return "max"
case "xhigh", "extrahigh":
return "xhigh"
case "off", "none":
return "off"
case "on", "enable", "enabled":
return "low"
case "min", "minimal", "think":
return "minimal"
case "low", "thinkhard":
return "low"
case "mid", "med", "medium", "thinkharder", "harder":
return "medium"
case "high", "ultra", "ultrathink", "thinkhardest", "highest":
return "high"
default:
return trimmed
}
return trimmed
}
}

View File

@@ -105,18 +105,15 @@ public struct BridgeHello: Codable, Sendable {
public struct BridgeHelloOk: Codable, Sendable {
public let type: String
public let serverName: String
public let canvasHostUrl: String?
public let mainSessionKey: String?
public init(
type: String = "hello-ok",
serverName: String,
canvasHostUrl: String? = nil,
mainSessionKey: String? = nil)
{
self.type = type
self.serverName = serverName
self.canvasHostUrl = canvasHostUrl
self.mainSessionKey = mainSessionKey
}
}

View File

@@ -6,6 +6,7 @@ public enum OpenClawCapability: String, Codable, Sendable {
case camera
case screen
case voiceWake
case talk
case location
case device
case watch

View File

@@ -116,7 +116,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
return nil
}
let tls = payload.tls ?? true
if !tls, !LoopbackHost.isLoopbackHost(host) {
if !tls, !LoopbackHost.isLocalNetworkHost(host) {
return nil
}
return GatewayConnectDeepLink(
@@ -143,7 +143,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
return nil
}
let tls = scheme == "wss" || scheme == "https"
if !tls, !LoopbackHost.isLoopbackHost(hostname) {
if !tls, !LoopbackHost.isLocalNetworkHost(hostname) {
return nil
}
return GatewayConnectDeepLink(
@@ -254,7 +254,7 @@ public enum DeepLinkParser {
}
let port = query["port"].flatMap { Int($0) } ?? 18789
let tls = (query["tls"] as NSString?)?.boolValue ?? false
if !tls, !LoopbackHost.isLoopbackHost(hostParam) {
if !tls, !LoopbackHost.isLocalNetworkHost(hostParam) {
return nil
}
return .gateway(

View File

@@ -522,7 +522,8 @@ public actor GatewayChannelActor {
(includeDeviceIdentity && explicitPassword == nil && explicitBootstrapToken == nil
? storedToken
: nil)
let authBootstrapToken = authToken == nil ? explicitBootstrapToken : nil
let authBootstrapToken =
authToken == nil && explicitPassword == nil ? explicitBootstrapToken : nil
let authDeviceToken = shouldUseDeviceRetryToken ? storedToken : nil
let authSource: GatewayAuthSource = if authDeviceToken != nil || (explicitToken == nil && authToken != nil) {
.deviceToken

View File

@@ -11,19 +11,6 @@ private struct NodeInvokeRequestPayload: Codable {
var idempotencyKey: String?
}
private func replaceCanvasCapabilityInScopedHostUrl(scopedUrl: String, capability: String) -> String? {
let marker = "/__openclaw__/cap/"
guard let markerRange = scopedUrl.range(of: marker) else { return nil }
let capabilityStart = markerRange.upperBound
let suffix = scopedUrl[capabilityStart...]
let nextSlash = suffix.firstIndex(of: "/")
let nextQuery = suffix.firstIndex(of: "?")
let nextFragment = suffix.firstIndex(of: "#")
let capabilityEnd = [nextSlash, nextQuery, nextFragment].compactMap(\.self).min() ?? scopedUrl.endIndex
guard capabilityStart < capabilityEnd else { return nil }
return String(scopedUrl[..<capabilityStart]) + capability + String(scopedUrl[capabilityEnd...])
}
func canonicalizeCanvasHostUrl(raw: String?, activeURL: URL?) -> String? {
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return nil }
@@ -152,7 +139,11 @@ public actor GatewayNodeSession {
}
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
private var canvasHostUrl: String?
private var pluginSurfaceUrls: [String: String] = [:]
private struct PluginSurfaceRefreshResponse: Decodable {
let pluginSurfaceUrls: [String: AnyCodable]?
}
public init() {}
@@ -270,47 +261,26 @@ public actor GatewayNodeSession {
}
public func currentCanvasHostUrl() -> String? {
self.canvasHostUrl
self.pluginSurfaceUrls["canvas"]
}
public func refreshNodeCanvasCapability(timeoutMs: Int = 8000) async -> Bool {
guard let channel = self.channel else { return false }
do {
let data = try await channel.request(
method: "node.canvas.capability.refresh",
params: [:],
timeoutMs: Double(max(timeoutMs, 1)))
guard
let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let rawCapability = payload["canvasCapability"] as? String
else {
self.logger.warning("node.canvas.capability.refresh missing canvasCapability")
return false
}
let capability = rawCapability.trimmingCharacters(in: .whitespacesAndNewlines)
guard !capability.isEmpty else {
self.logger.warning("node.canvas.capability.refresh returned empty capability")
return false
}
let scopedUrl = self.canvasHostUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !scopedUrl.isEmpty else {
self.logger.warning("node.canvas.capability.refresh missing local canvasHostUrl")
return false
}
guard let refreshed = replaceCanvasCapabilityInScopedHostUrl(
scopedUrl: scopedUrl,
capability: capability)
else {
self.logger.warning("node.canvas.capability.refresh could not rewrite scoped canvas URL")
return false
}
self.canvasHostUrl = refreshed
return true
} catch {
self.logger.warning(
"node.canvas.capability.refresh failed: \(error.localizedDescription, privacy: .public)")
return false
}
@discardableResult
public func refreshPluginSurfaceUrl(surface: String, timeoutSeconds: Int = 8) async -> String? {
guard let channel = self.channel else { return nil }
let trimmedSurface = surface.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedSurface.isEmpty else { return nil }
return await self.requestPluginSurfaceRefresh(
channel: channel,
method: "node.pluginSurface.refresh",
params: ["surface": AnyCodable(trimmedSurface)],
surface: trimmedSurface,
timeoutSeconds: timeoutSeconds)
}
@discardableResult
public func refreshCanvasHostUrl(timeoutSeconds: Int = 8) async -> String? {
await self.refreshPluginSurfaceUrl(surface: "canvas", timeoutSeconds: timeoutSeconds)
}
public func currentRemoteAddress() -> String? {
@@ -364,8 +334,7 @@ public actor GatewayNodeSession {
private func handlePush(_ push: GatewayPush) async {
switch push {
case let .snapshot(ok):
let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines)
self.canvasHostUrl = self.normalizeCanvasHostUrl(raw)
self.pluginSurfaceUrls = self.normalizePluginSurfaceUrls(ok.pluginsurfaceurls)
if self.hasEverConnected {
self.broadcastServerEvent(
EventFrame(type: "event", event: "seqGap", payload: nil, seq: nil, stateversion: nil))
@@ -436,6 +405,39 @@ public actor GatewayNodeSession {
canonicalizeCanvasHostUrl(raw: raw, activeURL: self.activeURL)
}
private func normalizePluginSurfaceUrls(_ raw: [String: AnyCodable]?) -> [String: String] {
var normalized: [String: String] = [:]
if let raw {
normalized = raw.compactMapValues { value in
self.normalizeCanvasHostUrl(value.value as? String)
}
}
return normalized
}
private func requestPluginSurfaceRefresh(
channel: GatewayChannelActor,
method: String,
params: [String: AnyCodable]?,
surface: String,
timeoutSeconds: Int) async -> String?
{
do {
let data = try await channel.request(
method: method,
params: params,
timeoutMs: Double(timeoutSeconds * 1000))
let decoded = try self.decoder.decode(PluginSurfaceRefreshResponse.self, from: data)
let urls = self.normalizePluginSurfaceUrls(decoded.pluginSurfaceUrls)
guard let refreshed = urls[surface] else { return nil }
self.pluginSurfaceUrls[surface] = refreshed
return refreshed
} catch {
self.logger.debug("\(method, privacy: .public) failed: \(error.localizedDescription, privacy: .public)")
return nil
}
}
private func handleEvent(_ evt: EventFrame) async {
self.broadcastServerEvent(evt)
guard evt.event == "node.invoke.request" else { return }

View File

@@ -41,16 +41,32 @@ public enum LoopbackHost {
}
public static func isLocalNetworkHost(_ rawHost: String) -> Bool {
let host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let host = self.normalizedHost(rawHost)
guard !host.isEmpty else { return false }
if self.isLoopbackHost(host) { return true }
if host.hasSuffix(".local") { return true }
if host.hasSuffix(".ts.net") { return true }
if host.hasSuffix(".tailscale.net") { return true }
// Allow MagicDNS / LAN hostnames like "peters-mac-studio-1".
if !host.contains("."), !host.contains(":") { return true }
guard let ipv4 = self.parseIPv4(host) else { return false }
return self.isLocalNetworkIPv4(ipv4)
if let ipv4 = self.parseIPv4(host) {
return self.isLocalNetworkIPv4(ipv4)
}
guard let ipv6 = IPv6Address(host) else { return false }
let bytes = Array(ipv6.rawValue)
let isUniqueLocal = (bytes[0] & 0xFE) == 0xFC
let isLinkLocal = bytes[0] == 0xFE && (bytes[1] & 0xC0) == 0x80
return isUniqueLocal || isLinkLocal
}
static func normalizedHost(_ rawHost: String) -> String {
var host = rawHost
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
if host.hasSuffix(".") {
host.removeLast()
}
if let zoneIndex = host.firstIndex(of: "%") {
host = String(host[..<zoneIndex])
}
return host
}
static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
@@ -73,8 +89,6 @@ public enum LoopbackHost {
if a == 127 { return true }
// 169.254.0.0/16 (link-local)
if a == 169, b == 254 { return true }
// Tailscale: 100.64.0.0/10
if a == 100, (64...127).contains(Int(b)) { return true }
return false
}
}

View File

@@ -2,7 +2,7 @@
// swiftlint:disable file_length
import Foundation
public let GATEWAY_PROTOCOL_VERSION = 3
public let GATEWAY_PROTOCOL_VERSION = 4
public enum ErrorCode: String, Codable, Sendable {
case notLinked = "NOT_LINKED"
@@ -98,7 +98,7 @@ public struct HelloOk: Codable, Sendable {
public let server: [String: AnyCodable]
public let features: [String: AnyCodable]
public let snapshot: Snapshot
public let canvashosturl: String?
public let pluginsurfaceurls: [String: AnyCodable]?
public let auth: [String: AnyCodable]
public let policy: [String: AnyCodable]
@@ -108,7 +108,7 @@ public struct HelloOk: Codable, Sendable {
server: [String: AnyCodable],
features: [String: AnyCodable],
snapshot: Snapshot,
canvashosturl: String?,
pluginsurfaceurls: [String: AnyCodable]?,
auth: [String: AnyCodable],
policy: [String: AnyCodable])
{
@@ -117,7 +117,7 @@ public struct HelloOk: Codable, Sendable {
self.server = server
self.features = features
self.snapshot = snapshot
self.canvashosturl = canvashosturl
self.pluginsurfaceurls = pluginsurfaceurls
self.auth = auth
self.policy = policy
}
@@ -128,7 +128,7 @@ public struct HelloOk: Codable, Sendable {
case server
case features
case snapshot
case canvashosturl = "canvasHostUrl"
case pluginsurfaceurls = "pluginSurfaceUrls"
case auth
case policy
}
@@ -1517,6 +1517,7 @@ public struct SessionsListParams: Codable, Sendable {
public let activeminutes: Int?
public let includeglobal: Bool?
public let includeunknown: Bool?
public let configuredagentsonly: Bool?
public let includederivedtitles: Bool?
public let includelastmessage: Bool?
public let label: String?
@@ -1529,6 +1530,7 @@ public struct SessionsListParams: Codable, Sendable {
activeminutes: Int?,
includeglobal: Bool?,
includeunknown: Bool?,
configuredagentsonly: Bool?,
includederivedtitles: Bool?,
includelastmessage: Bool?,
label: String?,
@@ -1540,6 +1542,7 @@ public struct SessionsListParams: Codable, Sendable {
self.activeminutes = activeminutes
self.includeglobal = includeglobal
self.includeunknown = includeunknown
self.configuredagentsonly = configuredagentsonly
self.includederivedtitles = includederivedtitles
self.includelastmessage = includelastmessage
self.label = label
@@ -1553,6 +1556,7 @@ public struct SessionsListParams: Codable, Sendable {
case activeminutes = "activeMinutes"
case includeglobal = "includeGlobal"
case includeunknown = "includeUnknown"
case configuredagentsonly = "configuredAgentsOnly"
case includederivedtitles = "includeDerivedTitles"
case includelastmessage = "includeLastMessage"
case label
@@ -1568,19 +1572,22 @@ public struct SessionsCleanupParams: Codable, Sendable {
public let enforce: Bool?
public let activekey: String?
public let fixmissing: Bool?
public let fixdmscope: Bool?
public init(
agent: String?,
allagents: Bool?,
enforce: Bool?,
activekey: String?,
fixmissing: Bool?)
fixmissing: Bool?,
fixdmscope: Bool?)
{
self.agent = agent
self.allagents = allagents
self.enforce = enforce
self.activekey = activekey
self.fixmissing = fixmissing
self.fixdmscope = fixdmscope
}
private enum CodingKeys: String, CodingKey {
@@ -1589,6 +1596,7 @@ public struct SessionsCleanupParams: Codable, Sendable {
case enforce
case activekey = "activeKey"
case fixmissing = "fixMissing"
case fixdmscope = "fixDmScope"
}
}
@@ -1910,6 +1918,7 @@ public struct SessionsCreateParams: Codable, Sendable {
public let label: String?
public let model: String?
public let parentsessionkey: String?
public let emitcommandhooks: Bool?
public let task: String?
public let message: String?
@@ -1919,6 +1928,7 @@ public struct SessionsCreateParams: Codable, Sendable {
label: String?,
model: String?,
parentsessionkey: String?,
emitcommandhooks: Bool?,
task: String?,
message: String?)
{
@@ -1927,6 +1937,7 @@ public struct SessionsCreateParams: Codable, Sendable {
self.label = label
self.model = model
self.parentsessionkey = parentsessionkey
self.emitcommandhooks = emitcommandhooks
self.task = task
self.message = message
}
@@ -1937,6 +1948,7 @@ public struct SessionsCreateParams: Codable, Sendable {
case label
case model
case parentsessionkey = "parentSessionKey"
case emitcommandhooks = "emitCommandHooks"
case task
case message
}
@@ -2231,6 +2243,9 @@ public struct SessionsUsageParams: Codable, Sendable {
public let startdate: String?
public let enddate: String?
public let mode: AnyCodable?
public let range: AnyCodable?
public let groupby: AnyCodable?
public let includehistorical: Bool?
public let utcoffset: String?
public let limit: Int?
public let includecontextweight: Bool?
@@ -2240,6 +2255,9 @@ public struct SessionsUsageParams: Codable, Sendable {
startdate: String?,
enddate: String?,
mode: AnyCodable?,
range: AnyCodable?,
groupby: AnyCodable?,
includehistorical: Bool?,
utcoffset: String?,
limit: Int?,
includecontextweight: Bool?)
@@ -2248,6 +2266,9 @@ public struct SessionsUsageParams: Codable, Sendable {
self.startdate = startdate
self.enddate = enddate
self.mode = mode
self.range = range
self.groupby = groupby
self.includehistorical = includehistorical
self.utcoffset = utcoffset
self.limit = limit
self.includecontextweight = includecontextweight
@@ -2258,6 +2279,9 @@ public struct SessionsUsageParams: Codable, Sendable {
case startdate = "startDate"
case enddate = "endDate"
case mode
case range
case groupby = "groupBy"
case includehistorical = "includeHistorical"
case utcoffset = "utcOffset"
case limit
case includecontextweight = "includeContextWeight"
@@ -2630,6 +2654,202 @@ public struct TalkModeParams: Codable, Sendable {
}
}
public struct TalkEvent: Codable, Sendable {
public let id: String
public let type: AnyCodable
public let sessionid: String
public let turnid: String?
public let captureid: String?
public let seq: Int
public let timestamp: String
public let mode: AnyCodable
public let transport: AnyCodable
public let brain: AnyCodable
public let provider: String?
public let final: Bool?
public let callid: String?
public let itemid: String?
public let parentid: String?
public let payload: AnyCodable
public init(
id: String,
type: AnyCodable,
sessionid: String,
turnid: String?,
captureid: String?,
seq: Int,
timestamp: String,
mode: AnyCodable,
transport: AnyCodable,
brain: AnyCodable,
provider: String?,
final: Bool?,
callid: String?,
itemid: String?,
parentid: String?,
payload: AnyCodable)
{
self.id = id
self.type = type
self.sessionid = sessionid
self.turnid = turnid
self.captureid = captureid
self.seq = seq
self.timestamp = timestamp
self.mode = mode
self.transport = transport
self.brain = brain
self.provider = provider
self.final = final
self.callid = callid
self.itemid = itemid
self.parentid = parentid
self.payload = payload
}
private enum CodingKeys: String, CodingKey {
case id
case type
case sessionid = "sessionId"
case turnid = "turnId"
case captureid = "captureId"
case seq
case timestamp
case mode
case transport
case brain
case provider
case final
case callid = "callId"
case itemid = "itemId"
case parentid = "parentId"
case payload
}
}
public struct TalkCatalogParams: Codable, Sendable {}
public struct TalkCatalogResult: Codable, Sendable {
public let modes: [AnyCodable]
public let transports: [AnyCodable]
public let brains: [AnyCodable]
public let speech: [String: AnyCodable]
public let transcription: [String: AnyCodable]
public let realtime: [String: AnyCodable]
public init(
modes: [AnyCodable],
transports: [AnyCodable],
brains: [AnyCodable],
speech: [String: AnyCodable],
transcription: [String: AnyCodable],
realtime: [String: AnyCodable])
{
self.modes = modes
self.transports = transports
self.brains = brains
self.speech = speech
self.transcription = transcription
self.realtime = realtime
}
private enum CodingKeys: String, CodingKey {
case modes
case transports
case brains
case speech
case transcription
case realtime
}
}
public struct TalkClientCreateParams: Codable, Sendable {
public let sessionkey: String?
public let provider: String?
public let model: String?
public let voice: String?
public let mode: AnyCodable?
public let transport: AnyCodable?
public let brain: AnyCodable?
public init(
sessionkey: String?,
provider: String?,
model: String?,
voice: String?,
mode: AnyCodable?,
transport: AnyCodable?,
brain: AnyCodable?)
{
self.sessionkey = sessionkey
self.provider = provider
self.model = model
self.voice = voice
self.mode = mode
self.transport = transport
self.brain = brain
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case provider
case model
case voice
case mode
case transport
case brain
}
}
public struct TalkClientToolCallParams: Codable, Sendable {
public let sessionkey: String
public let callid: String
public let name: String
public let args: AnyCodable?
public let relaysessionid: String?
public init(
sessionkey: String,
callid: String,
name: String,
args: AnyCodable?,
relaysessionid: String?)
{
self.sessionkey = sessionkey
self.callid = callid
self.name = name
self.args = args
self.relaysessionid = relaysessionid
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case callid = "callId"
case name
case args
case relaysessionid = "relaySessionId"
}
}
public struct TalkClientToolCallResult: Codable, Sendable {
public let runid: String
public let idempotencykey: String
public init(
runid: String,
idempotencykey: String)
{
self.runid = runid
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case idempotencykey = "idempotencyKey"
}
}
public struct TalkConfigParams: Codable, Sendable {
public let includesecrets: Bool?
@@ -2658,22 +2878,100 @@ public struct TalkConfigResult: Codable, Sendable {
}
}
public struct TalkRealtimeSessionParams: Codable, Sendable {
public struct TalkSessionAppendAudioParams: Codable, Sendable {
public let sessionid: String
public let audiobase64: String
public let timestamp: Double?
public init(
sessionid: String,
audiobase64: String,
timestamp: Double?)
{
self.sessionid = sessionid
self.audiobase64 = audiobase64
self.timestamp = timestamp
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
case audiobase64 = "audioBase64"
case timestamp
}
}
public struct TalkSessionCancelOutputParams: Codable, Sendable {
public let sessionid: String
public let turnid: String?
public let reason: String?
public init(
sessionid: String,
turnid: String?,
reason: String?)
{
self.sessionid = sessionid
self.turnid = turnid
self.reason = reason
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
case turnid = "turnId"
case reason
}
}
public struct TalkSessionCancelTurnParams: Codable, Sendable {
public let sessionid: String
public let turnid: String?
public let reason: String?
public init(
sessionid: String,
turnid: String?,
reason: String?)
{
self.sessionid = sessionid
self.turnid = turnid
self.reason = reason
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
case turnid = "turnId"
case reason
}
}
public struct TalkSessionCreateParams: Codable, Sendable {
public let sessionkey: String?
public let provider: String?
public let model: String?
public let voice: String?
public let mode: AnyCodable?
public let transport: AnyCodable?
public let brain: AnyCodable?
public let ttlms: Int?
public init(
sessionkey: String?,
provider: String?,
model: String?,
voice: String?)
voice: String?,
mode: AnyCodable?,
transport: AnyCodable?,
brain: AnyCodable?,
ttlms: Int?)
{
self.sessionkey = sessionkey
self.provider = provider
self.model = model
self.voice = voice
self.mode = mode
self.transport = transport
self.brain = brain
self.ttlms = ttlms
}
private enum CodingKeys: String, CodingKey {
@@ -2681,86 +2979,252 @@ public struct TalkRealtimeSessionParams: Codable, Sendable {
case provider
case model
case voice
case mode
case transport
case brain
case ttlms = "ttlMs"
}
}
public struct TalkRealtimeRelayAudioParams: Codable, Sendable {
public let relaysessionid: String
public let audiobase64: String
public let timestamp: Double?
public struct TalkSessionCreateResult: Codable, Sendable {
public let sessionid: String
public let provider: String?
public let mode: AnyCodable
public let transport: AnyCodable
public let brain: AnyCodable
public let relaysessionid: String?
public let transcriptionsessionid: String?
public let handoffid: String?
public let roomid: String?
public let roomurl: String?
public let token: String?
public let audio: AnyCodable?
public let model: String?
public let voice: String?
public let expiresat: Double?
public init(
relaysessionid: String,
audiobase64: String,
timestamp: Double?)
sessionid: String,
provider: String?,
mode: AnyCodable,
transport: AnyCodable,
brain: AnyCodable,
relaysessionid: String?,
transcriptionsessionid: String?,
handoffid: String?,
roomid: String?,
roomurl: String?,
token: String?,
audio: AnyCodable?,
model: String?,
voice: String?,
expiresat: Double?)
{
self.sessionid = sessionid
self.provider = provider
self.mode = mode
self.transport = transport
self.brain = brain
self.relaysessionid = relaysessionid
self.audiobase64 = audiobase64
self.timestamp = timestamp
self.transcriptionsessionid = transcriptionsessionid
self.handoffid = handoffid
self.roomid = roomid
self.roomurl = roomurl
self.token = token
self.audio = audio
self.model = model
self.voice = voice
self.expiresat = expiresat
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
case provider
case mode
case transport
case brain
case relaysessionid = "relaySessionId"
case audiobase64 = "audioBase64"
case timestamp
case transcriptionsessionid = "transcriptionSessionId"
case handoffid = "handoffId"
case roomid = "roomId"
case roomurl = "roomUrl"
case token
case audio
case model
case voice
case expiresat = "expiresAt"
}
}
public struct TalkRealtimeRelayMarkParams: Codable, Sendable {
public let relaysessionid: String
public let markname: String?
public struct TalkSessionJoinParams: Codable, Sendable {
public let sessionid: String
public let token: String
public init(
relaysessionid: String,
markname: String?)
sessionid: String,
token: String)
{
self.relaysessionid = relaysessionid
self.markname = markname
self.sessionid = sessionid
self.token = token
}
private enum CodingKeys: String, CodingKey {
case relaysessionid = "relaySessionId"
case markname = "markName"
case sessionid = "sessionId"
case token
}
}
public struct TalkRealtimeRelayStopParams: Codable, Sendable {
public let relaysessionid: String
public struct TalkSessionJoinResult: Codable, Sendable {
public let id: String
public let roomid: String
public let roomurl: String
public let sessionkey: String
public let sessionid: String?
public let channel: String?
public let target: String?
public let provider: String?
public let model: String?
public let voice: String?
public let mode: AnyCodable
public let transport: AnyCodable
public let brain: AnyCodable
public let createdat: Double
public let expiresat: Double
public let room: [String: AnyCodable]
public init(
relaysessionid: String)
id: String,
roomid: String,
roomurl: String,
sessionkey: String,
sessionid: String?,
channel: String?,
target: String?,
provider: String?,
model: String?,
voice: String?,
mode: AnyCodable,
transport: AnyCodable,
brain: AnyCodable,
createdat: Double,
expiresat: Double,
room: [String: AnyCodable])
{
self.relaysessionid = relaysessionid
self.id = id
self.roomid = roomid
self.roomurl = roomurl
self.sessionkey = sessionkey
self.sessionid = sessionid
self.channel = channel
self.target = target
self.provider = provider
self.model = model
self.voice = voice
self.mode = mode
self.transport = transport
self.brain = brain
self.createdat = createdat
self.expiresat = expiresat
self.room = room
}
private enum CodingKeys: String, CodingKey {
case relaysessionid = "relaySessionId"
case id
case roomid = "roomId"
case roomurl = "roomUrl"
case sessionkey = "sessionKey"
case sessionid = "sessionId"
case channel
case target
case provider
case model
case voice
case mode
case transport
case brain
case createdat = "createdAt"
case expiresat = "expiresAt"
case room
}
}
public struct TalkRealtimeRelayToolResultParams: Codable, Sendable {
public let relaysessionid: String
public struct TalkSessionTurnParams: Codable, Sendable {
public let sessionid: String
public let turnid: String?
public init(
sessionid: String,
turnid: String?)
{
self.sessionid = sessionid
self.turnid = turnid
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
case turnid = "turnId"
}
}
public struct TalkSessionTurnResult: Codable, Sendable {
public let ok: Bool
public let turnid: String?
public let events: [TalkEvent]?
public init(
ok: Bool,
turnid: String?,
events: [TalkEvent]?)
{
self.ok = ok
self.turnid = turnid
self.events = events
}
private enum CodingKeys: String, CodingKey {
case ok
case turnid = "turnId"
case events
}
}
public struct TalkSessionSubmitToolResultParams: Codable, Sendable {
public let sessionid: String
public let callid: String
public let result: AnyCodable
public init(
relaysessionid: String,
sessionid: String,
callid: String,
result: AnyCodable)
{
self.relaysessionid = relaysessionid
self.sessionid = sessionid
self.callid = callid
self.result = result
}
private enum CodingKeys: String, CodingKey {
case relaysessionid = "relaySessionId"
case sessionid = "sessionId"
case callid = "callId"
case result
}
}
public struct TalkRealtimeRelayOkResult: Codable, Sendable {
public struct TalkSessionCloseParams: Codable, Sendable {
public let sessionid: String
public init(
sessionid: String)
{
self.sessionid = sessionid
}
private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId"
}
}
public struct TalkSessionOkResult: Codable, Sendable {
public let ok: Bool
public init(
@@ -2903,6 +3367,8 @@ public struct ChannelsStatusResult: Codable, Sendable {
public let channelaccounts: [String: AnyCodable]
public let channeldefaultaccountid: [String: AnyCodable]
public let eventloop: [String: AnyCodable]?
public let partial: Bool?
public let warnings: [String]?
public init(
ts: Int,
@@ -2914,7 +3380,9 @@ public struct ChannelsStatusResult: Codable, Sendable {
channels: [String: AnyCodable],
channelaccounts: [String: AnyCodable],
channeldefaultaccountid: [String: AnyCodable],
eventloop: [String: AnyCodable]?)
eventloop: [String: AnyCodable]?,
partial: Bool?,
warnings: [String]?)
{
self.ts = ts
self.channelorder = channelorder
@@ -2926,6 +3394,8 @@ public struct ChannelsStatusResult: Codable, Sendable {
self.channelaccounts = channelaccounts
self.channeldefaultaccountid = channeldefaultaccountid
self.eventloop = eventloop
self.partial = partial
self.warnings = warnings
}
private enum CodingKeys: String, CodingKey {
@@ -2939,6 +3409,8 @@ public struct ChannelsStatusResult: Codable, Sendable {
case channelaccounts = "channelAccounts"
case channeldefaultaccountid = "channelDefaultAccountId"
case eventloop = "eventLoop"
case partial
case warnings
}
}
@@ -4775,6 +5247,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
public let severity: String?
public let toolname: String?
public let toolcallid: String?
public let alloweddecisions: [String]?
public let agentid: String?
public let sessionkey: String?
public let turnsourcechannel: String?
@@ -4791,6 +5264,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
severity: String?,
toolname: String?,
toolcallid: String?,
alloweddecisions: [String]?,
agentid: String?,
sessionkey: String?,
turnsourcechannel: String?,
@@ -4806,6 +5280,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
self.severity = severity
self.toolname = toolname
self.toolcallid = toolcallid
self.alloweddecisions = alloweddecisions
self.agentid = agentid
self.sessionkey = sessionkey
self.turnsourcechannel = turnsourcechannel
@@ -4823,6 +5298,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
case severity
case toolname = "toolName"
case toolcallid = "toolCallId"
case alloweddecisions = "allowedDecisions"
case agentid = "agentId"
case sessionkey = "sessionKey"
case turnsourcechannel = "turnSourceChannel"

View File

@@ -0,0 +1,50 @@
import OpenClawKit
import Testing
@testable import OpenClawChatUI
struct ChatEventTextTests {
@Test func `extracts assistant text from final chat event message`() {
let event = OpenClawChatEventPayload(
runId: "run-1",
sessionKey: "main",
state: "final",
message: AnyCodable([
"role": "assistant",
"content": [
["type": "text", "text": "hello"],
["type": "text", "text": "world"],
],
]),
errorMessage: nil)
#expect(OpenClawChatEventText.assistantText(from: event) == "hello\nworld")
}
@Test func `ignores user messages`() {
let event = OpenClawChatEventPayload(
runId: "run-1",
sessionKey: "main",
state: "delta",
message: AnyCodable([
"role": "user",
"content": [["type": "text", "text": "ignore me"]],
]),
errorMessage: nil)
#expect(OpenClawChatEventText.assistantText(from: event) == nil)
}
@Test func `extracts plain string content`() {
let event = OpenClawChatEventPayload(
runId: "run-1",
sessionKey: "main",
state: "final",
message: AnyCodable([
"role": "assistant",
"content": "plain reply",
]),
errorMessage: nil)
#expect(OpenClawChatEventText.assistantText(from: event) == "plain reply")
}
}

View File

@@ -46,6 +46,10 @@ private func sessionEntry(key: String, updatedAt: Double) -> OpenClawChatSession
contextTokens: nil)
}
private func thinkingOption(_ id: String, label: String? = nil) -> OpenClawChatThinkingLevelOption {
OpenClawChatThinkingLevelOption(id: id, label: label ?? id)
}
private func sessionEntry(
key: String,
updatedAt: Double,
@@ -1632,6 +1636,272 @@ extension TestChatTransportState {
}
}
@Test func decodesGatewayThinkingMetadataFromSessionList() throws {
let json = """
{
"defaults": {
"modelProvider": "anthropic",
"model": "claude-opus-4-7",
"thinkingLevels": [
{ "id": "off", "label": "off" },
{ "id": "adaptive", "label": "adaptive" },
{ "id": "max", "label": "maximum" }
],
"thinkingOptions": ["off", "adaptive", "maximum"],
"thinkingDefault": "adaptive"
},
"sessions": [
{
"key": "main",
"modelProvider": "openrouter",
"model": "deepseek/deepseek-v4",
"thinkingLevel": "max",
"thinkingLevels": [
{ "id": "off", "label": "off" },
{ "id": "xhigh", "label": "xhigh" },
{ "id": "max", "label": "max" }
],
"thinkingOptions": ["off", "xhigh", "max"],
"thinkingDefault": "max"
}
]
}
"""
let decoded = try JSONDecoder().decode(
OpenClawChatSessionsListResponse.self,
from: Data(json.utf8))
#expect(decoded.defaults?.modelProvider == "anthropic")
#expect(decoded.defaults?.thinkingLevels?.map(\.id) == ["off", "adaptive", "max"])
#expect(decoded.defaults?.thinkingLevels?.last?.label == "maximum")
#expect(decoded.defaults?.thinkingDefault == "adaptive")
#expect(decoded.sessions.first?.thinkingLevels?.map(\.id) == ["off", "xhigh", "max"])
#expect(decoded.sessions.first?.thinkingDefault == "max")
}
@Test func sessionThinkingLevelsDrivePickerOptions() async throws {
let history = OpenClawChatHistoryPayload(
sessionKey: "main",
sessionId: "sess-main",
messages: [],
thinkingLevel: "adaptive")
let sessions = OpenClawChatSessionsListResponse(
ts: 1,
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "openai-codex",
model: "gpt-5.5",
contextTokens: nil,
thinkingLevels: [
thinkingOption("off"),
thinkingOption("low"),
thinkingOption("xhigh"),
thinkingOption("max", label: "maximum"),
],
thinkingOptions: ["off", "low", "xhigh", "maximum"],
thinkingDefault: "xhigh"),
sessions: [
OpenClawChatSessionEntry(
key: "main",
kind: nil,
displayName: nil,
surface: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: 1,
sessionId: "sess-main",
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: "adaptive",
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "anthropic",
model: "claude-opus-4-7",
contextTokens: nil,
thinkingLevels: [
thinkingOption("off"),
thinkingOption("adaptive"),
thinkingOption("max", label: "maximum"),
],
thinkingOptions: ["off", "adaptive", "maximum"],
thinkingDefault: "adaptive"),
])
let (_, vm) = await makeViewModel(
historyResponses: [history],
sessionsResponses: [sessions])
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
#expect(await MainActor.run { vm.thinkingLevel } == "adaptive")
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"])
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "adaptive", "maximum"])
}
@Test func thinkingOptionsFallbackAndCurrentUnsupportedLevelStayVisible() async throws {
let history = OpenClawChatHistoryPayload(
sessionKey: "main",
sessionId: "sess-main",
messages: [],
thinkingLevel: "xhigh")
let sessions = OpenClawChatSessionsListResponse(
ts: 1,
path: nil,
count: 1,
defaults: nil,
sessions: [
OpenClawChatSessionEntry(
key: "main",
kind: nil,
displayName: nil,
surface: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: 1,
sessionId: "sess-main",
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: "xhigh",
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "openrouter",
model: "deepseek/deepseek-v4",
contextTokens: nil,
thinkingLevels: nil,
thinkingOptions: ["off", "max"],
thinkingDefault: "max"),
])
let (_, vm) = await makeViewModel(
historyResponses: [history],
sessionsResponses: [sessions])
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
#expect(await MainActor.run { vm.thinkingLevel } == "xhigh")
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "max", "xhigh"])
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "max", "xhigh"])
}
@Test func matchingDefaultThinkingLevelsBeatLegacyRowThinkingOptions() async throws {
let history = OpenClawChatHistoryPayload(
sessionKey: "main",
sessionId: "sess-main",
messages: [],
thinkingLevel: "adaptive")
let sessions = OpenClawChatSessionsListResponse(
ts: 1,
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "anthropic",
model: "claude-opus-4-7",
contextTokens: nil,
thinkingLevels: [
thinkingOption("off"),
thinkingOption("adaptive"),
thinkingOption("max"),
],
thinkingOptions: ["off", "adaptive", "max"],
thinkingDefault: "adaptive"),
sessions: [
OpenClawChatSessionEntry(
key: "main",
kind: nil,
displayName: nil,
surface: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: 1,
sessionId: "sess-main",
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: "adaptive",
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "anthropic",
model: "claude-opus-4-7",
contextTokens: nil,
thinkingLevels: nil,
thinkingOptions: ["off"],
thinkingDefault: "off"),
])
let (_, vm) = await makeViewModel(
historyResponses: [history],
sessionsResponses: [sessions])
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"])
}
@Test func defaultThinkingLevelsDoNotLeakToDifferentSessionModel() async throws {
let history = OpenClawChatHistoryPayload(
sessionKey: "main",
sessionId: "sess-main",
messages: [],
thinkingLevel: "max")
let sessions = OpenClawChatSessionsListResponse(
ts: 1,
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "anthropic",
model: "claude-opus-4-7",
contextTokens: nil,
thinkingLevels: [
thinkingOption("off"),
thinkingOption("adaptive"),
thinkingOption("max"),
],
thinkingOptions: ["off", "adaptive", "max"],
thinkingDefault: "adaptive"),
sessions: [
OpenClawChatSessionEntry(
key: "main",
kind: nil,
displayName: nil,
surface: nil,
subject: nil,
room: nil,
space: nil,
updatedAt: 1,
sessionId: "sess-main",
systemSent: nil,
abortedLastRun: nil,
thinkingLevel: "max",
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "openai",
model: "gpt-5.4",
contextTokens: nil),
])
let (_, vm) = await makeViewModel(
historyResponses: [history],
sessionsResponses: [sessions])
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
#expect(await MainActor.run { vm.thinkingLevel } == "max")
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } ==
["off", "minimal", "low", "medium", "high", "max"])
}
@Test func staleThinkingPatchCompletionReappliesLatestSelection() async throws {
let history = OpenClawChatHistoryPayload(
sessionKey: "main",

View File

@@ -59,6 +59,40 @@ private func setupCode(from payload: String) -> String {
password: nil))
}
@Test func setupCodeAllowsPrivateLanWs() {
let payload = #"{"url":"ws://192.168.1.20:18789","bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "192.168.1.20",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeAllowsMDNSWs() {
let payload = #"{"url":"ws://openclaw.local:18789","bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupCodeRejectsTailnetPlaintextWs() {
let payload = #"{"url":"ws://gateway.tailnet.ts.net:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeRejectsCgnatPlaintextWs() {
let payload = #"{"url":"ws://100.64.0.9:18789","bootstrapToken":"tok"}"#
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeParsesHostPayload() {
let payload = #"{"host":"gateway.tailnet.ts.net","port":443,"tls":true,"bootstrapToken":"tok"}"#
#expect(
@@ -88,6 +122,18 @@ private func setupCode(from payload: String) -> String {
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
}
@Test func setupCodeAllowsPrivateLanHostPayload() {
let payload = #"{"host":"openclaw.local","port":18789,"tls":false,"bootstrapToken":"tok"}"#
#expect(
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
host: "openclaw.local",
port: 18789,
tls: false,
bootstrapToken: "tok",
token: nil,
password: nil))
}
@Test func setupInputParsesFullCopiedSetupMessage() {
let payload = #"{"url":"wss://gateway.tailnet.ts.net","bootstrapToken":"tok"}"#
let message = """

View File

@@ -249,6 +249,42 @@ struct GatewayNodeSessionTests {
await gateway.disconnect()
}
@Test
func passwordTakesPrecedenceOverBootstrapToken() async throws {
let session = FakeGatewayWebSocketSession()
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "operator",
scopes: ["operator.read"],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "ui",
clientDisplayName: "iOS Test",
includeDeviceIdentity: false)
try await gateway.connect(
url: URL(string: "ws://example.invalid")!,
token: nil,
bootstrapToken: "stale-bootstrap-token",
password: "shared-password",
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
let auth = try #require(session.latestTask()?.latestConnectAuth())
#expect(auth["password"] as? String == "shared-password")
#expect(auth["bootstrapToken"] == nil)
#expect(auth["token"] == nil)
await gateway.disconnect()
}
@Test
func bootstrapHelloStoresAdditionalDeviceTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory

View File

@@ -9,6 +9,7 @@ const rootEntries = [
"src/index.ts!",
"src/entry.ts!",
"src/cli/daemon-cli.ts!",
"src/infra/kysely-node-sqlite.ts!",
"src/infra/warning-filter.ts!",
"src/infra/command-explainer/index.ts!",
bundledPluginFile("telegram", "src/audit.ts", "!"),
@@ -30,10 +31,12 @@ const bundledPluginEntries = [
const bundledPluginIgnoredRuntimeDependencies = [
"@agentclientprotocol/claude-agent-acp",
"@a2ui/lit",
"@azure/identity",
"@clawdbot/lobster",
"@discordjs/opus",
"@homebridge/ciao",
"@lit/context",
"@matrix-org/matrix-sdk-crypto-wasm",
"@mozilla/readability",
"@openai/codex",
@@ -42,6 +45,7 @@ const bundledPluginIgnoredRuntimeDependencies = [
"@zed-industries/codex-acp",
"jiti",
"json5",
"lit",
"linkedom",
"openclaw",
"pdfjs-dist",
@@ -74,6 +78,7 @@ const rootBundledPluginRuntimeDependencies = [
const config = {
ignoreFiles: [
"scripts/**",
"packages/*/dist/**",
"**/__tests__/**",
"src/test-utils/**",
"**/test-helpers/**",
@@ -134,6 +139,7 @@ const config = {
bundledPluginFile("msteams", "src/polls-store-memory.ts"),
bundledPluginFile("voice-call", "src/providers/index.ts"),
],
ignore: ["packages/*/dist/**"],
workspaces: {
".": {
entry: rootEntries,
@@ -155,6 +161,10 @@ const config = {
entry: ["index.html!", "src/main.ts!", "vite.config.ts!", "vitest*.ts!"],
project: ["src/**/*.{ts,tsx}!"],
},
"packages/sdk": {
entry: ["src/index.ts!"],
project: ["src/**/*.ts!"],
},
"packages/*": {
entry: ["index.js!", "scripts/postinstall.js!"],
project: ["index.js!", "scripts/**/*.js!"],
@@ -163,7 +173,7 @@ const config = {
// Bundled plugins often load their public surface via string specifiers in
// `index.ts` contracts, so Knip needs these convention-based entry files.
entry: bundledPluginEntries,
project: ["index.ts!", "src/**/*.ts!"],
project: ["index.ts!", "src/**/*.{js,mjs,ts}!"],
ignoreDependencies: bundledPluginIgnoredRuntimeDependencies,
},
},

View File

@@ -1,4 +1,4 @@
c93176f87a1e4576f5951b82037394c4bc9628bb6e056b6b24f96e662d6d636c config-baseline.json
92cbb12ca382f7424e7bd52df21798b10a57621f5c266909fa74e23f6cb973d7 config-baseline.core.json
cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json
6871e789b74722e4ff2c877940dac256c232433ae26b305fc6ca782b90662097 config-baseline.plugin.json
885a734aa93cf04f6c14f8d83c1e96a66a5b96705327ea2de7b2aa7314238976 config-baseline.json
074eb9a1480ff40836d98090ccb9be3465345ac4b46e0d273b7995504bbb8008 config-baseline.core.json
ed15b24c1ccf0234e6b3435149a6f1c1e709579d1259f1d09402688799b149bd config-baseline.channel.json
c4e8d8898eebc4d40f35b167c987870e426e6c82121696dc055ff929f6a24046 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
fe061b6f35adb2b152d8f48244a94d4934b335143cc5f5aebb8cc96e5ba8b287 plugin-sdk-api-baseline.json
495248d5981456192aaf7da2ed23d5951eaa6d9e59d70c716ab91c3da3620e73 plugin-sdk-api-baseline.jsonl
fecac0023b0a8de6334740483ef03500c72f3235e5b636e089bf581b00e8734a plugin-sdk-api-baseline.json
b427b2c8bddefb6c0ab4f411065adeec230d1e126a792ed30e6d0a45053dd4e3 plugin-sdk-api-baseline.jsonl

View File

@@ -19,9 +19,9 @@ Generated locale trees and live translation memory now live in the publish repo:
2. Push to `main`.
3. `openclaw/openclaw/.github/workflows/docs-sync-publish.yml` mirrors the docs tree into `openclaw/docs`.
4. The sync script rewrites the publish `docs/docs.json` so the generated locale picker blocks exist there even though they are no longer committed in the source repo.
5. `openclaw/docs/.github/workflows/translate-zh-cn.yml` refreshes `docs/zh-CN/**` once a day, on demand, and after source-repo release dispatches.
6. `openclaw/docs/.github/workflows/translate-zh-tw.yml` and `translate-ja-jp.yml` do the same for `docs/zh-TW/**` and `docs/ja-JP/**`.
7. `openclaw/docs/.github/workflows/translate-es.yml`, `translate-pt-br.yml`, `translate-ko.yml`, `translate-de.yml`, `translate-fr.yml`, `translate-ar.yml`, `translate-it.yml`, `translate-vi.yml`, `translate-nl.yml`, `translate-fa.yml`, `translate-tr.yml`, `translate-uk.yml`, `translate-id.yml`, `translate-pl.yml`, and `translate-th.yml` do the same for `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, `docs/fr/**`, `docs/ar/**`, `docs/it/**`, `docs/vi/**`, `docs/nl/**`, `docs/fa/**`, `docs/tr/**`, `docs/uk/**`, `docs/id/**`, `docs/pl/**`, and `docs/th/**`.
5. `openclaw/docs/.github/workflows/translate-all.yml` waits for `main` to settle, translates only stale or missing locale pages, and uploads per-locale artifacts.
6. The publish repo finalizer applies successful locale artifacts and pushes one aggregate `chore(i18n): refresh translations` commit.
7. A weekly `full` run reconciles every locale/page path so flaky model failures are retried without making hot docs commits wait.
## Why the split exists
@@ -66,15 +66,16 @@ Fields:
- `scripts/docs-i18n` still owns translation generation.
- Doc mode writes `x-i18n.source_hash` into each translated page.
- Each publish workflow precomputes a pending file list by comparing the current English source hash to the stored locale `x-i18n.source_hash`.
- The publish workflow precomputes a pending file list by comparing the current English source hash to the stored locale `x-i18n.source_hash`.
- If the pending count is `0`, the expensive translation step is skipped entirely.
- If there are pending files, the workflow translates only those files.
- The publish workflow retries transient model-format failures, but unchanged files stay skipped because the same hash check runs on each retry.
- The source repo also dispatches zh-CN, zh-TW, ja-JP, es, pt-BR, ko, de, fr, ar, it, vi, nl, fa, tr, uk, id, pl, and th refreshes after published GitHub releases so release docs can catch up without waiting for the daily cron.
- Locale workers retry transient model-format failures, but unchanged files stay skipped because the same hash check runs on each retry.
- Locale workers upload artifacts; the publish repo finalizer commits all successful locale outputs together.
- Published GitHub releases dispatch one aggregate translation refresh so release docs can catch up without waiting for the weekly reconciliation.
## Operational notes
- Sync metadata is written to `.openclaw-sync/source.json` in the publish repo.
- Source repo secret: `OPENCLAW_DOCS_SYNC_TOKEN`
- Publish repo secret: `OPENCLAW_DOCS_I18N_OPENAI_API_KEY`
- If locale output looks stale, check the matching `Translate <locale>` workflow in `openclaw/docs` first.
- If locale output looks stale, check the `Translate All` workflow in `openclaw/docs` first.

View File

@@ -3,6 +3,26 @@
"source": "OpenClaw",
"target": "OpenClaw"
},
{
"source": "iMessage",
"target": "iMessage"
},
{
"source": "Coming from BlueBubbles",
"target": "Coming from BlueBubbles"
},
{
"source": "BlueBubbles",
"target": "BlueBubbles"
},
{
"source": "Pairing",
"target": "配对"
},
{
"source": "Channel Routing",
"target": "频道路由"
},
{
"source": "ClawHub",
"target": "ClawHub"
@@ -31,10 +51,18 @@
"source": "Message lifecycle refactor",
"target": "消息生命周期重构"
},
{
"source": "ACP lifecycle refactor",
"target": "ACP 生命周期重构"
},
{
"source": "Channel message API",
"target": "频道消息 API"
},
{
"source": "Talk mode",
"target": "Talk 模式"
},
{
"source": "Azure Speech",
"target": "Azure Speech"
@@ -59,6 +87,10 @@
"source": "Gateway RPC reference",
"target": "Gateway RPC 参考"
},
{
"source": "Secure file operations",
"target": "安全文件操作"
},
{
"source": "Sessions",
"target": "会话"
@@ -575,6 +607,14 @@
"source": "Manage plugins",
"target": "管理插件"
},
{
"source": "Plugin path ownership",
"target": "插件路径所有权"
},
{
"source": "Docker permissions",
"target": "Docker 权限"
},
{
"source": "Plugin manifest",
"target": "插件清单"
@@ -758,5 +798,9 @@
{
"source": "/cli/config",
"target": "/cli/config"
},
{
"source": "fs-safe Cleanup Plan",
"target": "fs-safe Cleanup Plan"
}
]

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