Compare commits

..

379 Commits

Author SHA1 Message Date
Alex Knight
5fa840c26f fix: surface message-tool-only diagnostics 2026-05-22 15:14:46 +10:00
吴杨帆
e399a92e6c fix(anthropic): preserve unsafe integer tool inputs (#83063)
* fix(anthropic): preserve unsafe integer tool inputs

Fixes #47229

* docs: add Anthropic unsafe integer changelog

* fix: narrow Anthropic partial JSON type

---------

Co-authored-by: Alex Knight <aknight@atlassian.com>
2026-05-22 13:48:38 +10:00
WhatsSkiLL
36e76ef424 fix(codex): block progress-only completions [AI-assisted] (#85110)
Summary:
- The PR adds shared required-completion classification for ACP/subagent finalization, marks missing, progress-only, and delivery-exhausted completions as blocked, and adds regression tests plus a changelog entry.
- Reproducibility: yes. source-reproducible. Current main finalizes the implicated ACP and subagent success pa ... he linked issue supplies production-shaped evidence; this read-only pass did not run a live provider repro.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): preserve final completions after progress
- PR branch already contained follow-up commit before automerge: fix(codex): accept progress-prefixed final completions
- PR branch already contained follow-up commit before automerge: fix(codex): accept separator-delimited completions
- PR branch already contained follow-up commit before automerge: fix(codex): keep follow-up planning blocked
- PR branch already contained follow-up commit before automerge: fix(codex): block progress-only completions [AI-assisted]

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

Prepared head SHA: 21a1159165
Review: https://github.com/openclaw/openclaw/pull/85110#issuecomment-4513104331

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 03:43:28 +00:00
NVIDIAN
ddd3d69b86 fix(codex): unsubscribe app-server thread after runs (#84969)
Co-authored-by: ai-hpc <mail.speedy.hpc@hotmail.com>
2026-05-22 04:39:35 +01:00
Bob
ae4806ed9a feat(plugins): add embedding provider contract (#84947)
Summary:
- Merged feat(plugins): add embedding provider contract after ClawSweeper review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: chore(plugins): refresh embedding provider sdk baseline
- PR branch already contained follow-up commit before automerge: docs(plugins): document embedding provider contract
- PR branch already contained follow-up commit before automerge: fix(plugins): restore embedding providers after snapshot loads
- PR branch already contained follow-up commit before automerge: fix(plugins): resolve embedding providers from manifests
- PR branch already contained follow-up commit before automerge: fix(plugin-sdk): keep embedding provider registry mutators internal
- PR branch already contained follow-up commit before automerge: chore(plugin-sdk): refresh embedding provider API baseline

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

Prepared head SHA: 41ebd66ab4
Review: https://github.com/openclaw/openclaw/pull/84947#issuecomment-4514762026

Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-22 03:36:51 +00:00
WhatsSkiLL
0a4de3de57 [AI-assisted] fix(reply): wait for block replies before tools (#83722)
Summary:
- The branch adds an abort-aware dispatcher-idle wait after successful same-channel and direct ACP block replies, plus regression tests and a changelog entry.
- Reproducibility: yes. Current main source shows the same-channel block callback queues dispatcher delivery w ... spatcher idle, and the PR body supplies before/after diagnostic output for the tool-start ordering failure.

Automerge notes:
- PR branch already contained follow-up commit before automerge: [AI-assisted] fix(reply): wait for block replies before tools

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

Prepared head SHA: 32576209a2
Review: https://github.com/openclaw/openclaw/pull/83722#issuecomment-4480639845

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 03:32:45 +00:00
Kaspre
eb7f3b7b50 fix(agent): support explicit CLI session keys (#85121)
Summary:
- The PR adds `openclaw agent --session-key`, normalizes explicit session keys through Gateway and embedded agent execution, and updates docs, tests, and changelog.
- Reproducibility: yes. Current main's `openclaw agent` registration and gateway CLI option type lack `--sessi ... Gateway agent protocol already accepts `sessionKey`; this is source-reproducible without executing the CLI.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agent): support explicit CLI session keys

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

Prepared head SHA: 2c76dd339f
Review: https://github.com/openclaw/openclaw/pull/85121#issuecomment-4513508932

Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 03:08:25 +00:00
Vincent Koc
a4c81c6f35 fix(codex): recover final text after prompt timeout (#84993) 2026-05-22 11:02:47 +08:00
Josh Avant
b8e9ab9385 fix(codex): surface native compaction failures (#85160)
* fix(codex): surface native compaction failures

* docs: add changelog for codex compaction fix

* test: align compaction failure fixtures
2026-05-21 19:41:54 -07:00
Dallin Romney
c8a35c4645 fix: coalesce repeated idle TUI abort notices (#85167) 2026-05-21 18:57:56 -07:00
Josh Avant
577e64db63 fix: require configured subagent allowlist targets (#85154)
* fix subagent allowlists to configured agents

* add changelog for subagent allowlist fix
2026-05-21 18:53:30 -07:00
Vincent Koc
60d200f797 fix(codex): make post-tool raw assistant timeout configurable (#84974)
* fix(codex): make post-tool raw assistant timeout configurable

* docs(codex): align post-tool assistant timeout docs

* docs(changelog): move codex timeout note to unreleased

---------

Co-authored-by: 0x505badc0de <32790662+rozmiarD@users.noreply.github.com>
2026-05-22 09:39:38 +08:00
clawsweeper[bot]
7f4bd454fe fix(agents): preserve accepted spawn terminal success (#85135)
Summary:
- The branch adds accepted `sessions_spawn` tracking through embedded Pi subscribe, runner, fallback, replay, lifecycle, tests, deadcode allowlist, and changelog surfaces.
- Reproducibility: yes. at source level. Current main documents accepted `sessions_spawn` results but the pre- ...  and classifier paths do not carry that accepted child-run fact into incomplete-turn or fallback decisions.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(qa-lab): allow codex fixtures in deadcode
- PR branch already contained follow-up commit before automerge: fix(agents): preserve accepted spawn terminal success

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

Prepared head SHA: 0f6d92b8cd
Review: https://github.com/openclaw/openclaw/pull/85135#issuecomment-4513861326

Co-authored-by: samzong <samzong.lu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 01:16:41 +00:00
Josh Avant
221f5349b5 fix: redact denied exec failure params (#85140)
* fix exec failure log redaction

* docs: add exec redaction changelog

* test: satisfy redaction lint
2026-05-21 18:10:50 -07:00
Gio Della-Libera
ee9813f478 fix(cli): keep nodes json stdout clean (#84423)
Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-21 18:05:11 -07:00
Josh Avant
cbe68ba1a1 Fix inherited XDG env for exec subprocesses (#85139)
* fix exec xdg env inheritance

* chore changelog xdg env fix
2026-05-21 18:01:38 -07:00
Dallin Romney
d391434f4e perf: skip dts for local launcher builds (#85142) 2026-05-21 17:58:48 -07:00
Agustin Rivera
4faeb378ee fix(changelog): record provider setup trust fix (#81069) 2026-05-21 17:48:24 -07:00
Josh Avant
1f9ebb9dda Fix Matrix configured two-person room routing (#85137)
* Fix Matrix configured room DM routing

* Add Matrix room routing changelog
2026-05-21 17:40:17 -07:00
Michael Appel
0aabaebba1 fix: address issue (#81069) 2026-05-21 17:39:48 -07:00
Kevin Lin
6fe3088bc6 docs: refactor plugin bundle docs 2026-05-21 17:34:42 -07:00
Kevin Lin
7f499643b2 enhance(slack): deliver native plugin approvals (#85062)
* fix(slack): deliver native plugin approvals

* fix(slack): deliver plugin approvals with native UI

* docs: defer slack plugin approval docs
2026-05-21 17:31:06 -07:00
Kevin Lin
777a113973 fix(codex): await computer use elicitation bridge (#85117)
* fix(codex): bridge computer use elicitations

* fix(codex): preserve computer use approval boundary

* fix(codex): await app-server elicitation bridge
2026-05-21 17:17:46 -07:00
Gio Della-Libera
bc9e601491 fix: allow provider timeout overlays (#83990)
* fix: allow provider timeout overlays

* test: fix provider overlay fixture types
2026-05-21 17:10:32 -07:00
Firas Alswihry
0df9f297b6 fix(gateway): mirror source message sends into transcript (#84837)
Co-authored-by: Firas Alswihry <itzfiras@gmail.com>
2026-05-22 01:08:00 +01:00
Vincent Koc
f015c3ff52 test(qa-lab): tag live-only runtime sentinels 2026-05-22 07:42:09 +08:00
Vincent Koc
15a0156a8c fix(update): reject openclaw source package targets 2026-05-22 07:35:57 +08:00
Vincent Koc
fad1c8a071 test(qa-lab): add long-context watchdog scenario 2026-05-22 07:16:35 +08:00
Peter Steinberger
e2c92be90b chore(release): bump version to 2026.5.21 2026-05-22 00:09:45 +01:00
Josh Avant
ba06376c79 fix: harden codex sandbox execution
Harden the Codex app-server native execution bridge for OpenClaw sandboxed runs. The change keeps core sandbox policy in OpenClaw while exposing the process, filesystem, and HTTP relay behavior Codex needs inside a scoped exec server.

The large exec-server/test files were split into focused modules before landing, and the PR was rebased onto current main with focused tests, Testbox changed checks, CI, and Codex autoreview green.

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-21 23:47:32 +01:00
Andy Ye
c2004fe662 fix(agents): surface blocked subagent completions (#80886)
Summary:
- The PR adds shared blocked-liveness normalization, applies it to agent.wait, gateway dedupe, subagent registry, and announcement paths, and adds regression tests plus a changelog entry.
- Reproducibility: yes. from source inspection: current main accepts blocked lifecycle/wait metadata as ok thr ...  gateway wait and registry completion paths. I did not run a live provider overflow in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): normalize blocked wait completions
- PR branch already contained follow-up commit before automerge: fix(agents): surface blocked subagent completions

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

Prepared head SHA: 224785c8a6
Review: https://github.com/openclaw/openclaw/pull/80886#issuecomment-4427552621

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-21 22:34:21 +00:00
Peter Steinberger
373e3fc719 fix: harden autoreview target handling 2026-05-21 23:26:49 +01:00
Peter Steinberger
08f66133ef ci(release): link durable release evidence 2026-05-21 23:24:35 +01:00
Dallin Romney
dca9cecaee perf(plugins): thread install records through plugin load options (#85026)
Adds installRecords to PluginLoadOptions and PluginRuntimeLoadContext so
callers that already hold a PluginMetadataSnapshot can pass the snapshot's
in-memory records instead of forcing each downstream loader to re-read
installs.json. resolvePluginRuntimeLoadContext extracts the records from
the snapshot via extractPluginInstallRecordsFromInstalledPluginIndex,
buildPluginRuntimeLoadOptionsFromValues forwards them, and the setup +
runtime provider load paths in providers.runtime.ts pass them through
from params.pluginMetadataSnapshot. resolvePluginLoadCacheContext uses
the threaded records (falling back to the sync read) and
loader-provenance now uses params.installRecords ?? sync-read instead of
always reading and overlaying.
2026-05-21 15:24:31 -07:00
Peter Steinberger
d4c6bdfeae docs: credit per-agent lean changelog entry 2026-05-21 23:20:02 +01:00
Andy Ye
6b5eba1f43 fix(cli): preserve numeric config set record keys (#83769)
Merged via squash.

Prepared head SHA: cb55b4a40d
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-22 01:09:15 +03:00
xiaotian
1b77145687 fix(agents): tolerate in-process session writes during prompt release (#84250)
Merged via squash.

Prepared head SHA: 33f88febc3
Co-authored-by: tianxiaochannel-oss88 <272340815+tianxiaochannel-oss88@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-21 18:06:12 -04:00
Josh Avant
7cda26aa6c Handle Codex turns missing completion (#85107)
* fix(codex): handle missing turn completion

* docs: add changelog for Codex completion fix
2026-05-21 15:02:17 -07:00
Peter Steinberger
61150870e2 test(parallels): allow npm smoke host ip override 2026-05-21 22:53:56 +01:00
Peter Steinberger
75a011977d fix(qa): accept Telegram no-reply timeout details 2026-05-21 22:45:25 +01:00
Peter Steinberger
8aabb79a83 test(release): respect stable ClawHub channel 2026-05-21 22:45:25 +01:00
Peter Steinberger
cabb55380f feat(plugin-sdk): add session entry workflow helpers
Co-authored-by: Eduardo Piva <efpiva@gmail.com>
2026-05-21 22:41:45 +01:00
Josh Avant
0ab1449215 Fix Discord session recovery abort ownership (#85100)
* fix auto-reply abort ownership

* add changelog for #85100
2026-05-21 14:34:18 -07:00
Peter Steinberger
c5e8bd08b8 fix(whatsapp): remove baileys logger patch 2026-05-21 22:33:13 +01:00
openclaw-release-bot
de5f1fa99a chore(release): update appcast for 2026.5.20 2026-05-21 21:25:03 +00:00
Peter Steinberger
26e64bda14 fix(whatsapp): update baileys dependency 2026-05-21 22:20:09 +01:00
100menotu001
f52db027a0 fix(discord): log component registry error details
Log structured details when Discord persistent component registry state falls back after a store failure.

- Format Error name, message, stack, and cause metadata at the Discord registry warning call site.
- Forward plugin runtime logger metadata to the underlying child logger.
- Add focused regression coverage for the Discord fallback warning and runtime logging adapter.
- Add changelog credit for @100menotu001.

Fixes #84185.

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
Co-authored-by: Craig <froelich@craigs.mac.studio.froho>
2026-05-21 22:13:14 +01:00
Thomas Krohnfuß
98af51748d fix(channels): hint at when bundled channel module is missing (#76974)
Summary:
- The PR adds a bundled-channel load-error formatter, wires it into the bundled-channel warning paths, adds focused tests, and updates the changelog.
- Reproducibility: yes. source-level: current main logs bundled-channel load failures with bare `formatErrorMe ... cause`. The contributor's terminal proof demonstrates the same wrapped-error shape before and after the PR.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(channels): walk error cause chain to detect missing bundled modules
- PR branch already contained follow-up commit before automerge: docs(changelog): add Unreleased Fixes entry
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into fix/bundled-channel-l…
- PR branch already contained follow-up commit before automerge: Merge branch 'main' into fix/bundled-channel-load-doctor-hint

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

Prepared head SHA: 416a8a2e77
Review: https://github.com/openclaw/openclaw/pull/76974#issuecomment-4367336485

Co-authored-by: BSG2000 <github@hsu.hamburg>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: BSG2000 <BSG2000@users.noreply.github.com>
Co-authored-by: BSG2000 <thomas.krohnfuss@stud.th-luebeck.de>
Co-authored-by: Thomas Krohnfuß <BSG2000@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 21:03:49 +00:00
100menotu001
0212188cb6 fix(discord): preserve reusable presentation buttons
Preserve `reusable` for portable message presentation buttons and carry it through Discord component registration so repeatable callbacks stay available after a successful interaction.

Also keeps `reusable` through legacy presentation-to-interactive conversion and documents the user-visible change in the changelog.

Verification:
- `pnpm test src/interactive/payload.test.ts extensions/discord/src/shared-interactive.test.ts extensions/discord/src/components.test.ts -- --reporter=verbose`
- `git diff --check`
- `AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode local`
- PR CI at `52f25221b3e01f3255d8df37df73d0357ab7410b`: all completed checks green/skipped/neutral except pending CodeQL `Security High (mcp-process-tool-boundary)` at time auto-merge was armed.

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
2026-05-21 22:02:16 +01:00
clawsweeper[bot]
5f5e3b4511 Fix Ollama cloud API key discovery (#85091)
Summary:
- The branch teaches Ollama discovery to use resolved `discoveryApiKey` values for non-local cloud providers, preserves local marker auth, and adds focused provider-discovery regressions plus a changelog entry.
- Reproducibility: yes. from source inspection: current main can return the `OLLAMA_API_KEY` marker instead of ... ential for documented Ollama Cloud config. I did not run executable tests because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: ci: allowlist qa lab fixtures
- PR branch already contained follow-up commit before automerge: Fix Ollama cloud API key discovery
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8503…

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

Prepared head SHA: cb6b658819
Review: https://github.com/openclaw/openclaw/pull/85091#issuecomment-4512647237

Co-authored-by: Anup Sharma <anupnewsmail@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 20:54:59 +00:00
Peter Steinberger
48bb3b0a74 fix(config): refresh Discord component TTL metadata (#84189) (thanks @100menotu001) 2026-05-21 21:54:30 +01:00
OpenClaw Contributor
4c6fe55d20 fix(discord): cap component ttl at one day 2026-05-21 21:54:30 +01:00
OpenClaw Contributor
814386a10b fix(discord): deep merge agent component config 2026-05-21 21:54:30 +01:00
OpenClaw Contributor
c17a48ccfd docs(discord): document component ttl config 2026-05-21 21:54:30 +01:00
OpenClaw Contributor
9a4fb3ed7e fix(config): refresh Discord component ttl metadata 2026-05-21 21:54:30 +01:00
OpenClaw Contributor
ee915cfede fix(discord): allow component registry ttl override 2026-05-21 21:54:30 +01:00
Gio Della-Libera
8961eae3f0 fix(cli): reject invalid node run port (#84307)
Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-21 13:47:28 -07:00
martingarramon
7f4462e5c0 fix(agents): classify auth HTML provider responses (#79900)
Merged via squash.

Prepared head SHA: b00513414d
Co-authored-by: martingarramon <263922628+martingarramon@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-21 23:28:53 +03:00
Super Zheng
01d95b9757 fix(gateway): allow bearer-auth session history reads (#81815)
Merged via squash.

Prepared head SHA: eb49667324
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
2026-05-21 13:23:17 -07:00
Peter Steinberger
504f0dfa36 fix(installer): handle headless onboarding tty 2026-05-21 21:20:52 +01:00
Super Zheng
b77f36fb1c fix(exec): protect pathPrepend against posix login-shell RC overrides (#81403)
Merged via squash.

Prepared head SHA: 874fa90aa9
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
2026-05-21 13:19:41 -07:00
Super Zheng
9b7e431b89 refactor(gateway): remove unused readLastMessagePreviewFromTranscript helper (#84427)
Merged via squash.

Prepared head SHA: 257aab2d18
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
2026-05-21 13:17:05 -07:00
Super Zheng
faf96ff99b test: fix environment sensitivity in resolveNpmCommandInvocation test (#83405)
Merged via squash.

Prepared head SHA: b2c2e9a694
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
2026-05-21 13:15:47 -07:00
Dallin Romney
4399eee6e0 fix(auth): load legacy Codex OAuth sidecars in embedded secrets-runtime loaders (#85074)
The auto-migration introduced in #83312 only fires when a credential is loaded
via a path that reads its sidecar tokens. The OAuth refresh manager's internal
loader does (so direct CLI inference works and self-heals on first refresh).

The embedded runner's secrets-runtime loaders did not:

  - loadAuthProfileStoreForSecretsRuntime
  - loadAuthProfileStoreWithoutExternalProfiles
  - ensureAuthProfileStoreWithoutExternalProfiles

All three opted out of sidecar resolution. So for an upgraded user with a
legacy oauthRef-backed openai-codex profile, the credential loaded with no
access/refresh material, evaluateStoredCredentialEligibility marked it
ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider
threw "No API key found for provider 'openai-codex'" before the OAuth manager
(and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded
turns broke — only doctor-or-bust would fix it.

Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true
(matching loadStoredOAuthRefreshStore). The existing #83312 refresh-and-rewrite
then fires on the first embedded turn for these users and persists tokens
inline, removing the legacy sidecar from disk on the next doctor pass.

Cherry-picked and squashed from PR #84752 (commits 85f36e8d2b and
4624e34c06). Comments noting local-fork bookkeeping stripped per repo policy.

Co-authored-by: Will <totalsolutionspm@gmail.com>
2026-05-21 13:07:49 -07:00
litang9
016c34ff1d Fix/codex deactivated workspace failover (#55893)
Merged via squash.

Prepared head SHA: 3aa770fa84
Co-authored-by: litang9 <141409885+litang9@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-21 22:47:29 +03:00
Peter Steinberger
1d5b5db4d2 fix(codex): demote plugin thread eligibility log 2026-05-21 20:12:02 +01:00
Peter Steinberger
aef8d1771d fix(models): reset warmed provider auth on hot reload 2026-05-21 20:09:51 +01:00
Sarah Fortune
7ddcca6c77 address review v3: invalidate prepared map on auth-profile logout + defer plugin-reload rewarm
P1 (auth-profile logout): invalidateModelAuthStatusCache now also clears
the prepared provider-auth map, and the models.auth.logout handler fires
a rewarm against the current config. Without this, removing a provider's
auth profiles left the warmed 'true' answer in the map until restart,
so /models and pickers kept advertising the removed provider.

P1 (plugin-reload ordering): the previous version fired the rewarm
inline with the clear, before plan.reloadPlugins() ran. The warmer
reads plugin metadata and synthetic-auth hooks, so it published the
pre-reload runtime's answers. Moved the rewarm to fire after the
plugin-reload block completes, so it reads the new plugin runtime.

The early clear still happens upfront so callers don't keep seeing the
pre-reload answer during the reload window.
2026-05-21 20:09:51 +01:00
Sarah Fortune
c452a1e7e5 address review v2: workspace scope, warm generation guard, plugin reload trigger
ClawSweeper P1 + P2 findings on the prior review-fix commit.

- [P1] hasAuthForModelProvider now also checks workspaceDir against the
  warm's snapshot value. The warmer uses resolveDefaultAgentWorkspaceDir,
  but per-agent picker calls (buildModelsProviderData →
  resolveVisibleModelCatalog → createProviderAuthChecker) thread an
  agent-specific workspaceDir, and provider env/synthetic-auth
  resolution depends on it. Without this check the picker for agent B
  would silently reuse agent A's warmed answer.
- [P1] warmCurrentProviderAuthState now claims a generation counter at
  the start of its work and only publishes the new state if the
  generation hasn't been bumped (by a concurrent clear or another
  warm). Closes the race where a slow startup warm could overwrite a
  newer reload-driven rewarm with stale data.
- [P2] Reload handler now also clears and rewarms the prepared map
  when plugins.* config paths change. Provider auth can come from
  plugin env vars and plugin synthetic-auth wiring, so plugin hot
  reloads must invalidate the auth state too — not just model config
  paths.

Test: new case asserting a non-default workspaceDir caller bypasses the
prepared map and falls through to compute.
2026-05-21 20:09:51 +01:00
Sarah Fortune
01087cb936 address review: scope short-circuit by caller auth context + rewarm on reload
Two fixes flagged by ClawSweeper.

P1 — hasAuthForModelProvider now only short-circuits via the prepared
map when the caller's scope matches the warmer's (broad discovery, no
agentDir/env/store override). Read-only gateway model lists pass
runtimeAuthDiscovery: false, which the visibility helper maps to
discoverExternalCliAuth: false and allowPluginSyntheticAuth: false; the
prepared broad answer was previously masking that narrower intent. Now
those callers fall through to compute the narrow answer.

P2 — server-reload-handlers now also schedules a rewarm right after
clearing the prepared state on model-config reload, so long-lived
gateways don't regress to per-call discovery between reload and the
next restart.

Test: extends model-provider-auth.test.ts with a scope-narrowing case
asserting the prepared answer is bypassed when the caller passes
discoverExternalCliAuth: false / allowPluginSyntheticAuth: false.
2026-05-21 20:09:51 +01:00
Sarah Fortune
180cecda85 test(model-provider-auth): cover prepared-state short-circuit and clear
Asserts hasAuthForModelProvider returns the warmed answer for providers
in the prepared map and skips the compute path, and that
clearCurrentProviderAuthState restores fall-through to compute.
2026-05-21 20:09:51 +01:00
Sarah Fortune
4f80cc1943 perf(models): pre-warm provider auth state at gateway startup
Eliminates the per-call auth-filter loop that every /models invocation
(Discord/Telegram pickers, CLI, status commands) was paying — 30 unique
providers × ~600 ms each of plugin-runtime / external-CLI / auth-profile
discovery, done fresh on every call (~20 s per call).

warmCurrentProviderAuthState builds a provider->boolean map once at
gateway startup against a single AuthProfileStore scoped to every
candidate provider, and hasAuthForModelProvider consults the prepared
map first and short-circuits. The map is invalidated on config reload
alongside resetModelCatalogCache so the next read after a relevant
config change rewarms.

Per /models: ~20,569 ms → ~5 ms (~4,100×).
One-time startup warm cost: ~49 s (cold catalog + auth sweep), logged
via gateway log.info on completion.
2026-05-21 20:09:51 +01:00
Dallin Romney
ebd8b00cc3 fix(qa-lab): rename codex lifecycle fixtures to match knip ignore pattern (#85066)
knip's deadcode-unused-files check ignores fixtures matching **/*.fixture.ts
(dot before "fixture"). The codex lifecycle fixtures landed in bbf3eec786
as auth-profile-fixture.ts and codex-plugin-fixture.ts (hyphen), so knip
flagged them as unexpected unused files and CI's check-dependencies job
has been failing on main since then. Rename to auth-profile.fixture.ts
and codex-plugin.fixture.ts and update the lifecycle test, the fixture
cross-import, and the six qa/scenarios markdown files that reference
them by path and qaImport specifier.
2026-05-21 11:56:59 -07:00
Vincent Koc
b25a0d013b test(gateway): relax e2e node status waits 2026-05-22 02:25:30 +08:00
zhang-guiping
7d5afcbb3f fix #84745: scope Google preview model normalization to Google providers only (#84762)
Summary:
- The branch scopes config-time Google Gemini preview model normalization to Google providers or nested `google/` proxy suffixes, adds model-picker regression coverage, and adds a changelog entry.
- Reproducibility: yes. by source inspection. Current main sends every provider suffix through the Google prev ... i-3-flash` deterministically becomes `litellm/gemini-3-flash-preview`; I did not run a live cron preflight.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(config): scope Google preview model normalization to Google provi…
- PR branch already contained follow-up commit before automerge: fix #84745: scope Google preview model normalization to Google provid…
- PR branch already contained follow-up commit before automerge: fix #84745: preserve proxy Google model normalization

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

Prepared head SHA: c59163c809
Review: https://github.com/openclaw/openclaw/pull/84762#issuecomment-4504169062

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-21 17:45:57 +00:00
Vincent Koc
bbf3eec786 test(qa-lab): cover codex plugin lifecycle fixtures 2026-05-22 01:42:25 +08:00
Vincent Koc
ec0cf9af04 fix(tests): allow slower kitchen sink installs 2026-05-22 01:40:18 +08:00
Vincent Koc
46c8864048 revert(qa-lab): remove scenario github traceability metadata 2026-05-22 01:27:29 +08:00
Vincent Koc
23c58081d0 fix(docker): prune omitted plugin runtime deps 2026-05-22 01:08:48 +08:00
Dallin Romney
205c595b13 fix(auth): skip OAuth refresh adapter when credential has no refresh token (#85028)
OAuth credentials that loaded without their sidecar material (no access, no
refresh) would still enter the refresh path inside the per-profile lock,
where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s).
That made the eventual "No API key found for provider" surface to the user
only after a long stall, even though the resolver had no usable material to
attempt with.

Short-circuit doRefreshOAuthTokenWithLock to return null when there is no
refresh token to use, after the in-lock main-store adoption and external
bootstrap-credential checks have already had a chance to recover.

Thanks @romneyda.
2026-05-21 10:00:29 -07:00
Vincent Koc
178e510aae test(qa-lab): cover update package sentinel 2026-05-22 00:59:02 +08:00
clawsweeper[bot]
7f943b5d8f fix(json): retry on transient File changed during read race condition (#85029)
Summary:
- The PR wraps the async JSON file readers in `src/infra/json-files.ts` with bounded retries for fs-safe `File changed during read` races, adds regression tests, and adds a changelog entry.
- Reproducibility: yes. Source inspection shows fs-safe throws `File changed during read`, current main re-exp ... R proof includes before/after gateway logs; I did not run a new live race harness in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(json): preserve strict reader types (Promise<T> for readJson/read…
- PR branch already contained follow-up commit before automerge: test(json): add retry-success and retry-exhaustion coverage
- PR branch already contained follow-up commit before automerge: fix(json): resolve lint warnings (prefer-exponentiation-operator, cur…
- PR branch already contained follow-up commit before automerge: fix(json): retry on transient File changed during read race condition

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

Prepared head SHA: 00602a1c03
Review: https://github.com/openclaw/openclaw/pull/85029#issuecomment-4510494668

Co-authored-by: samson1357924 <98934496+samson1357924@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 16:56:46 +00:00
clawsweeper[bot]
5955f354f7 fix(status): add gateway delivery health telemetry (#85016)
Summary:
- This replacement PR adds inbound delivery diagnostic events, gateway status counters and warnings, transport ... ut, Prometheus/OpenTelemetry metrics, docs, changelog, and regression coverage for gateway delivery health.
- Reproducibility: no. high-confidence live reproduction of the original Feishu failure was run here. Source i ... ch/turn telemetry, and the source PR supplies after-fix live output for the connected WebChat gateway path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(types): restore PR conflict resolution type checks

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

Prepared head SHA: 6ffe08a9c7
Review: https://github.com/openclaw/openclaw/pull/85016#issuecomment-4510224436

Co-authored-by: Andi Liao <liaoandi95@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 16:55:29 +00:00
Vincent Koc
efb7e4742f test(qa-lab): trace scenario issue evidence 2026-05-22 00:51:32 +08:00
clawsweeper[bot]
b33deb4159 fix(sessions): preserve compatible auth overrides (#85014)
Summary:
- This replacement branch preserves compatible session auth profile overrides during `sessions.patch` model ch ... d/cross-provider regression coverage, and updates related doctor/Mantis test assertions plus the changelog.
- Reproducibility: yes. by source inspection: current main’s `sessions.patch` model branch calls `applyModelOv ... d helper clears auth fields unless preservation is requested. I did not run tests in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(mantis): align telegram proof evidence comment
- PR branch already contained follow-up commit before automerge: fix(sessions): preserve provider auth aliases
- PR branch already contained follow-up commit before automerge: fix(sessions): guard unprefixed auth overrides
- PR branch already contained follow-up commit before automerge: fix(doctor): preserve params prototype semantics
- PR branch already contained follow-up commit before automerge: fix(sessions): preserve compatible auth overrides

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

Prepared head SHA: 64a07393d5
Review: https://github.com/openclaw/openclaw/pull/85014#issuecomment-4510194125

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 16:17:50 +00:00
Vincent Koc
652712e0ad ci(qa): publish soak parity artifacts 2026-05-22 00:08:51 +08:00
Vincent Koc
9f2c0a80b4 fix(qa): keep searchable tool coverage report-only 2026-05-21 23:55:35 +08:00
Vincent Koc
da1925cb67 test(e2e): isolate kitchen sink rpc gateway 2026-05-21 23:54:33 +08:00
clawsweeper[bot]
277a4b6952 fix(ollama): allow Orb host local auth (#84999)
Summary:
- The PR adds Docker/OrbStack host aliases to Ollama local-auth classification, keeps those aliases out of loopback-only discovery suppression, adds regression tests, and updates the changelog.
- Reproducibility: yes. The linked report gives a concrete v2026.5.19 config and error, and current main source shows host.orb.internal is not classified as local for ollama-local marker auth.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(ollama): allow Orb host local auth

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

Prepared head SHA: cb82dcf522
Review: https://github.com/openclaw/openclaw/pull/84999#issuecomment-4509786332

Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-21 15:37:42 +00:00
Firas Alswihry
229323d37a test(qa-lab): add personal failure recovery scenario 2026-05-21 23:22:35 +08:00
Vincent Koc
0e6f314dbb ci: tune crabbox developer image config 2026-05-21 23:21:35 +08:00
Vincent Koc
cf0657852f feat(qa-lab): add jsonl replay harness 2026-05-21 23:03:51 +08:00
Neerav Makwana
66dcc4ee8f fix(codex): beta blocker - keep context engine on canonical session key (#84954)
Merged via squash.

Prepared head SHA: 6cdccaa007
Co-authored-by: neeravmakwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-21 11:01:35 -04:00
Peter Steinberger
1b1580cbc3 chore(release): refresh generated baselines 2026-05-21 15:52:04 +01:00
Sally O'Malley
e72f601925 fix(openshell): use NVIDIA CLI contract
Remove the unrelated npm openshell dependency and keep the OpenShell sandbox backend pointed at the NVIDIA CLI command contract.
2026-05-21 22:51:57 +08:00
Peter Steinberger
94b6d9f8b2 docs(release): prefer 1Password provider preflight 2026-05-21 15:46:34 +01:00
Gio Della-Libera
6dbd5bd446 Policy: add model, network, and MCP conformance checks (#80783)
* feat(policy): add model network and mcp conformance checks

* fix(policy): validate conformance rule shapes

* fix(policy): quote dynamic evidence paths

* fix(policy): scan per-agent model maps

* fix(policy): normalize model provider conformance
2026-05-21 07:27:16 -07:00
Vincent Koc
2bb00f6726 fix(agents): fence embedded session writes 2026-05-21 22:17:48 +08:00
Peter Steinberger
95eac52e92 test: update command auth expectations 2026-05-21 15:14:48 +01:00
Peter Steinberger
e0b53cae41 docs: remove stale owner tool wording 2026-05-21 15:14:48 +01:00
Peter Steinberger
02182d5a30 refactor: remove sender owner tool gating 2026-05-21 15:14:48 +01:00
Rubén Cuevas
159b3002e4 fix(xai): keep OAuth URL clickable (#84927) 2026-05-21 07:08:34 -07:00
Jesse Merhi
a901396ad1 Fix stale WebChat typing indicator after terminal session patch (#84565)
Summary:
- The branch clears WebChat local run and stream state when terminal session reconciliation completes the acti ...  session events, adjusts deferred history/queue flushing, adds regression tests, and updates the changelog.
- Reproducibility: yes. with high confidence from source inspection and PR evidence. Current main can apply a  ...  PR body, recording, and regression shape show the stale WebChat typing state being cleared by this branch.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: harden webchat session run reconciliation

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

Prepared head SHA: 89cca8dd01
Review: https://github.com/openclaw/openclaw/pull/84565#issuecomment-4498262223

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: jesse-merhi
2026-05-21 14:05:58 +00:00
Peter Steinberger
c49647ee23 docs: document rejected autoreview findings 2026-05-21 14:55:07 +01:00
Vincent Koc
db606a8475 docs(changelog): note VAPID subject fix
Add the missing changelog entry for the landed Web Push VAPID subject fix and refresh the config docs baseline hash to match the Node 24 check environment.
2026-05-21 21:54:47 +08:00
Shakker
6ccca4ae95 docs: add plugin registry reuse changelog 2026-05-21 13:41:29 +01:00
Shakker
b248b4816b test: cover dispatch registry reuse caller 2026-05-21 13:41:29 +01:00
ai-hpc
d2ad7d6b4c perf(plugins): reuse compatible gateway startup registry 2026-05-21 13:41:29 +01:00
Vincent Koc
bde07ddb15 fix(tests): wrap kitchen sink pnpm runner 2026-05-21 19:24:56 +08:00
Vincent Koc
04061bc801 fix(agents): cap heartbeat context hint fallback 2026-05-21 19:01:00 +08:00
Vincent Koc
88c49f9e68 chore(deadcode): dedupe repeated helpers 2026-05-21 18:47:09 +08:00
Frank Yang
f39f56a096 perf(cli): cache stable subcommand help (#84786)
Serve stable doctor, gateway, models, and plugins parent help from startup metadata while preserving strict argv validation and version precedence.

Verification:
- pnpm test src/cli/run-main.test.ts src/cli/run-main.exit.test.ts test/scripts/write-cli-startup-metadata.test.ts -- --reporter=default
- pnpm check:changed
- GitHub required checks passed
2026-05-21 18:01:32 +08:00
WhatsSkiLL
2000227e9e fix(ollama): preserve tool call ids [AI-assisted] (#84855)
Summary:
- The PR preserves native Ollama tool-call IDs through ingest and replay, opts native Ollama out of strict replay ID sanitization, and adds focused regression tests plus a changelog entry.
- Reproducibility: yes. Current main drops native Ollama tool-call IDs on ingest and replay and applies strict ...  PR discussion includes a maintainer-side before/after probe that reproduced the source-level failure path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(ollama): keep native tool ids through replay

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

Prepared head SHA: bb9fef7d4c
Review: https://github.com/openclaw/openclaw/pull/84855#issuecomment-4505423891

Co-authored-by: IWhatsskill <whatsskilll@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-21 09:51:00 +00:00
Peter Steinberger
f43e83c937 fix: align remaining copyright notice 2026-05-21 10:47:54 +01:00
tanshanshan
8a8f9dc8cb fix(config): append numeric bound hints to ceiling/floor validation errors (#84852)
* fix(config): append numeric bound hints to ceiling/floor validation errors

When a config value exceeds a schema-enforced ceiling or falls below a
floor, the error message now includes the constraint explicitly:
  - Inclusive: `(maximum: 20)` / `(minimum: 0)`
  - Exclusive: `(must be less than 5)` / `(must be greater than 0)`

This matches the clarity that enum/union rejections already get via
`(allowed: …)` hints, and avoids the misleading "minimum: 0" wording
that previous attempts produced for `.positive()` / `.gt(0)` rejections.

Only numeric-origin `too_big`/`too_small` issues are enriched; string,
array, and file-size origins are left unchanged.

Fixes #52500

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(config): update maxFileBytes test for numeric bound hint

The test snapshot for `logging.maxFileBytes: 0` rejection now includes
the `(must be greater than 0)` hint appended by the numeric bound
enrichment added in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(config): guard nullable record in appendNumericBoundHint call

ClawSweeper P1: `record` from `toIssueRecord()` can be null, but
`appendNumericBoundHint` expects a non-null `UnknownIssueRecord`.
Guard with a ternary so the original message is returned when record
is null (which only happens for malformed/empty issues that already
produce generic "Invalid input" messages).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: tanshanshan <tanshanshan@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:44:29 +08:00
Vincent Koc
0fb1de5f73 fix(qa): enable private self-check runtime 2026-05-21 17:42:42 +08:00
tanshanshan
b7f9bf5a5c fix(diffs): replace iconMarkup string with ToolbarIconName enum to el… (#83955)
* fix(diffs): replace iconMarkup string with ToolbarIconName enum to eliminate XSS sink

Replace createToolbarButton's iconMarkup: string parameter with icon: ToolbarIconName,
a union of known icon names. SVG generation moves into a sealed toolbarIconSvg map so
innerHTML only receives compile-time-known strings. The old splitIcon/unifiedIcon/
wrapIcon/backgroundIcon/themeIcon functions are removed; callers now pass icon name
literals instead of raw markup strings.

Closes #83918

* fix(diffs): remove jsdom dependency from viewer-client test

Use source file string analysis instead of jsdom to avoid missing
@types/jsdom declaration error in check-test-types CI job.

* fix(diffs): restore wrap icon arrow segment in ToolbarIconName map

The wrap-on and wrap-off SVG paths were missing the original wrap arrow
segment (M14 6h-4V5h4.5...). Restore the exact original path data and
rebuild the viewer runtime bundle.

* build(diffs): refresh viewer runtime after rebase

---------

Co-authored-by: tanshanshan <tanshanshan@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-21 17:39:01 +08:00
Peter Steinberger
3260da003d fix: update mac copyright owner 2026-05-21 10:33:49 +01:00
Vincent Koc
ec67290e0b fix(agents): normalize openapi tool schemas 2026-05-21 17:29:32 +08:00
NianJiu
c89632b647 fix(memory): stop recall tracking when dreaming is disabled
Fixes #84436.

- Gate memory search recall-tracking side effects on the memory-core dreaming master switch.
- Preserve normal search results and enabled-dreaming tracking behavior.
- Add CLI and tool regression coverage, plus the maintainer changelog entry.

Verification:
- node scripts/crabbox-wrapper.mjs run -- --provider blacksmith-testbox --blacksmith-org openclaw --blacksmith-workflow .github/workflows/ci-check-testbox.yml --blacksmith-job check --blacksmith-ref main --idle-timeout 90m --ttl 240m --timing-json --shell -- "CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 OPENCLAW_TESTBOX=1 OPENCLAW_TESTBOX_REMOTE_RUN=1 pnpm check:changed" (tbx_01ks4watvb6apj9wtdx46a1r31)
- GitHub checks passed on 148fa6595e, including Real behavior proof and CI.
2026-05-21 17:13:49 +08:00
clawsweeper[bot]
5813fa4584 fix(diagnostics-otel): suppress exporter rejection crashes (#84881)
Summary:
- The PR adds a diagnostics-otel scoped unhandled-rejection handler for nested OTLPExporterError values, unregisters it on stop/restart, adds regression tests, and adds a changelog entry.
- Reproducibility: yes. The source path is high-confidence: current main has no OTLPExporterError-specific dia ... ror for non-retryable OTLP HTTP failures; I did not run a live collector shutdown in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(diagnostics-otel): avoid stale exporter handler
- PR branch already contained follow-up commit before automerge: fix(diagnostics-otel): suppress exporter rejection crashes

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

Prepared head SHA: e19c06c992
Review: https://github.com/openclaw/openclaw/pull/84881#issuecomment-4506249586

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 09:01:32 +00:00
Frank Yang
233765b361 perf: speed up secrets and nodes help startup (#84818)
Merged via squash.

Prepared head SHA: d65ae1bd58
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-05-21 16:51:57 +08:00
Shakker
e3b77d6d2c docs: add PDF timeout changelog 2026-05-21 09:31:31 +01:00
luoyanglang
248169b646 fix(pdf): bound remote body reads 2026-05-21 09:31:31 +01:00
samzong
88fe39bc8b [Fix] Reject slow node event sends (#84387)
Merged via squash.

Prepared head SHA: b459f9ea57
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-05-21 16:22:16 +08:00
Vincent Koc
43c6c260de fix(doctor): detect Codex bwrap namespace denials
Fixes #83018.
2026-05-21 16:13:53 +08:00
Jason (Json)
4a360ac1cc fix(update): prune stale local bundled plugin shadows
Summary:\n- prune stale local bundled plugin path records during update/doctor repair\n- keep current, same-version, versionless, source-checkout, and arbitrary local path records preserved\n- add changelog and deterministic sort comparator cleanup\n\nVerification:\n- node scripts/run-vitest.mjs src/plugins/contracts/boundary-invariants.test.ts src/plugins/stale-local-bundled-plugin-install-records.test.ts src/cli/update-cli/post-core-plugin-convergence.test.ts src/commands/doctor-plugin-registry.test.ts\n- node scripts/run-oxlint-shards.mjs --threads=8\n- ./node_modules/.bin/oxfmt --check --threads=1 CHANGELOG.md src/plugins/stale-local-bundled-plugin-install-records.ts src/commands/doctor-plugin-registry.ts\n- git diff --check\n- GitHub exact-SHA: Real behavior proof, build-artifacts, checks-fast-contracts-plugins-a, check-prod-types, check-lint, check-test-types green on 8bcbf681ec
2026-05-21 00:49:19 -07:00
Peter Steinberger
3eb2d64392 ci: add live Codex plugin release check 2026-05-21 08:44:18 +01:00
Lucas Shadler
b05c6158c0 fix(slack): suppress reasoning reply payloads (#84322)
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-21 00:43:05 -07:00
Peter Steinberger
ec7495c993 chore: update vite 2026-05-21 08:33:45 +01:00
Peter Steinberger
ec10d12112 chore: update dependencies 2026-05-21 08:28:44 +01:00
Pavan Kumar Gondhi
3cc8b2a3d0 fix(config): validate browser sandbox bind sources [AI] (#84799)
* fix: validate browser sandbox bind sources

* docs: add changelog entry for PR merge
2026-05-21 12:54:48 +05:30
Pavan Kumar Gondhi
a2d0d6b0c2 doctor: constrain legacy plugin cleanup paths [AI] (#84801)
* fix: constrain legacy plugin dependency cleanup roots

* addressing review-skill

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing ci

* docs: add changelog entry for PR merge
2026-05-21 12:54:03 +05:30
Josh Avant
40db92f609 Fix Telegram isolated polling stall watchdog (#84861)
* fix(telegram): watch isolated polling stalls

* docs(changelog): note telegram polling watchdog fix
2026-05-21 00:19:10 -07:00
Peter Steinberger
3faddfb506 ci(release): keep non-waiting clawhub publish best effort 2026-05-21 08:03:48 +01:00
Peter Steinberger
2fd02c2060 ci(release): require resolved target before child dispatch 2026-05-21 07:58:15 +01:00
Peter Steinberger
624d920351 ci(release): keep focused validation reruns independent 2026-05-21 07:58:15 +01:00
Peter Steinberger
0604d25101 ci(release): preserve direct repair publishes 2026-05-21 07:58:15 +01:00
Peter Steinberger
1e8d9666b0 fix(docker): keep prune store warmup before offline stage 2026-05-21 07:58:15 +01:00
Peter Steinberger
1c5fda115f ci(release): streamline beta publish verification 2026-05-21 07:58:15 +01:00
Peter Steinberger
a329b9e1ee fix(docker): keep runtime prune offline 2026-05-21 07:58:15 +01:00
clawsweeper[bot]
e427262044 [Fix] Keep node systemd tokens out of unit files (#84815)
Summary:
- This replacement PR marks the Linux node daemon gateway token as file-backed, writes it to `node.systemd.env`, sanitizes and migrates systemd env artifacts, adds regression tests, and updates the changelog.
- Reproducibility: yes. from source inspection: current `main` copies `OPENCLAW_GATEWAY_TOKEN` into the node s ... e-backed before systemd rendering. I did not run a local live systemd install during this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(systemd): scrub single-quoted env tokens
- PR branch already contained follow-up commit before automerge: [Fix] Keep node systemd tokens out of unit files

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

Prepared head SHA: f626b66c09
Review: https://github.com/openclaw/openclaw/pull/84815#issuecomment-4505012292

Co-authored-by: samzong <samzong.lu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 06:48:15 +00:00
Peter Steinberger
9ec9fbf58d refactor(whatsapp): use async fs-safe credential checks 2026-05-21 07:38:51 +01:00
Marcus Castro
de743c5a54 fix(whatsapp): guard credential atomic writes 2026-05-21 07:38:51 +01:00
Marcus Castro
194f0786d4 fix: reject symlinked whatsapp creds 2026-05-21 07:38:51 +01:00
Gio Della-Libera
8284c035a0 fix(doctor): clear stale runtime override pins (#84221)
* fix(doctor): clear stale runtime override pins

* fix(doctor): register CLI runtime session owners
2026-05-20 23:00:03 -07:00
clawsweeper[bot]
ae80adbefb fix(agents): disable pi-coding-agent auto-retry to prevent tool call replay loops (#84798)
Summary:
- The PR disables pi-coding-agent auto-retry inside prepared embedded Pi settings, updates the focused settings test, and moves the changelog entry into Unreleased.
- Reproducibility: yes. source-reproducible: current main leaves embedded Pi retry enabled, while pi-coding-ag ... e assistant error before continuing. I did not run a live Feishu/Qwen replay loop in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): disable pi-coding-agent auto-retry to prevent tool call …

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

Prepared head SHA: ca745fd55d
Review: https://github.com/openclaw/openclaw/pull/84798#issuecomment-4504702875

Co-authored-by: yelog <yelogeek@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 05:56:41 +00:00
clawsweeper[bot]
c9b6a8b408 fix(trajectory): tolerate partial skill snapshot entries in support capture (#84797)
Summary:
- This PR filters partial skill snapshot entries in trajectory support metadata, accepts nullish support-redaction paths, adds regression tests, and records the fix in the changelog.
- Reproducibility: yes. Source inspection on current main shows undefined skill path/name values can reach str ... and the related source PR provides redacted live before/after gateway logs for the symlink-escape scenario.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(trajectory): tighten test types for partial skill entries
- PR branch already contained follow-up commit before automerge: fix(trajectory): tolerate partial skill snapshot entries in support c…

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

Prepared head SHA: ecb3df6c08
Review: https://github.com/openclaw/openclaw/pull/84797#issuecomment-4504703074

Co-authored-by: Luke Boyett <46942646+lukeboyett@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 05:38:57 +00:00
Val Alexander
3156d94bca fix(ui): widen settings personal card
Widen the Control UI settings Personal quick-settings card to the intended 3/1 desktop split, keep Personal before Appearance/Automations at the narrower layout, and make the focused CSS assertions tolerant of harmless formatting changes.

Verification:
- pnpm --dir ui test src/styles/config-quick.test.ts
- pnpm exec oxfmt --check --threads=1 ui/src/styles/config-quick.test.ts
- git diff --check
- GitHub CI on 4c8f6d7f50
2026-05-21 00:32:30 -05:00
Gio Della-Libera
79be940130 fix(agents): log pre-prompt compaction fits decisions (#84676)
Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-20 21:53:02 -07:00
clawsweeper[bot]
0671a2a788 fix(memory-core): allow bounded dreaming session cleanup (#84802)
Summary:
- The PR changes memory-core dreaming narratives to use stable workspace-and-phase session keys, timestamped idempotency keys, serialized pre/final cleanup, focused tests, and a changelog entry.
- Reproducibility: yes. Source inspection of current main shows the session key includes nowMs and is reused a ... plains timestamp-scoped `dreaming-narrative-*` session accumulation without needing a new product decision.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory-core): allow bounded dreaming session cleanup
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-7046…

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

Prepared head SHA: d519bbecac
Review: https://github.com/openclaw/openclaw/pull/84802#issuecomment-4504756650

Co-authored-by: chiyouYCH <563318445@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 04:45:19 +00:00
Frank Yang
168f8a758e perf(cli): lazy-load agents actions for help (#84483)
Lazy-load agents CLI action modules from command callbacks so agents --help avoids importing the full agents runtime.

Validated by GitHub required checks and local focused CLI gates.
2026-05-21 12:35:37 +08:00
Andy Ye
46030f5489 Skip empty sherpa structured transcripts (#84667)
Summary:
- The PR changes sherpa-onnx CLI audio parsing so structured JSON with an empty `text` field becomes no transcript, while preserving non-empty JSON extraction and adding direct plus auto-detect regression coverage.
- Reproducibility: yes. Source inspection on current main shows empty sherpa structured JSON misses extraction ... scord voice can skip empty transcripts; I did not run a live Discord reproduction in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Fix stale CI guardrails for sherpa transcript PR
- PR branch already contained follow-up commit before automerge: Skip empty sherpa structured transcripts

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

Prepared head SHA: ac03171cfc
Review: https://github.com/openclaw/openclaw/pull/84667#issuecomment-4501484167

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 04:23:59 +00:00
Patrick Erichsen
c0312748c4 feat: support git and local skill installs (#84793) 2026-05-20 21:12:03 -07:00
Gio Della-Libera
a30ac3f8d7 Policy: add tool metadata conformance (#80056)
* feat(policy): add tool metadata conformance checks

* Add policy trusted tool runtime gate

* Use requireMetadata for tool policy

Make tools.requireMetadata the canonical policy schema for risk, sensitivity, and owner requirements. Update runtime enforcement, doctor findings, evidence parsing, tests, and policy docs to use the new schema.

* fix(policy): persist approval metadata

* fix(policy): refresh approval metadata artifacts

* docs(policy): list all tool finding checks

* fix(policy): parse multiline tool metadata

* test(policy): cover unparseable policy check output

* fix(policy): resolve oc-path api in packaged dist

* fix(policy): clear post-rebase CI failures

* test(policy): clear post-rebase CI failures

* fix(policy): restore watch and align validation

* fix(policy): clear ci gate failures

* Simplify policy tool evidence parsing
2026-05-20 20:47:32 -07:00
clawsweeper[bot]
6745fe8e70 fix(doctor): warn when sandbox hides MCP tools (#84742)
Summary:
- This bot replacement PR adds an `openclaw doctor` warning, regression coverage, gateway docs, and a changelog entry for sandbox tool policies that hide configured MCP server tools.
- Reproducibility: yes. source-reproducible. Runtime policy inspection shows sandbox tool policy is a second g ... ed MCP tools, and the source PR supplies after-patch live `openclaw doctor` output showing the new warning.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(doctor): mirror sandbox policy fallback
- PR branch already contained follow-up commit before automerge: fix(doctor): preserve sandbox deny diagnostics
- PR branch already contained follow-up commit before automerge: fix(doctor): polish sandbox MCP warnings
- PR branch already contained follow-up commit before automerge: fix(doctor): warn when sandbox hides MCP tools
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8469…

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

Prepared head SHA: 79dfc3ebc8
Review: https://github.com/openclaw/openclaw/pull/84742#issuecomment-4503743579

Co-authored-by: David Huang <nxmxbbd@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 03:28:27 +00:00
Frank Yang
2c0c9c92f4 perf(cli): speed up onboarding help startup (#84488)
Merged via squash.

Prepared head SHA: b3b086e6d8
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-05-21 11:21:58 +08:00
Frank Yang
2585249737 perf: isolate doctor core check tests (#84493)
Merged via squash.

Prepared head SHA: 6229656ba1
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-05-21 10:47:43 +08:00
Eduardo Piva
3d3cf96dc9 feat(tasks): explain stale-running maintenance decisions (#84691)
Add JSON-only task maintenance diagnostics for stale running tasks and include the maintainer changelog entry.
2026-05-20 19:42:44 -07:00
clawsweeper[bot]
86ebceeb2e fix(minimax): stop advertising music duration control (#84765)
Summary:
- The PR removes MiniMax music duration support from provider capabilities and docs, stops prompt-injecting duration hints, updates the MiniMax provider test, and adds a changelog entry.
- Reproducibility: yes. by source inspection: current main advertises MiniMax duration support while the reque ... uage hint. I did not rerun a live pre-fix MiniMax request, but the code path and vendor contract are clear.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(minimax): align music controls
- PR branch already contained follow-up commit before automerge: docs(music): remove minimax duration steering claim
- PR branch already contained follow-up commit before automerge: fix(minimax): stop advertising music duration control

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

Prepared head SHA: 1c616da45c
Review: https://github.com/openclaw/openclaw/pull/84765#issuecomment-4504176794

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 02:26:17 +00:00
WhatsSkiLL
c4f14a39a5 fix(codex): guard path-only bootstrap files [AI-assisted] (#84736)
Summary:
- The PR updates Codex app-server system-prompt reporting to tolerate bootstrap files with `path` and `content` but no `name`, adds a focused regression test, and records the fix in the changelog.
- Reproducibility: yes. The PR body supplies current-main before output with the `undefined.trim()` stack, and source inspection confirms hook-supplied path-only bootstrap files can reach the Codex report helper.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): guard path-only bootstrap files [AI-assisted]

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

Prepared head SHA: 4667110899
Review: https://github.com/openclaw/openclaw/pull/84736#issuecomment-4503672362

Co-authored-by: JARVIS-Glasses <whatsskilll@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 01:55:29 +00:00
lukaIvanic
9cdf8a1e2f Warn on plaintext secret config in doctor (#84718)
Summary:
- Adds a `doctor` security warning for plaintext secret-bearing `openclaw.json` fields by reusing the secrets target registry and shared model-provider header sensitivity policy.
- Reproducibility: yes. for source-level behavior: current main has plaintext secret audit coverage but no doc ... llector for those config targets, and the PR body includes live patched CLI output showing the new warning.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Warn on plaintext secret config in doctor

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

Prepared head SHA: 31f83aae19
Review: https://github.com/openclaw/openclaw/pull/84718#issuecomment-4503210496

Co-authored-by: qingsenlab <qingsenlab@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 01:27:34 +00:00
Jesse Merhi
e964987cd2 Remove skill prelude exec allowlist (#84570)
Summary:
- The PR removes the legacy `cat SKILL.md && printf ... && <skill-wrapper>` exec-approval allowlist path, updates focused exec-approval tests, and adds a changelog entry.
- Reproducibility: yes. Current-main source and tests show the old `cat SKILL.md && printf ... && <wrapper>` c ... ed this by source and test inspection rather than executing tests because the checkout review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Remove skill prelude exec allowlist

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

Prepared head SHA: 0ca7f3e8ef
Review: https://github.com/openclaw/openclaw/pull/84570#issuecomment-4498357535

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: jesse-merhi
2026-05-21 01:03:35 +00:00
Dallin Romney
b79effefee perf(tui): defer EmbeddedTuiBackend import, drop dead warmup helpers (#84701)
* perf(tui): skip plugin-aware config validation on remote TUI startup

Cold `openclaw tui` against a remote gateway was synchronously calling
loadPluginMetadataSnapshot() via getRuntimeConfig() -> loadConfig() ->
validateConfigObjectWithPlugins(), pulling the full plugin metadata
snapshot (200k+ file reads) onto the TUI's event loop. The TUI itself
never consumes plugin metadata in remote mode; it queries the gateway
over RPC. The work was being done purely to validate the config and
then thrown away.

Thread an opt-in `skipPluginValidation` flag through getRuntimeConfig()
and loadConfig() (createConfigIO already supports pluginValidation: "skip";
it just wasn't reachable from the runtime entrypoints). The TUI passes
skipPluginValidation: !isLocalMode so:

- Remote-mode TUI: no plugin metadata load, no event-loop freeze after
  first render
- Embedded (--local) mode: unchanged; the in-process agent runtime
  still gets a fully validated config

* remove verbose comments

* perf(tui): move context cache warmup from module top-level to embedded backend

agents/context.ts fired ensureContextWindowCacheLoaded() unconditionally
at module-eval time for non-skip-listed CLI commands. The TUI transitively
imports this module, so the warmup ran on every TUI startup including
remote-mode, cascading into ensureOpenClawModelsJson -> resolveImplicitProviders
-> runProviderCatalog and dominating the cold-start freeze (CPU profile
showed ~55s of resolveProviderSyntheticAuthWithPlugin, lstat, open, etc.).

It also pre-emptively called getRuntimeConfig() without skipPluginValidation,
pinning the full snapshot and nullifying the skip flag added on this branch.

Remove the top-level side effect and trigger the warmup explicitly from
EmbeddedTuiBackend.start(), which only runs when an in-process agent
runtime actually needs the cache.

* perf(tui): defer EmbeddedTuiBackend import until local mode

* refactor(agents): remove dead context-cache warmup helpers
2026-05-20 17:43:52 -07:00
Dallin Romney
d91ef6bb17 perf(tui): skip plugin metadata + provider catalog on remote TUI startup (#84686)
* perf(tui): skip plugin-aware config validation on remote TUI startup

Cold `openclaw tui` against a remote gateway was synchronously calling
loadPluginMetadataSnapshot() via getRuntimeConfig() -> loadConfig() ->
validateConfigObjectWithPlugins(), pulling the full plugin metadata
snapshot (200k+ file reads) onto the TUI's event loop. The TUI itself
never consumes plugin metadata in remote mode; it queries the gateway
over RPC. The work was being done purely to validate the config and
then thrown away.

Thread an opt-in `skipPluginValidation` flag through getRuntimeConfig()
and loadConfig() (createConfigIO already supports pluginValidation: "skip";
it just wasn't reachable from the runtime entrypoints). The TUI passes
skipPluginValidation: !isLocalMode so:

- Remote-mode TUI: no plugin metadata load, no event-loop freeze after
  first render
- Embedded (--local) mode: unchanged; the in-process agent runtime
  still gets a fully validated config

* remove verbose comments

* perf(tui): move context cache warmup from module top-level to embedded backend

agents/context.ts fired ensureContextWindowCacheLoaded() unconditionally
at module-eval time for non-skip-listed CLI commands. The TUI transitively
imports this module, so the warmup ran on every TUI startup including
remote-mode, cascading into ensureOpenClawModelsJson -> resolveImplicitProviders
-> runProviderCatalog and dominating the cold-start freeze (CPU profile
showed ~55s of resolveProviderSyntheticAuthWithPlugin, lstat, open, etc.).

It also pre-emptively called getRuntimeConfig() without skipPluginValidation,
pinning the full snapshot and nullifying the skip flag added on this branch.

Remove the top-level side effect and trigger the warmup explicitly from
EmbeddedTuiBackend.start(), which only runs when an in-process agent
runtime actually needs the cache.
2026-05-20 17:43:24 -07:00
clawsweeper[bot]
b3ec4f08d1 Route JSON-mode plugin registration logs to stderr (#84741)
Summary:
- The PR extracts JSON-mode console-to-stderr routing into a shared CLI helper, wraps root and `nodes` lazy plugin registration, adds nodes registration coverage, and adds a changelog entry.
- Reproducibility: yes. for source-level reproduction: the linked report shows `openclaw nodes list --json 2>  ... ssing the existing JSON stderr guard. I did not run the live Helm/container repro in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Route JSON-mode plugin registration logs to stderr

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

Prepared head SHA: c9d0867db0
Review: https://github.com/openclaw/openclaw/pull/84741#issuecomment-4503741078

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-21 00:35:40 +00:00
Dallin Romney
cd019cfa41 build: suppress rolldown-plugin-dts CommonJS dts warnings from bundled zod locales (#84592)
* build: suppress rolldown-plugin-dts CommonJS dts warnings from bundled zod locales

After bumping rolldown-plugin-dts to 0.25.1 (94ac563399), every
`pnpm build` emits a 'CommonJS dts' warning per zod `v4/locales/*.d.cts`
file because zod is intentionally inlined for global pnpm install
resolution (#78515) and tsdown's external option cannot be scoped to the
dts pass only. Filter the warning in the existing onLog suppression list
(same pattern as PLUGIN_TIMINGS / UNRESOLVED_IMPORT / EVAL) so other
rolldown-plugin-dts warnings remain visible.

* docs(changelog): move rolldown-dts entry into 2026.5.20 fixes
2026-05-20 17:20:47 -07:00
clawsweeper[bot]
5c4c6a4207 [codex] Fix macOS app copyright year (#84729)
Summary:
- The PR updates the macOS About settings copyright text to 2026, adds a changelog entry, and adjusts changed-check planning so non-macOS hosts without SwiftLint emit an explicit app-lint skip with matching test coverage.
- Reproducibility: yes. from source inspection: current main still renders the 2025 copyright literal in the m ...  launch the app locally, but the source path and source PR proof make the observable issue high-confidence.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8438…

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

Prepared head SHA: 26816c18d6
Review: https://github.com/openclaw/openclaw/pull/84729#issuecomment-4503529931

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 23:45:47 +00:00
Kevin Lin
b58572e283 fix(approval): route /approve through approval resolver (#84678) 2026-05-20 16:00:37 -07:00
Dallin Romney
4d47f9a4c0 test(secret-file): cover NickServ + account-level symlinks, narrow inspect catch (#84713)
Followup nits from the #84711 review:

- Narrow the inspectTokenFile catch in
  extensions/telegram/src/account-inspect.ts to FsSafeError so only
  fs-safe validation throws map to configured_unavailable; any other
  throw (programmer error, unexpected I/O) is rethrown.
- Add a regression test for the IRC NickServ password file symlink
  rejection path (extensions/irc/src/accounts.ts:118), paralleling the
  existing top-level passwordFile test.
- Add a regression test for the Telegram account-level tokenFile
  symlink rejection path (extensions/telegram/src/token.ts:149),
  paralleling the existing channel-level tokenFile test.

Behavior was already correct after #84711; this just locks coverage and
tightens the catch.
2026-05-20 15:35:52 -07:00
Dallin Romney
90fd26b602 fix(infra): restore symlink rejection in tryReadSecretFileSync (#84711)
* fix(infra): restore symlink rejection in tryReadSecretFileSync

The local wrapper added in 9e4eca00ff swallowed all errors from
@openclaw/fs-safe@0.2.7's tryReadSecretFileSync via a bare try/catch,
silently downgrading every rejectSymlink: true caller (Telegram, LINE,
Zalo, IRC, Nextcloud Talk credential files) to accept symlinked
credential files. It also broke the infra-state CI shard's symlink
expectation that #84595 had just realigned with the new fail-closed
upstream contract.

Restore the direct re-export so the upstream contract surfaces:
undefined for blank/missing/not-found, FsSafeError for symlink,
oversize, non-regular file, and hardlink validation failures.

* test(plugins): align stale symlink tests with fail-closed contract

5 token/account resolver tests still asserted the pre-fs-safe-0.2.7
"silent skip" behavior (token: "", source: "none") on rejected symlinks;
they passed only because the swallow-all wrapper in secret-file.ts hid
the throw. Restoring the upstream fail-closed contract surfaces the
throw, so update the tests to expect FsSafeError.

inspectTelegramAccount reports credential status (its return type has an
explicit configured_unavailable state for "configured but unreadable"),
so its callsite is the right boundary to catch the FsSafeError and map
it to configured_unavailable rather than letting the throw bubble.

Affected:
- extensions/zalo/src/token.test.ts
- extensions/line/src/accounts.test.ts
- extensions/telegram/src/token.test.ts
- extensions/irc/src/accounts.test.ts
- extensions/nextcloud-talk/src/setup.test.ts
- extensions/telegram/src/account-inspect.ts (catch + report status)
2026-05-20 15:21:13 -07:00
Peter Steinberger
3844513431 test: align release timeout budget expectations
(cherry picked from commit a185ca283a)
2026-05-20 22:38:43 +01:00
Peter Steinberger
6b52105b23 ci: extend stable release validation monitors
(cherry picked from commit ca3c3fca43)
2026-05-20 22:38:43 +01:00
Peter Steinberger
d786b4eb55 ci: preserve node path across setup action steps
(cherry picked from commit a6172a7d0e)
2026-05-20 22:38:43 +01:00
Peter Steinberger
1fdeee380e fix: preserve update compatibility host during release upgrades
(cherry picked from commit 2823725134)
2026-05-20 22:38:43 +01:00
Peter Steinberger
2e389b6a46 fix(update): prefer npm during post-core repair
(cherry picked from commit eab57ad8ad)
2026-05-20 22:38:43 +01:00
Peter Steinberger
f4dc9b1232 fix(update): defer legacy parent plugin repair
(cherry picked from commit 93c2d1ea99)
2026-05-20 22:38:43 +01:00
Peter Steinberger
aa687a08cd fix(update): adopt post-core plugin payloads
(cherry picked from commit 29faac2f9c)
2026-05-20 22:38:43 +01:00
Peter Steinberger
e57fa51412 fix(update): preserve post-core host version
(cherry picked from commit e8d8c5dd6f)
2026-05-20 22:38:42 +01:00
Peter Steinberger
3c3ef6067e fix(update): prefer existing npm plugins during repair
(cherry picked from commit 3743d6bdeb)
2026-05-20 22:38:42 +01:00
openclaw-release-bot
ec8e7003a6 chore(release): update appcast for 2026.5.19 2026-05-20 21:35:27 +00:00
Peter Steinberger
6c7fe58468 chore(release): refresh generated baselines 2026-05-20 21:59:52 +01:00
Peter Steinberger
7b9066120a chore(release): bump version to 2026.5.20 2026-05-20 21:58:56 +01:00
Gio Della-Libera
6e9d47bd12 fix(doctor): migrate invalid thinking formats (#84626) 2026-05-20 13:58:01 -07:00
Kevin Lin
9e4eca00ff fix(slack): normalize approval user ids (#84671)
* fix(slack): normalize approval user ids

* chore(openrouter): satisfy spread fallback lint

* fix(ci): unblock status and secret-file checks
2026-05-20 13:40:14 -07:00
Kevin Lin
404fd6d9ab fix(codex): bridge computer use elicitations 2026-05-20 13:39:11 -07:00
Peter Steinberger
6e7bd551f2 chore(deps): update whatsapp baileys 2026-05-20 21:36:39 +01:00
Zhaocun Sun
ca0fe884ff fix(cli): gate exported subcli descriptors (#84519)
Summary:
- This PR filters exported sub-CLI descriptors through the private-QA gate, centralizes that filter, adds regr ... ge, and carries small validation repairs in workspace glob and tunnel-timeout tests plus a changelog entry.
- Reproducibility: yes. Current-main source shows the raw SUB_CLI_DESCRIPTORS export can include qa while the helper surfaces filter it, and src/cli/argv.ts consumes that export for root command policy.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cli): gate exported subcli descriptors
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8451…

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

Prepared head SHA: ba197a6f30
Review: https://github.com/openclaw/openclaw/pull/84519#issuecomment-4496549642

Co-authored-by: Zhaocun <zhaocunsun@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 20:32:01 +00:00
Xu Xiang
d5cc0d53b7 fix(browser): honor image sanitization config for screenshots (#84595)
Summary:
- The branch threads `agents.defaults.imageMaxDimensionPx` into browser screenshot and labeled snapshot image results, adds regression coverage and a changelog entry, and includes small repair-pass type/lint cleanup.
- Reproducibility: yes. source-level reproduction is high confidence: current `main` calls `imageResultFromFil ...  both browser image-returning paths, while the shared sanitizer falls back to `1200px` without an override.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(browser): honor image sanitization config for screenshots
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8459…

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

Prepared head SHA: c01fde7990
Review: https://github.com/openclaw/openclaw/pull/84595#issuecomment-4499178477

Co-authored-by: Xu Xiang <xx205@outlook.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 20:09:32 +00:00
Peter Steinberger
1a7669bc63 fix: update fs-safe fallback dependency 2026-05-20 19:35:08 +01:00
Dallin Romney
447a3643c6 fix(errors): dedupe identical messages when traversing error .cause chain (#84556)
Merged via squash.

Prepared head SHA: 46aa27fa12
Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-20 21:26:14 +03:00
Neerav Makwana
950e5c8c50 fix(agents): credit delivered subagent completions (#84383) 2026-05-20 14:19:30 -04:00
Aayush Pratap Singh
0af55f971d fix: check billing errors before surfacing rate-limit message (#79489)
Merged via squash.

Prepared head SHA: 2ea757ce8c
Co-authored-by: aayushprsingh <172073271+aayushprsingh@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-20 20:55:58 +03:00
Peter Steinberger
a13468320c fix: clarify pinned session model status 2026-05-20 15:59:24 +01:00
Peter Steinberger
c8a953af93 fix: keep cron final output over tool warnings 2026-05-20 14:50:50 +01:00
Alex Knight
ac69776330 Add OpenRouter provider routing params (#84579)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-20 23:27:34 +10:00
Jason (Json)
48a14e41e2 feat(discord): add realtime voice bootstrap context
Add bounded realtime profile context for Discord realtime voice sessions.
Default to `IDENTITY.md`, `USER.md`, and `SOUL.md`; `voice.realtime.bootstrapContextFiles: []` disables the extra context.
Document the config/SDK surface and refresh generated metadata.

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-05-20 14:13:59 +01:00
googlerest
32fbb9ff01 test(cli): cover parsePort edge cases (#84518)
Summary:
- The PR updates `src/cli/shared/parse-port.test.ts` to cover numeric strings, whitespace-padded strings, fractional strings, invalid suffixes, and safe-integer overflow for `parsePort`.
- Reproducibility: not applicable. This PR adds test coverage rather than reporting a failing runtime behavior. Source inspection confirms the current parser contract and the exact baseline coverage gap on main.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

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

Prepared head SHA: 14213cc8f4
Review: https://github.com/openclaw/openclaw/pull/84518#issuecomment-4496552268

Co-authored-by: googlerest <127843198+googlerest@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 13:06:25 +00:00
Moeed Ahmed
9c00268914 fix: honour tool error suppression for mutating tools (#81561)
Merged via squash.

Prepared head SHA: 7462a862be
Co-authored-by: moeedahmed <5780040+moeedahmed@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-20 05:53:15 -07:00
Chunyue Wang
5d775122c1 fix(codex/command-account): respect explicit auth order over lastGood (#84412)
Fixes openclaw#84386. resolveActiveProfileId in extensions/codex/src/command-account.ts returned store.lastGood whenever that profile was still in the resolved order, ignoring rank, so /codex account marked the stale openai-codex:default profile as active after models auth login + models auth order set. Tracks whether the order came from an explicit operator source (store.order / config.auth.order, including the openai alias key), picks the first usable explicit-order profile, and returns undefined when no candidate is eligible so the display surfaces "no working credential" instead of marking a lower-ranked profile active. Runtime selection via resolveCodexAppServerAuthProfileId is unchanged.
2026-05-20 20:02:28 +08:00
Peter Steinberger
99c88629c3 fix(macos): update embedded Peekaboo bridge 2026-05-20 12:58:56 +01:00
Dallin Romney
9a6744baba perf(plugins): scan-scoped package.json cache in discovery (#84302)
* perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts

Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult
through the remaining helpers that still call discoverOpenClawPlugins
internally during startup:

- loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts):
  add discovery? to PluginLoadOptions and consult it before falling back to
  an internal scan at both call sites.

- loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept
  discovery? as a more ergonomic alternative to the existing candidates? /
  diagnostics? pair; candidates? still wins when both are supplied.

- resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts):
  add discovery? to LoadInstalledPluginIndexParams and use it when
  candidates aren't supplied.

- resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add
  discovery? and thread it into the bundled-fallback discovery call.

Add discovery-threading.test.ts asserting each entry point skips its
internal discoverOpenClawPlugins call when discovery is supplied, calls it
when nothing is supplied, and prefers explicit candidates over discovery
when both are present (6 tests, all pass).

discoverOpenClawPlugins remains stateless; sharing is function-scoped per
src/plugins/CLAUDE.md guidance. Backward compatible: every change is
additive (new optional param).

* perf(plugins): drop verbose JSDoc from discovery? params

* perf(plugins): scan-scoped package.json cache in discovery

Adds a per-scan Map<string, PackageManifest | null> threaded through
discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed
by the directory's resolved real path. Within one discovery scan, a
plugin's package.json is now read from disk once and reused across the
overlapping discovery code paths (bundled overlay scan, stock-root scan,
source-checkout extensions scan, installed-path scan, global-root scan)
that previously each fired their own read.

The cache lifetime is one scan (created in runPluginDiscovery alongside
the existing realpathCache and seen Set, dies when the scan returns).
discoverOpenClawPlugins remains stateless externally; no persistent
metadata cache.

* perf(plugins): expose raw parsed package.json on PluginCandidate

Discovery already reads each plugin's package.json once and produces a
parsed PackageManifest object before distilling it into metadata via
getPackageManifestMetadata. Currently only the distilled metadata is
kept on the candidate; the full parsed manifest is discarded.

Store the full parsed manifest on rawPackageManifest so downstream
consumers iterating candidates can use it instead of re-reading from
disk. This is the candidate-side groundwork for the scenario-C followup
that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.)
through the cached field; those consumers currently do their own
directory scans and would need to be refactored to iterate
PluginCandidate arrays before they can benefit.

The field is a frozen-at-discovery-time snapshot, same lifetime semantics
as the existing packageManifest / packageName / packageVersion fields on
PluginCandidate. No new staleness window introduced.

* perf(plugins): make package-manifest cache key trust-aware
2026-05-20 04:57:45 -07:00
Jason (Json)
befb0f3d39 feat(discord): follow configured users in voice
Summary:
- Adds Discord voice followUsers/followUsersEnabled config, metadata, docs, and changelog coverage.
- Makes Discord voice follow configured users across joins, moves, disconnects, admin moves, handoff, bounded reconciliation, transient REST failures, destroy cleanup, and DAVE recovery.
- Adds focused Discord voice/config regression tests and refreshes generated config docs metadata.

Verification:
- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/discord/src/voice/manager.e2e.test.ts
- node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-discord.config.ts extensions/discord/src/config-schema.test.ts
- pnpm config:channels:check
- pnpm config:docs:check
- pnpm config:schema:check
- pnpm exec oxfmt --check --threads=1 docs/channels/discord.md extensions/discord/src/voice/manager.ts extensions/discord/src/voice/manager.e2e.test.ts src/config/bundled-channel-config-metadata.generated.ts CHANGELOG.md
- git diff --check
- pnpm build
- pnpm check:test-types
- Mac Studio config validate + gateway:watch proof on cf67023fdf; Discord provider started and gateway ready
- Autoreview passed after two actionable findings were fixed

CI notes:
- PR-specific proof is green: check-docs, config-boundary, real behavior proof, check-test-types, OpenGrep, CodeQL, no-tabs, security-fast.
- Remaining broad CI reds match current main failures/noise on unrelated fs-safe Python helper, Windows ACL locale, managed media staging, and dependency guardrail surfaces.

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-05-20 12:49:15 +01:00
Peter Steinberger
d1470360c4 fix: stabilize mac app packaging 2026-05-20 07:35:11 -04:00
Peter Steinberger
94ac563399 build: update dependencies 2026-05-20 12:08:17 +01:00
Gio Della-Libera
cbf72e5e26 feat(policy): add channel conformance checks (#80407)
Summary:
- Add the bundled Policy plugin with policy-backed doctor checks for channel conformance.
- Add `openclaw policy check` attestations, accepted-attestation drift checks, and opt-in doctor repair.
- Add policy CLI docs, generated plugin inventory/reference docs, and changelog credit.

Verification:
- node --import tsx scripts/sync-plugin-versions.ts --check
- pnpm plugins:inventory:check
- pnpm docs:list
- git diff --check origin/main..HEAD
- node scripts/run-vitest.mjs extensions/policy/src/policy-state.test.ts extensions/policy/src/cli.test.ts extensions/policy/src/doctor/register.test.ts src/flows/bundled-health-checks.test.ts src/cli/program/register.maintenance.test.ts
- codex review --uncommitted; accepted finding fixed, reran clean
- codex review --commit HEAD
- GitHub CI for 4e09b067f4: CI, Workflow Sanity, CodeQL, CodeQL Critical Quality, OpenGrep PR Diff, Real behavior proof, Dependency Change Awareness all green; reran failed Windows Node setup job successfully

Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-20 11:50:21 +01:00
Peter Steinberger
9c5e8eb495 docs: note GitHub paste preflight 2026-05-20 11:45:19 +01:00
Peter Steinberger
3c8050c44c docs: keep developer tooling out of release tweets 2026-05-20 11:39:46 +01:00
Peter Steinberger
45930457ca docs: keep qa proof out of release tweets 2026-05-20 11:37:51 +01:00
Peter Steinberger
167e73cd5f build: bump bundled Codex harness to 0.132.0 2026-05-20 10:38:35 +01:00
yaoyi1222
110042d840 fix(cron-cli): bound loadCronJobForShow pagination (#83856) (#83989)
Summary:
- Adds a 50-page and advancing-`nextOffset` guard to `loadCronJobForShow`, exports that helper for regression tests, and adds an unreleased changelog entry.
- Reproducibility: yes. Current main is source-reproducible because `loadCronJobForShow` loops while `hasMore` ... ed numeric `nextOffset`; the PR discussion also includes terminal before/after proof for the same CLI path.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

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

Prepared head SHA: 7828b4bdae
Review: https://github.com/openclaw/openclaw/pull/83989#issuecomment-4484474655

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 07:19:38 +00:00
Sarah Fortune
ea8f4ebb4d fix(config): accept execApprovals.enabled="auto" in zod schema 2026-05-20 00:16:41 -07:00
Pavan Kumar Gondhi
0c67dc7f82 fix(mattermost): fail closed on missing channel type [AI] (#84091)
* fix: fail closed on missing Mattermost channel type

* addressing codex review

* docs: add changelog entry for PR merge
2026-05-20 12:33:55 +05:30
Pavan Kumar Gondhi
e98760a1bf Recheck rebuilt system.run argv [AI] (#84090)
* fix: recheck rebuilt system run argv

* docs: add changelog entry for PR merge
2026-05-20 12:30:26 +05:30
Gio Della-Libera
67c12e0368 fix(cli): use active node for startup bench scripts (#84451) 2026-05-19 23:21:26 -07:00
Ayaan Zaidi
989e53c20d fix(android): address overhaul review findings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
bbcac0019b refactor(android): make overhaul UI canonical 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
64b6cafcaa test(android): update gateway hello callback fixtures 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
98f2e568b3 refactor(android): centralize v2 separated list rows 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
c289e3ea87 fix(android): expand v2 settings toggle hit areas 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
c0ac4564f7 fix(android): clarify v2 voice settings action 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
07b28a6dd6 fix(android): gate v2 cron job save action 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
85ef8fb975 fix(android): request v2 capability permissions 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
c885a1c243 feat(android): wire v2 chat image attachments 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
dd772307a3 fix(android): tighten v2 navigation affordances 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
43b03b7621 fix(android): expand v2 model catalog groups 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
9868f4cf29 fix(android): align v2 control affordances 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
d3cf65eb14 fix(android): remove dead v2 chat controls 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
3aefd355c4 fix(android): wire v2 onboarding actions 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
8d492637af fix(android): wire v2 navigation controls 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
5de8f8e8a9 feat(android): polish v2 voice surfaces 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
338a0062c4 feat(android): add v2 chat starters 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
69e646f680 fix(android): prevent provider setup button overlap 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
d41f595c75 feat(android): polish v2 provider setup 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
a9669c0f9f feat(android): polish v2 overview navigation 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
2294c28355 style(android): refine v2 touch rhythm 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
499ccd1522 feat(android): add v2 cron job editor 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
de195645f9 refactor(android): reuse v2 list primitives 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
357e3ecc65 feat(android): add v2 about update status 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
f359299df4 feat(android): add v2 health logs 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
3d5be4c5a9 feat(android): add v2 dreaming settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
6db000630c feat(android): add v2 channels settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
fd05179d0a feat(android): add v2 canvas settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
e067203b22 feat(android): add v2 nodes devices settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
817ca4bf65 feat(android): add v2 skills settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
41175edd98 feat(android): add v2 usage settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
b6e04fa6a2 feat(android): add v2 cron jobs settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
efe7393064 feat(android): add v2 approvals settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
d7a90ebea6 feat(android): add v2 agents settings 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
e5cd050e51 refactor(android): split v2 shell screens 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
aca22366f2 feat(android): add v2 settings detail screens 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
e8a90a03df feat(android): add v2 command palette 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
c842f542cd feat(android): restore readable v2 typography 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
415a338dc6 feat(android): tighten voice transcript cards 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ceb7e04108 feat(android): keep overview modules honest 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ee6c42945a feat(android): tighten dictation fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
abf70ac04e feat(android): tighten talk session fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
2ce12552bb feat(android): tighten settings fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
289eea04d0 feat(android): tighten provider model fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ac28341ebf feat(android): tighten voice mockup fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ca4264202e feat(android): tighten chat mockup fidelity 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
af5e0b26ef feat(android): tighten overview density 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
f4cc4655ef feat(android): tighten sessions density 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
5a82e4aa19 feat(android): tighten voice hub density 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
eff8b41fb0 feat(android): tighten chat density 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
d593f5b062 feat(android): add providers models surface 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
6af2fa4ec3 feat(android): tighten settings screen 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
6db48f70e8 feat(android): tighten chat chrome 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
818aa36f7c feat(android): add focused dictation 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
7d0bb236f2 feat(android): add focused talk session 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
1882984380 feat(android): tighten voice hub 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
9342deeae3 feat(android): tighten sessions experience 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
26352f5a13 feat(android): tighten overview experience 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ff50cdf396 feat(android): rebuild chat experience 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
7e0584579c feat(android): overhaul gateway onboarding 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
5200e8a436 feat(android): add v2 app shell 2026-05-20 10:54:08 +05:30
Ayaan Zaidi
ac43f47820 feat(android): add v2 design system 2026-05-20 10:54:08 +05:30
pashpashpash
448eb36f75 Revert "fix: prompt Codex to send visible channel replies (#84397)" (#84442)
This reverts commit 47eb4ca14f.
2026-05-20 14:20:56 +09:00
clawsweeper[bot]
65030f3164 fix(pi): keep message-tool delivery in session lock (#84437)
Summary:
- The replacement branch adds an owned transcript write context around Pi prompt-time delivery mirror appends and a message-tool-only terminal hook, with focused tests and a changelog entry.
- Reproducibility: yes. the source PR includes before/after redacted live Discord logs for a message-tool-only ... ession-lock and transcript append code. I did not rerun the live Discord scenario in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(pi): keep message-tool delivery in session lock

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

Prepared head SHA: f16678175c
Review: https://github.com/openclaw/openclaw/pull/84437#issuecomment-4494545360

Co-authored-by: Andrew Meyer <andrewmeyer@andrews-air.lan>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 05:20:03 +00:00
clawsweeper[bot]
7811e313b3 fix(channels): suppress verbose failed-tool dumps (#84354)
Summary:
- The branch suppresses regular verbose failed-tool raw output after final replies across shared dispatch, Codex, Telegram, and Discord paths, keeps raw detail under `/verbose full`, and updates tests, docs, and changelog.
- Reproducibility: yes. The current-main source path and supplied before screenshot show failed text-only tool ... ping after a final reply; I did not rerun a live Telegram or Discord reproduction in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix message-tool delivery gating
- PR branch already contained follow-up commit before automerge: fix(channels): keep verbose tool failures compact
- PR branch already contained follow-up commit before automerge: fix(channels): suppress in-flight final progress
- PR branch already contained follow-up commit before automerge: fix(replies): suppress failed tool dumps in message-only mode
- PR branch already contained follow-up commit before automerge: fix(replies): avoid duplicate exec failure warnings
- PR branch already contained follow-up commit before automerge: Revert "fix(replies): avoid duplicate exec failure warnings"

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

Prepared head SHA: d15ae6951b
Review: https://github.com/openclaw/openclaw/pull/84354#issuecomment-4493007024

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 05:09:38 +00:00
Galin Iliev
ddf9fbed34 fix(gateway): expose runtime version in gateway status
Closes #56222
2026-05-19 22:09:14 -07:00
WhatsSkiLL
29f8715f05 [AI-assisted] fix(cron): preserve legacy array stores (#84433)
Summary:
- The PR changes cron store loading to normalize legacy top-level array `jobs.json` files into the versioned store shape and adds store, service, doctor, gateway tests plus a changelog entry.
- Reproducibility: yes. Current `main` clearly maps a top-level parsed array to `{}` before reading `.jobs`, and the PR body supplies before/after runtime output for the load/add/save path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: [AI-assisted] fix(cron): preserve legacy array stores

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

Prepared head SHA: 446014b4c1
Review: https://github.com/openclaw/openclaw/pull/84433#issuecomment-4494478724

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 05:01:24 +00:00
Peter Steinberger
5c39e0019d ci: harden pnpm setup node selection 2026-05-20 05:34:20 +01:00
Josh Avant
47eb4ca14f fix: prompt Codex to send visible channel replies (#84397)
* fix: prompt codex to send visible channel replies

* chore: add codex reply changelog entry

* test: refresh codex prompt snapshots
2026-05-19 23:29:41 -05:00
Galin Iliev
9eee202a69 fix(cron): isolate main-session cron wake lanes (#82767)
* fix(cron): isolate main-session cron wake lanes

* test(cron): expect dedicated main cron lanes

* fix(cron): route global main cron wakes

* docs(changelog): note cron main-session lane fix

---------

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>
2026-05-19 21:12:28 -07:00
Josh Avant
a54c73687f fix(agents): provenance-bound Codex reasoning replay (#84367)
* fix(agents): recover stale Codex encrypted reasoning replay

* docs(changelog): note Codex encrypted replay recovery

* fix(agents): bind Codex reasoning replay provenance

* fix(agents): pin codex reasoning replay provenance
2026-05-19 23:05:19 -05:00
clawsweeper[bot]
a57ab2448f docs(imessage): warn that cliPath wrappers must stream JSON-RPC stdio (#84330) (#84420)
Summary:
- The PR adds a Warning block to `docs/channels/imessage.md` explaining that iMessage `cliPath` wrappers and SSH proxies must stream long-lived JSON-RPC stdin/stdout incrementally.
- Reproducibility: not applicable. for this docs-only PR. Source inspection verifies the runtime uses long-lived line-framed stdio, and current main lacks the operator warning being added.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(imessage): warn that cliPath wrappers must stream JSON-RPC stdio…

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

Prepared head SHA: a371ee998e
Review: https://github.com/openclaw/openclaw/pull/84420#issuecomment-4494313781

Co-authored-by: HCL <chenglunhu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 03:51:32 +00:00
Galin Iliev
c982358753 fix: dedupe OpenAI strict schema downgrade diagnostics (#82933)
* fix: dedupe openai strict schema downgrade logs

* test: align openai transport helper export

* test: cover openai downgrade log behavior

* docs: note openai downgrade diagnostic dedupe

---------

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
2026-05-19 20:48:26 -07:00
Dave Morin
18a514e39e docs: align xai code execution auth docs (#84416) 2026-05-19 20:39:04 -07:00
Andy Ye
33fc2375f8 fix(anthropic): preserve configured Claude image capability (#84180)
Summary:
- The PR routes model-list row construction through provider-owned resolved-model normalization for configured ...  rows, adds Anthropic regression coverage, updates focused test mocks/fixtures, and adds a changelog entry.
- Reproducibility: yes. at source level: current main renders configured/default list rows without calling the ... ty is restored. The PR body also supplies terminal output showing the fixed configured row as `text+image`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(models): update forward compat agent-scope mock
- PR branch already contained follow-up commit before automerge: test(models): isolate provider catalog row tests
- PR branch already contained follow-up commit before automerge: test(models): complete provider catalog fixtures
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'upstream/main' into fix/anthropic-confi…
- PR branch already contained follow-up commit before automerge: test(workflows): match alpha concurrency rules

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

Prepared head SHA: 7a1caa7dff
Review: https://github.com/openclaw/openclaw/pull/84180#issuecomment-4489015944

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 03:32:12 +00:00
Galin Iliev
ad925bd43b Preserve AGENTS.md policy during bootstrap truncation (#82921)
Fixes #82920
2026-05-19 20:25:27 -07:00
Andy Ye
9108ae0114 Include delivery errors in subagent announce give-up logs (#84281)
* Include delivery error in subagent announce give-up logs

* test(agents): type announce delivery error response
2026-05-19 23:22:48 -04:00
Jason (Json)
2ab3a4e422 Filter heartbeat response-tool transcript artifacts (#83477)
Summary:
- This PR replaces pair-only heartbeat filtering with span-based filtering before embedded-runner prompt assem ... ession coverage and a changelog entry, and updates the LINE command type to use the SDK command definition.
- Reproducibility: yes. from source and report evidence: current main only removes immediate heartbeat prompt/ ...  body supplies same-session terminal proof and a commenter supplied a matching Discord gateway observation.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: filter heartbeat transcript artifacts
- PR branch already contained follow-up commit before automerge: fix: clean up heartbeat filter lint
- PR branch already contained follow-up commit before automerge: fix: keep line entry on channel SDK
- PR branch already contained follow-up commit before automerge: fix: filter heartbeat response text transcript shapes
- PR branch already contained follow-up commit before automerge: Filter heartbeat response-tool transcript artifacts

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

Prepared head SHA: e019c74bb5
Review: https://github.com/openclaw/openclaw/pull/83477#issuecomment-4475062400

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 03:19:35 +00:00
Galin Iliev
5d799c2d20 fix: yield diagnostic event drains (#82937)
Summary:
- The branch caps async diagnostic drains at 100 events per turn, adds pending/full-drain diagnostic helpers,  ... rminal diagnostics to inspect pending events, and adds regression coverage plus changelog/baseline updates.
- Reproducibility: yes. from source inspection. Current main drains the entire async diagnostic queue in one s ... ck, and the PR body supplies a focused 250-event after-fix probe showing 100/200/250 delivery across turns.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: yield diagnostic event drains

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

Prepared head SHA: 95610934cd
Review: https://github.com/openclaw/openclaw/pull/82937#issuecomment-4469498220

Co-authored-by: Galin Iliev <galini@microsoft.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-20 02:55:17 +00:00
Jiaming Guo
125f0c31dd fix(msteams): mark external system events as non-owner
Marks skipped and supplemental Microsoft Teams system events as non-owner/untrusted while preserving active primary message dispatch behavior.

Verified before merge:
- PR was open, not draft, mergeable, and clean against main
- Matched head: 4f79f46205
- GitHub checks passed, including Real behavior proof, auto-response, build artifacts, type/lint checks, channel/runtime critical quality checks, and security-fast
- ClawSweeper marked proof sufficient with no concrete contributor-facing blocker remaining

Co-authored-by: GuoJiaming <804436395@qq.com>
2026-05-19 21:48:17 -05:00
Josh Avant
e1c1c57242 Fix node approval scope requests (#84392)
* fix(cli): request node approval scopes

* docs(changelog): note node approval scope fix
2026-05-19 21:47:10 -05:00
Peter Steinberger
0556ac0291 fix(update): repair plugins for legacy updater doctors 2026-05-20 03:41:45 +01:00
clawsweeper[bot]
eb814b0216 Fix Codex image generation tool timeout (#84369)
Summary:
- The branch gives Codex `image_generate` dynamic-tool calls a 120s default watchdog in main and side-thread paths and updates docs, tests, and changelog.
- Reproducibility: yes. Source inspection on current main shows unconfigured Codex `image_generate` calls fall ... -tool default, and the linked source PR includes live Gateway before/after output for the timeout behavior.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8425…
- PR branch already contained follow-up commit before automerge: Fix Codex image generation tool timeout

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

Prepared head SHA: 10c7f87023
Review: https://github.com/openclaw/openclaw/pull/84369#issuecomment-4493288493

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: moritzmmayerhofer <254141390+moritzmmayerhofer@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 02:29:49 +00:00
Peter Steinberger
a002c416c7 fix(update): omit compatibility host env when package version is missing 2026-05-20 02:46:27 +01:00
Peter Steinberger
fd790e2977 chore(release): refresh generated release baselines 2026-05-20 02:46:27 +01:00
Peter Steinberger
6b82eaa2cd fix(update): carry candidate plugin API version through doctor 2026-05-20 02:36:52 +01:00
Gio Della-Libera
70e51b81cf fix(doctor): preserve unknown web search records (#83315)
* fix(doctor): preserve unknown web search records

* fix(doctor): filter dangerous web search keys

* fix(config): preserve extensible web search settings

* fix(config): keep legacy web search validation strict

* fix(config): reject blocked web search keys
2026-05-19 18:35:44 -07:00
clawsweeper[bot]
0e2a06ae10 fix(code-mode): sharpen exec tool description so models stop wasting turns rediscovering constraints (#84368)
Summary:
- The PR updates the code-mode exec tool description, adds regression coverage for the model-visible constraints, and records the fix in the changelog.
- Reproducibility: yes. at source level: current main's exec schema omits constraints that the current code-mo ...  also includes a live before/after recitation path showing the model receives the changed tool description.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(code-mode): cover exec tool guidance
- PR branch already contained follow-up commit before automerge: fix(code-mode): sharpen exec tool description so models stop wasting …

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

Prepared head SHA: 8ff85071ce
Review: https://github.com/openclaw/openclaw/pull/84368#issuecomment-4493273853

Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 01:25:04 +00:00
clawsweeper[bot]
6048cd43a5 fix(cron): keep recovered tool warnings diagnostic (#84308)
Summary:
- The PR threads middleware tool-error metadata into reply payloads, teaches cron outcome and diagnostics code to keep marked recovered warnings non-fatal, and adds focused regression coverage plus a changelog entry.
- Reproducibility: yes. Source inspection shows current main lacks a non-terminal recovered-warning path in cr ... fication, and the linked source PR includes a terminal runtime probe for the affected cron payload outcome.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cron): keep recovered tool warnings diagnostic

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

Prepared head SHA: 8b8a36e912
Review: https://github.com/openclaw/openclaw/pull/84308#issuecomment-4491925358

Co-authored-by: abnershang <abner.shang@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 01:19:52 +00:00
Peter Steinberger
d7896ed4c9 ci: retry release artifact downloads 2026-05-20 01:59:34 +01:00
Josh Avant
f6de2b3885 Fix Anthropic CLI auth routing for shorthand refs (#84374)
* Fix Anthropic CLI auth routing

* Add changelog for Anthropic CLI routing
2026-05-19 19:58:07 -05:00
Peter Steinberger
2a01fbb56c ci: keep ClawHub advisory for alpha publish 2026-05-20 01:57:00 +01:00
clawsweeper[bot]
7f8141ead9 fix(cron): use structured denial signals (#84311)
Summary:
- The PR changes isolated cron denial handling to use structured embedded tool-error metadata, preserves node-host denial wrappers, and updates cron docs, changelog, and focused regression tests.
- Reproducibility: yes. for source-level reproduction: current main scans cron summary, output, synthesized te ... denial tokens and promotes matches into fatal cron state. I did not execute tests in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cron): normalize node denial wrappers
- PR branch already contained follow-up commit before automerge: fix(cron): use structured denial signals
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8406…

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

Prepared head SHA: 047622fe8d
Review: https://github.com/openclaw/openclaw/pull/84311#issuecomment-4491946986

Co-authored-by: abnershang <abner.shang@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Abner Shang <75654486+abnershang@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 00:49:49 +00:00
clawsweeper[bot]
ab7aa88ef2 gateway: use identity.name in agent summaries when name is unset (#84355)
Summary:
- The PR updates Gateway agent summary builders to use `agents.list[].identity.name` when explicit `agents.list[].name` is absent, adds focused gateway regression tests, and records a changelog fix.
- Reproducibility: yes. Current main can be source-reproduced: both gateway summary builders set top-level `na ... list[].name`, so identity-only configured agents have no summary name for consumers that read `agent.name`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(gateway): cover missing agent summary names
- PR branch already contained follow-up commit before automerge: fix(gateway): remove stale name fallback import
- PR branch already contained follow-up commit before automerge: gateway: use identity.name in agent summaries when name is unset

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

Prepared head SHA: 9f7024f55c
Review: https://github.com/openclaw/openclaw/pull/84355#issuecomment-4493008710

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 00:45:08 +00:00
Peter Steinberger
4408e60c31 test(codex): avoid provider normalization in sandbox tool test 2026-05-20 01:38:37 +01:00
clawsweeper[bot]
165cc581cd fix(discord): preserve streamed replies after tool warnings (#84169)
* fix(discord): preserve previews after tool warnings

* fix(discord): preserve streamed replies after tool warnings

* test(discord): cover progress warning finalization

---------

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-19 19:36:28 -05:00
clawsweeper[bot]
ff5354ee4f fix(twitch): export clearRegistryForTest for cross-test isolation (#83887) (#84309)
Summary:
- The PR adds an async test-only Twitch client-manager registry reset helper, a focused registry isolation test, and an Unreleased changelog entry.
- Reproducibility: yes. Source inspection shows getOrCreateClientManager() returns the cached module-level manager for the same account id, and the repo’s Vitest configuration is explicitly non-isolated.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(twitch): export clearRegistryForTest for cross-test isolation (#8…

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

Prepared head SHA: 38c3fadc91
Review: https://github.com/openclaw/openclaw/pull/84309#issuecomment-4491930986

Co-authored-by: HCL <chenglunhu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-20 00:26:21 +00:00
Josh Avant
00da318350 fix: constrain wildcard subagent targets (#84357)
* fix subagent wildcard targets

* add changelog for subagent wildcard fix
2026-05-19 19:21:13 -05:00
Peter Steinberger
eea71708ac test(release): update workflow concurrency expectations 2026-05-20 01:16:43 +01:00
Peter Steinberger
79197b3196 ci(release): stabilize beta validation gates 2026-05-20 00:53:32 +01:00
Peter Steinberger
d0bc520de8 test(plugins): update prerelease shard expectations 2026-05-20 00:53:32 +01:00
pash-openai
e0d1a2a9b9 Move Codex soul context to developer instructions (#84331)
* Move Codex soul context to developer instructions

* Route Codex workspace context by lifetime

* Refresh Codex prompt snapshots

* Update prompt snapshot expectations

* Fix Codex workspace context diagnostics
2026-05-19 16:47:32 -07:00
Gio Della-Libera
68c5a892d0 fix(config): dedupe missing official plugin warnings (#84227) 2026-05-19 16:44:21 -07:00
Peter Steinberger
375afbad2d ci: cancel duplicate Tideclaw alpha release runs 2026-05-20 00:42:39 +01:00
Dave Morin
a00e7d3898 docs: clarify xai oauth setup (#84350) 2026-05-19 16:33:18 -07:00
100menotu001
1bb0ebab0b Expose messageId in message CLI JSON output (#84191)
Summary:
- The PR promotes direct or nested send receipt IDs into `openclaw message send --json`, adds a focused command test, and adds a changelog entry.
- Reproducibility: yes. at source level. Current main serializes only the raw payload while send receipts can carry `payload.result.messageId`; I did not execute the CLI in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(cli): fix message json payload type
- PR branch already contained follow-up commit before automerge: chore: retrigger PR checks
- PR branch already contained follow-up commit before automerge: Expose messageId in message CLI JSON output

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

Prepared head SHA: 9eba815fcc
Review: https://github.com/openclaw/openclaw/pull/84191#issuecomment-4489100591

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
Co-authored-by: Craig <froelich@craigs.mac.studio.froho>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-19 23:30:24 +00:00
clawsweeper[bot]
97aa0c8c01 Preserve disabled Discord presentation buttons (#84312)
Summary:
- Adds `disabled` to the message presentation button schema, advertises Discord disabled-button support, prese ... through Discord component mapping and link serialization, and adds regression tests plus a changelog entry.
- Reproducibility: yes. Source inspection on current main shows `disabled` exists in the runtime type but is a ... rtised in Discord capabilities, dropped by adaptation, and omitted from Discord mapping/link serialization.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(discord): advertise disabled presentation support
- PR branch already contained follow-up commit before automerge: fix(discord): preserve disabled link buttons
- PR branch already contained follow-up commit before automerge: Preserve disabled Discord presentation buttons

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

Prepared head SHA: 9bb60d8cbf
Review: https://github.com/openclaw/openclaw/pull/84312#issuecomment-4491983845

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 23:29:48 +00:00
clawsweeper[bot]
e61fe1c539 feat(ui): tool name style in usage panel (#84310)
Summary:
- This PR adds scoped truncation and hover titles to usage-panel context-breakdown names and adds a changelog entry crediting the source PR.
- Reproducibility: yes. at source/proof level: current main renders long context names without truncation or t ... he overflow before and ellipsis/tooltip after. I did not run a live browser session in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: feat(ui): tool name style in usage panel

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

Prepared head SHA: 396e405b3b
Review: https://github.com/openclaw/openclaw/pull/84310#issuecomment-4491942108

Co-authored-by: Rain120 <1085131904@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 23:25:00 +00:00
Dallin Romney
88d8d6af93 perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts (#84283)
* perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts

Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult
through the remaining helpers that still call discoverOpenClawPlugins
internally during startup:

- loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts):
  add discovery? to PluginLoadOptions and consult it before falling back to
  an internal scan at both call sites.

- loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept
  discovery? as a more ergonomic alternative to the existing candidates? /
  diagnostics? pair; candidates? still wins when both are supplied.

- resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts):
  add discovery? to LoadInstalledPluginIndexParams and use it when
  candidates aren't supplied.

- resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add
  discovery? and thread it into the bundled-fallback discovery call.

Add discovery-threading.test.ts asserting each entry point skips its
internal discoverOpenClawPlugins call when discovery is supplied, calls it
when nothing is supplied, and prefers explicit candidates over discovery
when both are present (6 tests, all pass).

discoverOpenClawPlugins remains stateless; sharing is function-scoped per
src/plugins/CLAUDE.md guidance. Backward compatible: every change is
additive (new optional param).

* perf(plugins): drop verbose JSDoc from discovery? params
2026-05-19 16:22:30 -07:00
Thiago Costa
b9a2c11521 fix(clawhub): preserve base URL path prefix [AI-assisted] (#83982)
Summary:
- The PR updates `src/infra/clawhub.ts` URL joining, adds a path-prefix regression test in `src/infra/clawhub.test.ts`, and adds a changelog bullet.
- Reproducibility: yes. Source inspection plus a direct Node URL check show current main drops `/clawhub` when resolving a leading-slash API path against a prefixed base URL.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(clawhub): preserve base URL path prefix [AI-assisted]

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

Prepared head SHA: 7bb2cb8764
Review: https://github.com/openclaw/openclaw/pull/83982#issuecomment-4484348274

Co-authored-by: Thiago Costa <thiago12_fera@hotmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 23:22:01 +00:00
Kevin Lin
ecb6da9289 docs: move codex native plugins nav (#84341) 2026-05-19 15:27:51 -07:00
Eva
5c9a8f33b3 fix(plugins): add default timeout for before_compaction/after_compaction hooks (#84153)
Merged via squash.

Prepared head SHA: 41fa5fed37
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-19 15:14:15 -07:00
Extra Small
d7b23d5bca fix(cli): honor --no-prefix-cwd in acp
Fixes #83901. Honors Commander negated option handling for ACP prompt-prefix forwarding and adds focused CLI regression coverage. Verified with Crabbox AWS cbx_1689d0ad78e9 run run_a406418db6fe and Real behavior proof run 26127392365.
2026-05-19 15:10:17 -07:00
Eva
a059309a9f fix(agents): bound plugin-owned context-engine compaction with a safety timeout (#84083)
Merged via squash.

Prepared head SHA: 9121a1a5ea
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-19 14:49:00 -07:00
Andy Ye
3bc728eaa9 fix(twitch): register chat intent for refreshing auth (#83750)
Summary:
- The PR registers Twitch refreshing-token users with Twurple's chat intent and adds regression coverage for that contract.
- Reproducibility: yes. by source and dependency contract. Current main does not register the chat intent, and ...  RefreshingAuthProvider only resolves getAccessTokenForIntent('chat') when that intent is mapped to a user.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(twitch): register chat intent for refreshing auth

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

Prepared head SHA: 1fdadcff04
Review: https://github.com/openclaw/openclaw/pull/83750#issuecomment-4481748086

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-19 21:20:25 +00:00
Alex Knight
c81271ee6e Fix managed Gateway updates across CLI and service Node skew (#84043)
Summary:
- The PR pins managed Gateway package updates, runtime preflight, post-install doctor, post-core update, service refresh, and restart follow-ups to the Node binary and package root baked into the Gateway service.
- Reproducibility: yes. source-level. Current main validates and follows up with the shell process Node in the ...  body provides a concrete two-Node Docker reproduction, though I did not execute it in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(update): detect service node mismatch even when package roots match
- PR branch already contained follow-up commit before automerge: fix(update): pin package install to service root when nodes differ wi…

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

Prepared head SHA: 5607e441f6
Review: https://github.com/openclaw/openclaw/pull/84043#issuecomment-4485613931

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: Alex Knight <aknight@atlassian.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: amknight
Co-authored-by: amknight <15041791+amknight@users.noreply.github.com>
2026-05-19 20:44:29 +00:00
Dallin Romney
3d96111a5a Revert "perf(plugins): extend discovery threading to loader, manifest registr…" (#84278)
This reverts commit f5f0b2c7c9.
2026-05-19 12:35:27 -07:00
Dallin Romney
f5f0b2c7c9 perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts (#84258)
Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult
through the remaining helpers that still call discoverOpenClawPlugins
internally during startup:

- loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts):
  add discovery? to PluginLoadOptions and consult it before falling back to
  an internal scan at both call sites.

- loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept
  discovery? as a more ergonomic alternative to the existing candidates? /
  diagnostics? pair; candidates? still wins when both are supplied.

- resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts):
  add discovery? to LoadInstalledPluginIndexParams and use it when
  candidates aren't supplied.

- resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add
  discovery? and thread it into the bundled-fallback discovery call.

Add discovery-threading.test.ts asserting each entry point skips its
internal discoverOpenClawPlugins call when discovery is supplied, calls it
when nothing is supplied, and prefers explicit candidates over discovery
when both are present (6 tests, all pass).

discoverOpenClawPlugins remains stateless; sharing is function-scoped per
src/plugins/CLAUDE.md guidance. Backward compatible: every change is
additive (new optional param).
2026-05-19 12:35:01 -07:00
Sebastien Tardif
28beea9e88 perf(plugins): thread explicit discovery to avoid redundant filesystem walks (#75451)
Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry,
resolveBundledPluginSources, and listChannelCatalogEntries so callers
that already hold a PluginDiscoveryResult can skip redundant filesystem
walks.

In contracts/registry.ts, the retry loop in
loadScopedCapabilityRuntimeRegistryEntries computes discovery once
and shares it across retry attempts (function-scoped, not module-scoped).

discoverOpenClawPlugins() itself remains stateless with no hidden cache.

Closes #82308

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-19 12:15:33 -07:00
Nimrod Gutman
edd7c8e4a1 [codex] fix iOS TestFlight release archive (#84255)
Merged via squash.

Prepared head SHA: c59a81a4bf
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-19 22:04:33 +03:00
Kevin Lin
9b97e1ef2f feat(codex): add plugin list enable disable commands (#83293)
* feat(codex): add plugin enable disable list commands

* fix(codex): escape plugin management output

* test(codex): narrow plugin command coverage

* fix(codex): gate plugin management writes

* test(codex): type command plugin context

* docs(codex): document plugin management commands
2026-05-19 11:39:50 -07:00
Nimrod Gutman
94d8391c03 [codex] restore QR bootstrap operator handoff (#83684)
Merged via squash.

Prepared head SHA: 2dc955cfb7
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-19 20:59:09 +03:00
Md. Al-Mosabbir Rakib
e00cb664ad docs: clarify /new vs /reset semantics in slash-commands (#81073)
Summary:
- The PR changes one bullet in `docs/tools/slash-commands.md` to distinguish `/new` from `/reset` and remove the misleading alias wording.
- Reproducibility: yes. Reading current main reproduces the misleading docs line at `docs/tools/slash-commands.md:127`, and adjacent source/tests show `/new` and `/reset` take different paths in the Control UI.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs/slash-commands: drop inaccurate Control UI/ACP cross-reference (…
- PR branch already contained follow-up commit before automerge: Merge branch 'main' into docs/fix-reset-alias-misleading

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

Prepared head SHA: bb92b6050a
Review: https://github.com/openclaw/openclaw/pull/81073#issuecomment-4432165259

Co-authored-by: Md. Al-Mosabbir Rakib <mrakib50.cse@gmail.com>
Co-authored-by: Md. Al-Mosabbir Rakib <34891461+mosabbirrakib@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 17:51:48 +00:00
samzong
323c9760d3 [Docs] Document gateway benchmark probes (#83866)
Summary:
- The PR updates `docs/cli/gateway.md` and `docs/reference/test.md` to document Gateway startup/restart benchmark prerequisites, commands, case IDs, probes, output semantics, and platform limits.
- Reproducibility: not applicable. as a runtime bug; docs correctness is source-checkable against the benchmar ... ipts, and readiness source. The current PR head corrected the earlier startup-hook readiness wording issue.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(gateway): correct benchmark readiness wording

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

Prepared head SHA: 5bd0f6c463
Review: https://github.com/openclaw/openclaw/pull/83866#issuecomment-4483820005

Co-authored-by: samzong <samzong.lu@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 17:50:36 +00:00
Ayaan Zaidi
edcf862da5 fix(mantis): finish interrupted telegram proof sessions 2026-05-19 22:59:50 +05:30
Shakker
78d226bb3b docs: add provider timeout changelog entry 2026-05-19 17:16:48 +01:00
Shakker
6899eff155 test: cover provider timeout bare hostnames 2026-05-19 17:16:48 +01:00
yujiawei
9e9feb52f4 fix(llm-idle-timeout): honor models.providers.<id>.timeoutSeconds for cloud providers
The schema.help text for `models.providers.*.timeoutSeconds` documents the
key as the user-facing knob for "slow local or self-hosted model servers".
In practice the option is also the only configurable lever for the LLM
idle/first-token watchdog. However `resolveLlmIdleTimeoutMs` was still
running the explicit provider timeout through `clampImplicitTimeoutMs`,
clamping it back down to the implicit ~120s `DEFAULT_LLM_IDLE_TIMEOUT_MS`
ceiling for any non-cron, non-local provider.

Consequence (matches #77744 and #78361):
- User sets `models.providers.llamacpp.timeoutSeconds: 14400` (or 600 for
  a slow Gemini/Opus turn with a large tool payload).
- Hot reload accepts the value, runtime resolves
  `modelRequestTimeoutMs = 14_400_000`.
- Idle watchdog still trips at ~120s with
  "LLM idle timeout (120s): no response from model", aborting an
  otherwise-healthy upstream that is mid-prefill or buffering thinking
  tokens.

Fix: when the caller passes an explicit `modelRequestTimeoutMs`
(sourced from `models.providers.<id>.timeoutSeconds` /
`model.requestTimeoutMs`), treat it as a deliberate ceiling for cloud
providers too. The run-timeout / agent-timeout bounds still apply via
`timeoutBounds`, so a shorter explicit run timeout always wins. The
implicit default watchdog still kicks in when the user has not set a
provider timeout, preserving the network-silence-as-hang guard for
default configs.

Updated the two corresponding test cases that asserted the old
clamp-on-cloud behavior; all 71 tests in `llm-idle-timeout.test.ts`
and the wider 430-test `src/agents/pi-embedded-runner/run/` lane pass.
Schema help text refreshed to call out that the same knob raises the
idle watchdog ceiling.

Refs: #77744, #78361
2026-05-19 17:16:48 +01:00
Pavan Kumar Gondhi
48acdd3d85 harden update restart script creation [AI] (#84088)
* fix: harden update restart script creation

* docs: add changelog entry for PR merge
2026-05-19 21:05:37 +05:30
YuanHanzhong
d0f7c8fa28 fix(docker): keep codex plugin in release images 2026-05-19 10:36:50 -04:00
hcl
5d19beb547 fix(cli): format acp client errors with formatErrorMessage (#83904) (#84080)
Summary:
- The PR changes `openclaw acp client` error handling to use `formatErrorMessage`, adds a plain-object rejection regression test, and adds a changelog entry.
- Reproducibility: yes. Current main visibly sends `openclaw acp client` caught errors through `String(err)`,  ...  catch already uses `formatErrorMessage`; I did not run a live failing ACP server in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cli): format acp client errors with formatErrorMessage (#83904)

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

Prepared head SHA: 69ef0e7270
Review: https://github.com/openclaw/openclaw/pull/84080#issuecomment-4486666922

Co-authored-by: HCL <chenglunhu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 14:34:51 +00:00
Bob
13c97c5a8d feat(agents): support per-agent local model lean mode (#84073)
Summary:
- The branch adds per-agent `agents.list[].experimental.localModelLean` config and applies lean tool filtering through agent, session, and default-agent resolution.
- Reproducibility: not applicable. this is a feature/config PR rather than a current-main bug report. The chan ... or is supported by source review, focused tests in the branch, and the PR body's redacted live runtime log.

Automerge notes:
- PR branch already contained follow-up commit before automerge: feat(agents): support per-agent local model lean mode
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8407…

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

Prepared head SHA: 1f9a9554da
Review: https://github.com/openclaw/openclaw/pull/84073#issuecomment-4486397570

Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-19 14:11:38 +00:00
Thomas Krohnfuß
b7ba7c3f2a fix(cli): preserve first line of channels logs at window boundary (#84106)
Summary:
- The PR updates `openclaw channels logs` tail-window reading to keep a complete first line when the 1 MB window starts on a newline boundary, adds a regression test, and adds a changelog entry.
- Reproducibility: yes. Source inspection on current main shows the unconditional first-line drop, and the PR  ... s provide terminal before/after CLI output for a 2 MB log whose tail window starts exactly after a newline.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into fix/channels-logs-dro…
- PR branch already contained follow-up commit before automerge: fix(cli): preserve first line of channels logs at window boundary

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

Prepared head SHA: 284b312b31
Review: https://github.com/openclaw/openclaw/pull/84106#issuecomment-4487313048

Co-authored-by: BSG2000 <bsg2000@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 13:27:43 +00:00
clawsweeper[bot]
f07c87405c Fix config queue overrides for Matrix (#84104)
Summary:
- The branch updates queue-by-channel config schema/types for Matrix, Google Chat, and Mattermost, refreshes config baseline hashes, adds config/schema regression tests, and records the user-visible fix in the changelog.
- Reproducibility: yes. Source inspection gives a high-confidence path: current main's strict `messages.queue. ... matrix`, and the linked source PR records the same config failing before the patch and validating after it.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

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

Prepared head SHA: 3865178550
Review: https://github.com/openclaw/openclaw/pull/84104#issuecomment-4487285061

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 12:05:46 +00:00
clawsweeper[bot]
03d774d6d8 [codex] Fix Control UI terminal run status recovery (#84112)
Summary:
- Adds shared Control UI session-run active-state handling, applies terminal-status precedence in chat/session rendering and lifecycle recovery, and adds focused regressions plus a changelog entry.
- Reproducibility: yes. Current main has a source-visible path where `status: "done"` plus stale `hasActiveRun ... eeps abort/in-progress UI alive, and the linked proof exercises the fixed stale-terminal state in Chromium.

Automerge notes:
- PR branch already contained follow-up commit before automerge: [codex] Fix Control UI terminal run status recovery

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

Prepared head SHA: f9f503add0
Review: https://github.com/openclaw/openclaw/pull/84112#issuecomment-4487409085

Co-authored-by: NianJiuZst <3235467914@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 11:56:09 +00:00
clawsweeper[bot]
4e60ad7212 fix(media): decode remote URL fallback filenames (#84108)
Summary:
- This replacement PR decodes valid percent escapes in remote media URL fallback basenames, replaces decoded s ... scores, preserves malformed escapes, adds `saveRemoteMedia` regression coverage, and updates the changelog.
- Reproducibility: yes. Source inspection plus a Node check on current main show the URL path basename remains `My%20Report.pdf`, and the linked source PR supplies after-fix runtime proof through `saveRemoteMedia`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(media): decode remote URL fallback filenames

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

Prepared head SHA: 8cbac43f9b
Review: https://github.com/openclaw/openclaw/pull/84108#issuecomment-4487334097

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 11:42:16 +00:00
clawsweeper[bot]
d916f176e1 fix(cli): preserve equals in root option values [AI-assisted] (#84107)
Summary:
- This PR updates CLI root option parsing to preserve embedded equals signs, adds focused Vitest coverage for inline and space-separated values, and records the fix in the changelog.
- Reproducibility: yes. by source inspection: current main uses `raw.split("=", 2)`, so `--token=abc=def` returns only `abc`; the PR body also supplies after-fix live output for the same path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cli): preserve equals in root option values [AI-assisted]

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

Prepared head SHA: 8a15801e79
Review: https://github.com/openclaw/openclaw/pull/84107#issuecomment-4487314163

Co-authored-by: Thiago Costa <thiago12_fera@hotmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 11:37:35 +00:00
hcl
e2c8e7c8ae fix(cli): reject out-of-range port numbers in parsePort (#83900) (#84008)
Summary:
- The PR adds a 65,535 upper-bound check to the shared CLI `parsePort` helper, a colocated regression test, and a changelog entry for the linked port-range bug.
- Reproducibility: yes. Source inspection on current main shows `parsePort('99999')` delegates to `parseStrict ... sitive safe integer, so the return would be `99999`; I did not execute it because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cli): reject out-of-range port numbers in parsePort (#83900)

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

Prepared head SHA: 9ad0705c44
Review: https://github.com/openclaw/openclaw/pull/84008#issuecomment-4484883200

Co-authored-by: HCL <chenglunhu@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 11:36:12 +00:00
clawsweeper[bot]
d7083bab4c chore: move Motivation section above Change Type in PR template (#84098)
Summary:
- This PR moves the existing Motivation section in `.github/pull_request_template.md` from below Linked Issue/PR to immediately after Summary without changing the section text.
- Reproducibility: not applicable. this is a PR-template ordering cleanup, not a runtime bug. Source inspection of current main and the PR head verifies the before/after section order.

Automerge notes:
- PR branch already contained follow-up commit before automerge: chore: move Motivation section above Change Type in PR template

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

Prepared head SHA: 6c68583fac
Review: https://github.com/openclaw/openclaw/pull/84098#issuecomment-4487082864

Co-authored-by: Huan Jiang <seraphjiang@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 11:05:57 +00:00
clawsweeper[bot]
5e0850fc54 fix(ollama): default unknown capabilities to tools (#84075)
Summary:
- The branch makes unknown-capabilities Ollama model definitions explicitly tool-capable, adds regression assertions and changelog text, and guards the issue-labeler job to run only on issue events.
- Reproducibility: yes. for the metadata gap: current main builds unknown-capabilities Ollama models without a ... er-fix live provider output with `supportsTools: true`. I did not run local tests in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(ollama): default unknown capabilities to tools

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

Prepared head SHA: 27527716c0
Review: https://github.com/openclaw/openclaw/pull/84075#issuecomment-4486492661

Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-19 09:51:24 +00:00
clawsweeper[bot]
1c1c75df72 fix(memory): close local embedding providers on timeout (#84048)
Summary:
- The branch adds a close lifecycle for local memory embedding providers, scoped memory search/index teardown for one agent, Active Memory timeout cleanup, focused tests, and a changelog entry.
- Reproducibility: yes. The linked issue gives a concrete OpenClaw 2026.5.18 Telegram Active Memory timeout pa ... current-main source inspection confirms there is no timeout cleanup for that local embedding provider path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory): close local embedding providers on timeout

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

Prepared head SHA: 8e2e369b5c
Review: https://github.com/openclaw/openclaw/pull/84048#issuecomment-4485705481

Co-authored-by: brokemac79 <martin_cleary@yahoo.co.uk>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-19 09:19:09 +00:00
Sarah Fortune
aef93881af fix(installer): preserve windows onboarding tui (#84044)
Co-authored-by: sjf-oa <sjf-oa@users.noreply.github.com>
2026-05-19 01:12:26 -07:00
Ayaan Zaidi
1d77170a30 fix(xai): use public plugin test runtime (#84005) (thanks @fuller-stack-dev) 2026-05-19 12:35:54 +05:30
Ayaan Zaidi
6ee60fcfe2 fix(xai): add device code changelog (#84005) (thanks @fuller-stack-dev) 2026-05-19 12:35:54 +05:30
Ayaan Zaidi
b66e91ba77 fix(xai): decouple device code discovery 2026-05-19 12:35:54 +05:30
FullerStackDev
896fd13b1c feat(xai): add device code oauth login 2026-05-19 12:35:54 +05:30
Galin Iliev
ddeaebfc68 fix(agents): add trajectory flush timeout diagnostics
Adds bounded queued-writer diagnostics to pi-trajectory-flush cleanup timeout warnings so operators can see pending write count, queued bytes, active operation, and append size without exposing paths or payloads.

Closes #82961
2026-05-18 23:29:37 -07:00
Galin Iliev
04eac15f43 fix: recover stale subagent completion announces
Recover stale subagent completion delivery by retrying unsupported transcript-wait wakes without transcript waiting and forcing the existing message-tool handoff when the requester run is stale and direct completion is invisible.\n\nAdds regression coverage for the stale wake sequence and records the maintainer changelog entry.\n\nFixes #83699.
2026-05-18 23:09:20 -07:00
Galin Iliev
754b4234cb fix(agents): ignore duplicate embedded run clears
* fix(agents): ignore duplicate embedded run clears

* test(agents): fix embedded run clear lint

* docs(changelog): note embedded run clear fix

---------

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
2026-05-18 23:05:37 -07:00
Ayaan Zaidi
cf235b209f chore(android): bump play upload version code 2026-05-19 11:06:12 +05:30
Ayaan Zaidi
ac07701833 fix(android): remove photo library access from play build 2026-05-19 11:06:12 +05:30
Gio Della-Libera
ff871e162a fix(config): allow bundled provider timeout overlays (#83267)
* fix config provider timeout overlays

Allow bundled model provider config entries to act as overlays so fields like timeoutSeconds can be configured without redeclaring baseUrl and models. Keep unknown custom provider declarations strict, and guard configured-provider fallback against overlay entries without models.

* fix(config): include provider aliases in model overlays

* fix(config): guard Foundry timeout overlays

* fix(config): normalize bundled provider overlays

* fix(models): reject overlay-only fallback models
2026-05-18 21:50:10 -07:00
Ayaan Zaidi
f0a86450b1 fix(mantis): release interrupted telegram proof leases 2026-05-19 09:58:34 +05:30
Patrick Erichsen
d60ab48511 Add Telegram progress preview flows (#83847)
* feat(telegram): add progress preview flow tooling

* docs: add channel flow preview skill

* test(telegram): exercise native draft flow fixture

* fix(telegram): remove progress label ellipsis animation

* fix(telegram): address progress preview review
2026-05-18 21:23:55 -07:00
clawsweeper[bot]
b86435f0b5 fix: forward-port Tideclaw alpha release fixes
Forward-port Tideclaw alpha stabilization fixes from the 2026-05-19 nightly release branch.
2026-05-19 04:13:38 +00:00
clawsweeper[bot]
6fcfeed5dc fix: include gateway plugin commands in TUI autocomplete (#83941)
Summary:
- The PR adds TUI-side Gateway `commands.list` fetching, dynamic slash-command merging, backend typing/tests, and a changelog entry so Gateway-connected TUI sessions suggest plugin-owned slash commands.
- Reproducibility: yes. Source inspection shows current main builds TUI autocomplete without any `commands.lis ... y exposes text-scope plugin commands, and the source PR supplies after-fix command output plus screenshots.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: include gateway plugin commands in TUI autocomplete
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8364…

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

Prepared head SHA: 2eba76a42d
Review: https://github.com/openclaw/openclaw/pull/83941#issuecomment-4484023526

Co-authored-by: Se7en <se7en-agent@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 03:55:26 +00:00
clawsweeper[bot]
b2f9f197a5 fix(whatsapp): clarify inbound group diagnostics (#83969)
Summary:
- The PR updates WhatsApp inbound listener and group-drop diagnostics, adds focused tests, and documents that observed but unregistered groups must be admitted through `channels.whatsapp.groups`.
- Reproducibility: yes. from source inspection: current main still emits the DM-only startup log and vague gro ... sions/whatsapp/src/auto-reply/monitor.ts` and `extensions/whatsapp/src/auto-reply/monitor/group-gating.ts`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(whatsapp): clarify group drop guidance
- PR branch already contained follow-up commit before automerge: fix(whatsapp): make inbound diagnostics policy-aware
- PR branch already contained follow-up commit before automerge: fix(whatsapp): clarify inbound group diagnostics

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

Prepared head SHA: 0da24e3bbb
Review: https://github.com/openclaw/openclaw/pull/83969#issuecomment-4484218945

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 03:53:56 +00:00
Ayaan Zaidi
6da73ac90f fix(mantis): wait for telegram desktop bootstrap 2026-05-19 09:15:15 +05:30
Josh Avant
cc835b6d72 fix(agents): preserve bare Telegram reply context (#83953)
* fix(agents): preserve bare reply context

* docs: add Telegram reply context changelog
2026-05-18 22:33:48 -05:00
clawsweeper[bot]
cbaf858227 fix: retry config snapshot after rejection (#83944)
Summary:
- This PR clears the cached CLI config snapshot promise when a read rejects, adds a reject-retry-cache regression test, and adds an Unreleased changelog entry.
- Reproducibility: yes. Current main clearly caches the first snapshot-read promise, and the source PR supplied a focused reject, recover, cached-success probe; I did not rerun it in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: retry config snapshot after rejection

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

Prepared head SHA: a46b5ec5c7
Review: https://github.com/openclaw/openclaw/pull/83944#issuecomment-4484051060

Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-19 03:28:26 +00:00
Ayaan Zaidi
1f794d2816 fix(mantis): skip zombie telegram queue runs 2026-05-19 08:45:31 +05:30
Josh Avant
ba7ce3c6b9 Fix stuck Codex-native subagent tasks after blocked spawn (#83945)
* fix codex native subagent terminal mirror

* add changelog for codex subagent terminal mirror
2026-05-18 22:12:43 -05:00
Galin Iliev
57ec361682 fix(agents): skip dormant completion wake probes
Skip embedded-run wake/steer attempts for dormant completion requesters and keep late subagent completions on the requester-agent/direct handoff path.\n\nAlso records the missing regression assertion that dormant completion requesters do not call queueEmbeddedPiMessageWithOutcome and adds the maintainer changelog entry.\n\nVerification:\n- node scripts/run-vitest.mjs src/agents/subagent-announce-delivery.test.ts\n- git diff --check\n- Codex autoreview via local Copilot endpoint: no actionable regressions\n- CI on 0108ebb2b3: clean
2026-05-18 20:07:36 -07:00
Ayaan Zaidi
d75e16a1b9 fix(mantis): ignore stale telegram queue blockers 2026-05-19 08:32:26 +05:30
1529 changed files with 82579 additions and 19071 deletions

View File

@@ -23,10 +23,11 @@ Use when:
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- Keep going until the selected review path returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun the review helper.
- Default to Codex review. If Codex is unavailable or exits with an error, the helper falls back to the first configured CLI from `claude -p`, `pi -p`, `opencode run`, `droid exec`, or `copilot`. Prefer Codex for final closeout because it uses native review mode; non-Codex reviewers use a Codex-inspired generated diff prompt. The helper runs nested Codex review in yolo/full-access mode by default; use `--no-yolo` only when intentionally testing sandbox behavior.
- Default to Codex review with no fallback. Prefer Codex for final closeout because it uses native review mode; non-Codex reviewers use a Codex-inspired generated diff prompt. Use `--fallback-reviewer auto|claude|pi|opencode|droid|copilot` only when a second-model fallback is explicitly wanted and authenticated. The helper runs nested Codex review in yolo/full-access mode by default; use `--no-yolo` only when intentionally testing sandbox behavior.
- Stop as soon as the review command/helper exits 0 with no accepted/actionable findings. Do not run an extra direct `codex review` just to get a nicer "clean" line, a second opinion, or clearer closeout wording.
- Treat the helper's successful exit plus absence of actionable findings as the clean review result, even if the underlying Codex CLI output is terse.
- If rejecting a finding as intentional/not worth fixing, add a brief inline code comment only when it explains a real invariant or ownership decision that future reviewers should know.
- If creating or updating a PR while rejecting any autoreview finding, record the rejected finding and reason in the PR description so later reviewers can distinguish intentional design decisions from missed review output.
- Do not push just to review. Push only when the user requested push/ship/PR update.
- For OpenClaw maintainers, keep autoreview validation Crabbox/Testbox-aware when maintainer validation mode is enabled (`OPENCLAW_TESTBOX=1` or `AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION=1`). A review pass may inspect files and run cheap non-Node probes, but it must not start local `pnpm`, Vitest, `tsgo`, `npm test`, or `node scripts/run-vitest.mjs` from a Codex/worktree review unless the operator explicitly requested local proof. For runtime proof, use existing evidence or route through Crabbox/Testbox and report the id. Do not apply this rule to ordinary contributors who do not have maintainer Testbox access.
@@ -51,11 +52,11 @@ git fetch origin
codex review --base origin/main
```
Do not pass any prompt with `--base`. Some Codex CLI versions reject both inline
and stdin prompt forms, including the helper's `codex review --base <ref> -`,
with `--base <BRANCH> cannot be used with [PROMPT]`. If the helper hits this
error, run plain `codex review --base <ref>` and report that the helper prompt
injection was skipped.
Do not pass any prompt with `--base`, `--commit`, or `--uncommitted`. Codex CLI
review targets and custom review prompts are mutually exclusive: target modes
generate their own review prompt internally. Use plain target review for native
Codex closeout, or use custom prompt review (`codex review -`) only when you
intentionally want a generated diff prompt instead of native target review.
If an open PR exists, use its actual base:
@@ -116,13 +117,13 @@ The helper:
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
- supports `--reviewer codex|claude|pi|opencode|droid|copilot|auto`; `auto` means Codex first
- supports `--fallback-reviewer auto|claude|pi|opencode|droid|copilot|none`; default is configured CLI fallback
- supports `--fallback-reviewer auto|claude|pi|opencode|droid|copilot|none`; default is `none`
- falls back only when Codex is unavailable or exits nonzero, not when Codex reports findings
- writes only to stdout unless `--output` or `AUTOREVIEW_OUTPUT` is set
- supports `--dry-run`, `--parallel-tests`, and commit refs
- runs nested review with `--dangerously-bypass-approvals-and-sandbox --sandbox danger-full-access` by default
- injects maintainer-only OpenClaw validation policy into native Codex review when `OPENCLAW_TESTBOX=1` or `AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION=1`, so local memory-heavy Node/Vitest checks are avoided in favor of Crabbox/Testbox proof
- branch mode may fail on Codex CLI versions that reject `--base` plus the helper's stdin prompt; on that exact parser error, rerun plain `codex review --base <ref>` instead of falling back to a non-Codex reviewer
- with `OPENCLAW_TESTBOX=1` or `AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION=1`, disables auto local `pnpm run check` and routes Codex through generated prompt review (`codex review -`) so the no-local-heavy-tests policy is included; native Codex target review cannot accept extra prompt text
- non-Codex reviewers receive the generated diff prompt and maintainer validation policy text when maintainer validation is active
- keeps accepting `--full-access`; use `--no-yolo` or `AUTOREVIEW_YOLO=0` to opt out
- still accepts legacy `CODEX_REVIEW_*` env vars when the matching `AUTOREVIEW_*` var is unset
- prints `autoreview clean: no accepted/actionable findings reported` when the selected review command exits 0

View File

@@ -11,9 +11,9 @@ Options:
--base REF Base ref for branch review. Default: PR base or origin/main.
--commit REF Commit ref for commit review. Default: HEAD.
--reviewer codex|claude|pi|opencode|droid|copilot|auto
Review engine. Default: Codex with configured fallback on error.
Review engine. Default: Codex.
--fallback-reviewer auto|claude|pi|opencode|droid|copilot|none
Fallback when Codex is unavailable or exits nonzero. Default: auto.
Fallback when Codex is unavailable or exits nonzero. Default: none.
--codex-bin PATH Codex binary. Default: codex.
--claude-bin PATH Claude binary. Default: claude.
--pi-bin PATH Pi binary. Default: pi.
@@ -44,7 +44,7 @@ mode=auto
base_ref=
commit_ref=HEAD
reviewer=${AUTOREVIEW_REVIEWER:-${CODEX_REVIEW_REVIEWER:-auto}}
fallback_reviewer=${AUTOREVIEW_FALLBACK_REVIEWER:-${CODEX_REVIEW_FALLBACK_REVIEWER:-auto}}
fallback_reviewer=${AUTOREVIEW_FALLBACK_REVIEWER:-${CODEX_REVIEW_FALLBACK_REVIEWER:-none}}
codex_bin=${CODEX_BIN:-codex}
claude_bin=${CLAUDE_BIN:-claude}
pi_bin=${PI_BIN:-pi}
@@ -58,7 +58,6 @@ parallel_tests=
parallel_tests_auto=false
dry_run=false
codex_review_prompt=
codex_review_stdin_prompt=false
codex_review_prompt_file=false
openclaw_maintainer_validation=${AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION:-${OPENCLAW_TESTBOX:-0}}
@@ -249,13 +248,8 @@ OpenClaw maintainer autoreview validation policy:
- If remote validation is not necessary for the finding, state the targeted proof that should be run instead of starting local tests.
EOF
)
if [[ "$review_kind" == local ]]; then
review_cmd+=(-)
codex_review_stdin_prompt=true
else
review_cmd=("$codex_bin" "${codex_args[@]}" review -)
codex_review_prompt_file=true
fi
review_cmd=("$codex_bin" "${codex_args[@]}" review -)
codex_review_prompt_file=true
fi
printf 'autoreview target: %s\n' "$review_kind"
@@ -278,8 +272,8 @@ if [[ "$reviewer" == auto || "$reviewer" == codex ]]; then
printf 'review:'
printf ' %q' "${review_cmd[@]}"
printf '\n'
if [[ "$codex_review_stdin_prompt" == true || "$codex_review_prompt_file" == true ]]; then
printf 'review policy: OpenClaw maintainer Crabbox/Testbox-aware validation prompt injected\n'
if [[ "$codex_review_prompt_file" == true ]]; then
printf 'review policy: OpenClaw maintainer validation active; using generated prompt review because Codex target review cannot accept extra prompt text\n'
fi
else
printf 'review: %s prompt review\n' "$reviewer"
@@ -337,11 +331,8 @@ run_review() {
rm -f "$prompt_file"
prompt_file=
return "$status"
elif [[ "$codex_review_stdin_prompt" == true ]]; then
printf '%s\n' "$codex_review_prompt" | "${review_cmd[@]}" 2>&1 | tee "$review_output"
else
"${review_cmd[@]}" 2>&1 | tee "$review_output"
fi
"${review_cmd[@]}" 2>&1 | tee "$review_output"
}
diff_for_review() {

View File

@@ -0,0 +1,44 @@
---
name: channel-message-flows
description: "Use when previewing local channel message flow fixtures."
---
# Channel Message Flows
Use this from the OpenClaw repo root to send canned channel preview flows while iterating on message UX. These are real sends/edits/deletes against the configured channel target.
## Telegram
Native Telegram `sendMessageDraft` tool progress, then a final answer:
```bash
node --import tsx scripts/dev/channel-message-flows.ts \
--channel telegram \
--target <telegram-chat-id> \
--flow working-final \
--duration-ms 20000
```
Thinking preview, then a final answer:
```bash
node --import tsx scripts/dev/channel-message-flows.ts \
--channel telegram \
--target <telegram-chat-id> \
--flow thinking-final
```
## Options
- `--account <accountId>`: Telegram account id when not using the default.
- `--thread-id <id>`: Telegram forum topic/message thread id.
- `--delay-ms <ms>`: Override preview update cadence.
- `--duration-ms <ms>`: Simulated working duration for `working-final`.
- `--final-text <text>`: Override the durable final message.
## Notes
- `--target` is the numeric Telegram chat id.
- `working-final` exercises native Telegram `sendMessageDraft` with static `Working` status and sample tool progress.
- `thinking-final` exercises formatted `Thinking` reasoning preview clearing before the final answer.
- Only `--channel telegram` is implemented for now.

View File

@@ -45,6 +45,10 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
shim can be stale.
- Check `.crabbox.yaml` for direct-provider defaults. Omitting `--provider`
means brokered AWS today.
- The brokered AWS default is a Linux developer image in `eu-west-1`; the repo
config pins hot `eu-west-1a/b/c` placement so Fast Snapshot Restore can apply.
If warmup drifts well past the minute-scale path, verify image promotion,
region/AZ placement, and FSR state before blaming OpenClaw.
- For broad OpenClaw maintainer `pnpm` gates, prefer the repo wrapper with
`--provider blacksmith-testbox` or the repo Testbox helpers when the standing
Testbox policy applies.
@@ -78,6 +82,25 @@ Use these only when the task needs an existing non-Linux host. OpenClaw broad
Linux validation uses the repo Crabbox config unless a provider is explicitly
requested.
Native brokered Windows is available for Windows-specific proof. Use the AWS
developer image in `us-west-2` on demand; it has the expected OpenClaw developer
toolchain and Docker image cache. Keep broad Linux gates on Linux/Testbox unless
the bug is Windows-specific:
```sh
../crabbox/bin/crabbox warmup \
--provider aws \
--target windows \
--windows-mode normal \
--region us-west-2 \
--market on-demand \
--timing-json
```
The hydrate workflow assumes Docker should already be baked into Linux images
and only installs it as a fallback. Do not add per-run Docker installs to proof
commands unless the image probe shows Docker is actually missing.
When the user explicitly asks for brokered macOS runners, use Crabbox AWS
macOS only after confirming the deployed coordinator supports EC2 Mac host
lifecycle/image routes and the operator has AWS EC2 Mac Dedicated Host quota

View File

@@ -28,7 +28,10 @@ git status --short --branch
git rev-parse HEAD
```
If env lacks keys, use `$one-password` to inject or set them, then rerun the script. The script prints only provider status and HTTP class, never tokens.
1Password service-account values are the first source for release provider
preflight. Inject those exact targeted keys first, then run the verifier; use
ambient env only when it was already intentionally injected for this release.
The script prints only provider status and HTTP class, never tokens.
## Dispatch

View File

@@ -170,6 +170,13 @@ live`; keep it clearly beta and avoid implying stable promotion.
CI, validation, or internal release mechanics unless the release is explicitly
about those. Peter prefers concrete user wins: features, integrations,
workflow improvements, and practical reliability fixes.
- Do not feature QA parity, test coverage, release gates, or validation lanes in
user-facing launch tweets. Keep them for release notes or maintainer proof
unless the operator explicitly asks for validation-focused copy.
- Do not feature plugin-author or developer tooling such as SDK helpers,
tool-plugin scaffolding, build/validate/init commands, or internal CLI
plumbing in general user-facing launch tweets unless the operator explicitly
asks for developer-focused copy.
- Tone: high-signal, slightly cheeky, confident, not corporate. One joke is
enough. Avoid punching down, insulting users, or promising what was not
verified.

View File

@@ -6,6 +6,10 @@ capacity:
strategy: most-available
fallback: on-demand-after-120s
hints: true
availabilityZones:
- eu-west-1a
- eu-west-1b
- eu-west-1c
regions:
- eu-west-1
- eu-west-2

View File

@@ -40,6 +40,7 @@ runs:
id: pnpm-cache
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ inputs.node-version }}
pnpm-version: ${{ inputs.pnpm-version }}
cache-key-suffix: ${{ inputs.cache-key-suffix }}
@@ -58,14 +59,15 @@ runs:
if command -v bun &>/dev/null; then bun -v; fi
- name: Capture node path
if: inputs.install-deps == 'true'
shell: bash
run: |
node_bin="$(dirname "$(node -p 'process.execPath')")"
if command -v cygpath >/dev/null 2>&1; then
node_bin="$(cygpath -u "$node_bin")"
fi
# zizmor: ignore[github-env] node_bin comes from trusted actions/setup-node output in this composite action.
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
echo "$node_bin" >> "$GITHUB_PATH"
- name: Install dependencies
if: inputs.install-deps == 'true'

View File

@@ -5,6 +5,10 @@ inputs:
description: pnpm version to activate via corepack.
required: false
default: "11.0.8"
node-version:
description: Expected Node.js version already installed by actions/setup-node.
required: false
default: "24.x"
cache-key-suffix:
description: Suffix appended to the cache key.
required: false
@@ -41,12 +45,85 @@ runs:
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
PNPM_VERSION: ${{ inputs.pnpm-version }}
REQUESTED_NODE_VERSION: ${{ inputs.node-version }}
run: |
set -euo pipefail
if [[ ! "$PNPM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid pnpm-version input: '$PNPM_VERSION'"
exit 2
fi
requested_node="${REQUESTED_NODE_VERSION:-${NODE_VERSION:-}}"
requested_node="${requested_node#v}"
node_version_matches() {
local actual="$1"
local requested="$2"
if [[ -z "$requested" ]]; then
return 0
fi
case "$requested" in
*x)
[[ "${actual%%.*}" == "${requested%%.*}" ]]
;;
*.*.*)
[[ "$actual" == "$requested" ]]
;;
*.*)
[[ "$actual" == "$requested".* ]]
;;
*)
[[ "${actual%%.*}" == "$requested" ]]
;;
esac
}
active_node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
if ! node_version_matches "$active_node_version" "$requested_node"; then
node_roots=()
for root in \
"${RUNNER_TOOL_CACHE:-}" \
"${AGENT_TOOLSDIRECTORY:-}" \
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
"/opt/hostedtoolcache" \
"/home/runner/_work/_tool" \
"/Users/runner/hostedtoolcache" \
"/c/hostedtoolcache/windows"
do
if [[ -d "$root/node" ]]; then
node_roots+=("$root/node")
elif [[ "$(basename "$root")" == "node" && -d "$root" ]]; then
node_roots+=("$root")
fi
done
node_bin=""
for node_root in "${node_roots[@]}"; do
while IFS= read -r candidate; do
candidate_version="$("$candidate" -p 'process.versions.node' 2>/dev/null || true)"
if node_version_matches "$candidate_version" "$requested_node"; then
node_bin="$candidate"
break 2
fi
done < <(find "$node_root" \( -name node -o -name node.exe \) -type f 2>/dev/null | sort -r)
done
if [[ -n "$node_bin" ]]; then
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
export PATH="$(dirname "$node_bin"):$PATH"
hash -r
fi
fi
active_node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
if ! node_version_matches "$active_node_version" "$requested_node"; then
echo "::error::Expected Node '${requested_node}', but active node is '${active_node_version:-missing}' at $(command -v node || true)"
exit 1
fi
node -v
command -v node
command -v corepack
corepack enable
for attempt in 1 2 3; do
if corepack prepare "pnpm@$PNPM_VERSION" --activate; then

View File

@@ -119,8 +119,10 @@ than Telegram-visible behavior`. Use this manifest shape and do not create
`$OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT`, the workflow-provided `crabbox`
binary, and the workflow-provided local `ffmpeg`/`ffprobe`; do not generate,
install, or patch replacement proof tooling during the run. Use the same
proof idea for baseline and candidate. You may iterate and rerun if the
visual result is not convincing.
proof idea for baseline and candidate. Let `start` return or fail on its
own; do not kill it while Crabbox is still waiting for bootstrap. Use a long
command timeout for `start`, `send`, `view`, and `finish`. You may iterate
and rerun if the visual result is not convincing.
7. Open Telegram Desktop directly to the newest relevant message with the
runner `view` command before finishing each recording. Keep the chat scrolled
to the bottom so new proof messages appear in-frame.

5
.github/labeler.yml vendored
View File

@@ -286,6 +286,11 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/oc-path/**"
"extensions: policy":
- changed-files:
- any-glob-to-any-file:
- "extensions/policy/**"
- "docs/cli/policy.md"
"extensions: open-prose":
- changed-files:
- any-glob-to-any-file:

View File

@@ -9,6 +9,12 @@ If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta
- What changed:
- What did NOT change (scope boundary):
## Motivation
Explain why this change should exist now. Link it to the user pain, failure mode, maintainer need, or product goal. If this is purely mechanical, write `N/A`.
-
## Change Type (select all)
- [ ] Bug fix
@@ -35,12 +41,6 @@ If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta
- Related #
- [ ] This PR fixes a bug or regression
## Motivation
Explain why this change should exist now. Link it to the user pain, failure mode, maintainer need, or product goal. If this is purely mechanical, write `N/A`.
-
## 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. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.

View File

@@ -62,17 +62,20 @@ jobs:
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Ensure Docker is available
- name: Ensure Docker is running
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl -fsSL https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl start docker
sudo systemctl start docker || true
elif command -v service >/dev/null 2>&1; then
sudo service docker start || true
fi
if [ -S /var/run/docker.sock ]; then
@@ -82,6 +85,10 @@ jobs:
sudo chmod 666 /var/run/docker.sock
fi
docker version
docker buildx version || true
docker compose version || true
- name: Hydrate provider env helper
shell: bash
env:

View File

@@ -155,7 +155,7 @@ jobs:
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
OPENCLAW_EXTENSIONS=diagnostics-otel,codex
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
sbom: true
@@ -253,7 +253,7 @@ jobs:
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
OPENCLAW_EXTENSIONS=diagnostics-otel,codex
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
sbom: true

View File

@@ -88,6 +88,11 @@ on:
required: false
default: ""
type: string
codex_plugin_spec:
description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref
required: false
default: ""
type: string
npm_telegram_provider_mode:
description: Provider mode for the package Telegram E2E lane
required: false
@@ -108,7 +113,7 @@ permissions:
concurrency:
group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }}
cancel-in-progress: ${{ inputs.ref == 'main' && inputs.rerun_group == 'all' }}
cancel-in-progress: ${{ (inputs.ref == 'main' && inputs.rerun_group == 'all') || startsWith(inputs.ref, 'tideclaw/alpha/') }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -151,6 +156,7 @@ jobs:
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
RERUN_GROUP: ${{ inputs.rerun_group }}
@@ -208,14 +214,43 @@ jobs:
else
echo "- Package Acceptance package spec: SHA-built release artifact"
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
docker_runtime_assets_preflight:
name: Verify Docker runtime-assets prune path
needs: [resolve_target]
if: inputs.rerun_group == 'all'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
steps:
- name: Checkout target SHA
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify Docker runtime-assets prune path
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --foreground --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="matrix" \
.
normal_ci:
name: Run normal full CI
needs: [resolve_target]
if: contains(fromJSON('["all","ci"]'), inputs.rerun_group)
needs: [resolve_target, docker_runtime_assets_preflight]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 240 || 60 }}
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
@@ -312,10 +347,10 @@ jobs:
plugin_prerelease:
name: Run plugin prerelease validation
needs: [resolve_target]
if: contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group)
needs: [resolve_target, docker_runtime_assets_preflight]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || 60 }}
timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
@@ -412,10 +447,10 @@ jobs:
release_checks:
name: Run release/live/Docker/QA validation
needs: [resolve_target]
if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group)
needs: [resolve_target, docker_runtime_assets_preflight]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 240 || 60 }}
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
@@ -437,6 +472,7 @@ jobs:
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
run: |
set -euo pipefail
@@ -532,6 +568,9 @@ jobs:
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
child_rerun_group="$RERUN_GROUP"
@@ -560,13 +599,16 @@ jobs:
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")
fi
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
prepare_release_package:
name: Prepare release package artifact
needs: [resolve_target]
if: ${{ inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' }}
needs: [resolve_target, docker_runtime_assets_preflight]
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' && needs.docker_runtime_assets_preflight.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
@@ -735,7 +777,7 @@ jobs:
summary:
name: Verify full validation
needs: [resolve_target, normal_ci, plugin_prerelease, release_checks, npm_telegram]
needs: [resolve_target, docker_runtime_assets_preflight, normal_ci, plugin_prerelease, release_checks, npm_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
@@ -751,6 +793,8 @@ jobs:
PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }}
RERUN_GROUP: ${{ inputs.rerun_group }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
@@ -798,6 +842,7 @@ jobs:
echo
echo "| Child | Result | Minutes | Head SHA | Run |"
echo "| --- | --- | ---: | --- | --- |"
echo "| \`docker_runtime_assets_preflight\` | \`${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}\` | | current workflow | |"
} >> "$GITHUB_STEP_SUMMARY"
append_child_row() {
@@ -933,23 +978,46 @@ jobs:
}
failed=0
normal_ci_required=0
plugin_prerelease_required=0
release_checks_required=0
if [[ "$RERUN_GROUP" == "all" && "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" != "success" ]]; then
echo "::error::Docker runtime-assets preflight ended with ${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}."
failed=1
elif [[ "$RERUN_GROUP" == "all" ]]; then
normal_ci_required=1
plugin_prerelease_required=1
release_checks_required=1
else
case "$RERUN_GROUP" in
ci)
normal_ci_required=1
;;
plugin-prerelease)
plugin_prerelease_required=1
;;
release-checks|install-smoke|cross-os|live-e2e|package|qa|qa-parity|qa-live)
release_checks_required=1
;;
esac
fi
append_child_overview
if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then
check_child "normal_ci" "" 0 || failed=1
check_child "normal_ci" "" "$normal_ci_required" || failed=1
else
check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1
fi
if [[ "$PLUGIN_PRERELEASE_RESULT" == "skipped" && -z "${PLUGIN_PRERELEASE_RUN_ID// }" ]]; then
check_child "plugin_prerelease" "" 0 || failed=1
check_child "plugin_prerelease" "" "$plugin_prerelease_required" || failed=1
else
check_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" 1 || failed=1
fi
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then
check_child "release_checks" "" 0 || failed=1
check_child "release_checks" "" "$release_checks_required" || failed=1
else
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
fi
@@ -994,9 +1062,17 @@ jobs:
exit 0
fi
evidence_package_spec="$PACKAGE_SPEC"
if [[ -z "${evidence_package_spec// }" ]]; then
tag_ref="${TARGET_REF#refs/tags/}"
if [[ "$tag_ref" =~ ^v([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?)$ ]]; then
evidence_package_spec="openclaw@${BASH_REMATCH[1]}"
fi
fi
release_id="${TARGET_REF#refs/tags/}"
release_id="${release_id#v}"
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
if [[ "$evidence_package_spec" =~ ^openclaw@(.+)$ ]]; then
release_id="${BASH_REMATCH[1]}"
fi
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
@@ -1010,7 +1086,7 @@ jobs:
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
--arg release_id "$release_id" \
--arg release_ref "$TARGET_REF" \
--arg package_spec "$PACKAGE_SPEC" \
--arg package_spec "$evidence_package_spec" \
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
'{
event_type: "openclaw_full_release_validation_completed",
@@ -1032,6 +1108,15 @@ jobs:
https://api.github.com/repos/openclaw/releases-private/dispatches \
-d "$payload"; then
echo "::warning::Automatic private release evidence dispatch failed; child workflow validation remains authoritative."
{
echo "### Private release evidence dispatch failed"
echo
echo "Child workflow validation remains authoritative. Backfill durable evidence from \`openclaw/releases-private\`:"
echo
echo "\`\`\`bash"
echo "gh workflow run openclaw-release-evidence-from-full-validation.yml --repo openclaw/releases-private --ref main -f full_validation_run_id=${GITHUB_RUN_ID_VALUE} -f release_id=${release_id} -f release_ref=${TARGET_REF} -f package_spec=${evidence_package_spec}"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Write release validation manifest

View File

@@ -760,6 +760,7 @@ jobs:
core.info(`Processed ${processed} pull requests.`);
label-issues:
if: github.event_name == 'issues'
permissions:
issues: write
runs-on: ubuntu-24.04

View File

@@ -308,18 +308,36 @@ jobs:
run: |
set -euo pipefail
current_created="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" --jq .created_at)"
stale_before="$(date -u -d '8 hours ago' +%Y-%m-%dT%H:%M:%SZ)"
run_has_active_jobs() {
local run_id="$1"
local run_state="$2"
if [[ "$run_state" != "in_progress" ]]; then
return 0
fi
local active_jobs
active_jobs="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json jobs --jq '[.jobs[] | select(.status == "queued" or .status == "in_progress" or .status == "waiting" or .status == "pending" or .status == "requested")] | length')"
[[ "$active_jobs" != "0" ]]
}
while true; do
blockers="$(
candidates="$(
for workflow in mantis-telegram-desktop-proof.yml mantis-telegram-live.yml; do
for status in queued in_progress waiting pending requested; do
gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --status "$status" --limit 100 --json databaseId,status,createdAt,url \
| jq -r \
--argjson current_id "$GITHUB_RUN_ID" \
--arg current_created "$current_created" \
'.[] | select(.databaseId != $current_id) | select(.createdAt < $current_created or (.createdAt == $current_created and .databaseId < $current_id)) | "\(.createdAt)\t#\(.databaseId)\t\(.status)\t\(.url)"'
--arg stale_before "$stale_before" \
'.[] | select(.databaseId != $current_id) | select(.createdAt >= $stale_before) | select(.createdAt < $current_created or (.createdAt == $current_created and .databaseId < $current_id)) | "\(.createdAt)\t#\(.databaseId)\t\(.status)\t\(.url)"'
done
done | sort -u
)"
blockers=""
while IFS=$'\t' read -r created run_id run_state url; do
if [[ -n "$run_id" ]] && run_has_active_jobs "${run_id#\#}" "$run_state"; then
blockers+="${created}"$'\t'"${run_id}"$'\t'"${run_state}"$'\t'"${url}"$'\n'
fi
done <<<"$candidates"
if [[ -z "$blockers" ]]; then
break
fi
@@ -408,7 +426,7 @@ jobs:
printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"'
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER"'
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_CANDIDATE_TRUST MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_CREDENTIAL_OWNER_ID OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"'
} | sudo tee /etc/sudoers.d/mantis-codex-env >/dev/null
sudo chmod 0440 /etc/sudoers.d/mantis-codex-env
@@ -444,6 +462,7 @@ jobs:
MANTIS_PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CREDENTIAL_OWNER_ID: mantis-telegram-desktop-${{ github.run_id }}-${{ github.run_attempt }}
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
OPENCLAW_TELEGRAM_USER_CRABBOX_BIN: /usr/local/bin/crabbox
@@ -462,6 +481,47 @@ jobs:
codex-user: codex
allow-bot-users: clawsweeper[bot]
- name: Release leaked Telegram proof leases
if: ${{ always() }}
env:
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
shell: bash
run: |
set -euo pipefail
if [[ ! -d .artifacts/qa-e2e ]]; then
exit 0
fi
status=0
mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/session.json' -type f -print0)
for session_file in "${session_files[@]}"; do
lease_file="${session_file%/session.json}/.session/lease.json"
if [[ ! -f "$lease_file" ]]; then
continue
fi
if ! sudo -u codex env \
OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \
OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \
OPENCLAW_TELEGRAM_USER_CRABBOX_BIN=/usr/local/bin/crabbox \
OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER="$CRABBOX_PROVIDER" \
node --import tsx "$GITHUB_WORKSPACE/scripts/e2e/telegram-user-crabbox-proof.ts" \
finish --session "$session_file" --preview-crop telegram-window; then
status=1
fi
done
mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/.session/lease.json' -type f -print0)
for lease_file in "${lease_files[@]}"; do
if ! sudo -u codex env \
OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \
OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \
node --import tsx "$GITHUB_WORKSPACE/scripts/e2e/telegram-user-credential.ts" \
release --lease-file "$lease_file"; then
status=1
fi
done
exit "$status"
- name: Inspect Mantis evidence manifest
id: inspect
if: ${{ always() }}

View File

@@ -272,18 +272,36 @@ jobs:
run: |
set -euo pipefail
current_created="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" --jq .created_at)"
stale_before="$(date -u -d '8 hours ago' +%Y-%m-%dT%H:%M:%SZ)"
run_has_active_jobs() {
local run_id="$1"
local run_state="$2"
if [[ "$run_state" != "in_progress" ]]; then
return 0
fi
local active_jobs
active_jobs="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json jobs --jq '[.jobs[] | select(.status == "queued" or .status == "in_progress" or .status == "waiting" or .status == "pending" or .status == "requested")] | length')"
[[ "$active_jobs" != "0" ]]
}
while true; do
blockers="$(
candidates="$(
for workflow in mantis-telegram-desktop-proof.yml mantis-telegram-live.yml; do
for status in queued in_progress waiting pending requested; do
gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --status "$status" --limit 100 --json databaseId,status,createdAt,url \
| jq -r \
--argjson current_id "$GITHUB_RUN_ID" \
--arg current_created "$current_created" \
'.[] | select(.databaseId != $current_id) | select(.createdAt < $current_created or (.createdAt == $current_created and .databaseId < $current_id)) | "\(.createdAt)\t#\(.databaseId)\t\(.status)\t\(.url)"'
--arg stale_before "$stale_before" \
'.[] | select(.databaseId != $current_id) | select(.createdAt >= $stale_before) | select(.createdAt < $current_created or (.createdAt == $current_created and .databaseId < $current_id)) | "\(.createdAt)\t#\(.databaseId)\t\(.status)\t\(.url)"'
done
done | sort -u
)"
blockers=""
while IFS=$'\t' read -r created run_id run_state url; do
if [[ -n "$run_id" ]] && run_has_active_jobs "${run_id#\#}" "$run_state"; then
blockers+="${created}"$'\t'"${run_id}"$'\t'"${run_state}"$'\t'"${url}"$'\n'
fi
done <<<"$candidates"
if [[ -z "$blockers" ]]; then
break
fi

View File

@@ -68,6 +68,11 @@ on:
required: false
default: ""
type: string
codex_plugin_spec:
description: Optional Codex plugin install spec for the live package lane; blank packs extensions/codex from the selected ref
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -173,6 +178,11 @@ on:
required: false
default: ""
type: string
codex_plugin_spec:
description: Optional Codex plugin install spec for the live package lane; blank packs extensions/codex from the selected ref
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -631,7 +641,7 @@ jobs:
profiles: stable full
- chunk_id: package-update-openai
label: package/update OpenAI install
timeout_minutes: 20
timeout_minutes: 45
profiles: beta minimum stable full
- chunk_id: package-update-anthropic
label: package/update Anthropic install
@@ -734,6 +744,7 @@ jobs:
OPENCLAW_DOCKER_E2E_REPO_ROOT: ${{ github.workspace }}
OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
OPENCLAW_DOCKER_ALL_RELEASE_PROFILE: ${{ inputs.release_test_profile }}
OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}
@@ -974,6 +985,7 @@ jobs:
OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
OPENCLAW_DOCKER_E2E_REPO_ROOT: ${{ github.workspace }}
OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }}

View File

@@ -20,6 +20,10 @@ on:
description: Successful Full Release Validation run id for this tag/SHA, required for real publish
required: false
type: string
release_publish_run_id:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
npm_dist_tag:
description: npm dist-tag to publish to
required: true
@@ -32,7 +36,7 @@ on:
concurrency:
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', inputs.tag, inputs.npm_dist_tag) || github.ref }}
cancel-in-progress: false
cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only && inputs.npm_dist_tag == 'alpha' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -367,6 +371,7 @@ jobs:
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Require trusted workflow ref for publish
@@ -389,6 +394,7 @@ jobs:
env:
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
run: |
set -euo pipefail
if [[ -z "${PREFLIGHT_RUN_ID}" ]]; then
@@ -399,6 +405,32 @@ jobs:
echo "Real publish requires full_release_validation_run_id from a successful Full Release Validation run." >&2
exit 1
fi
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" && "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "Workflow-dispatched real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2
exit 1
fi
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "OpenClaw npm publish dispatched by another workflow must include release_publish_run_id." >&2
exit 1
fi
echo "Direct OpenClaw npm publish; relying on this workflow's npm-release environment approval."
exit 0
fi
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
echo "OpenClaw npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
publish_openclaw_npm:
# KEEP THE REAL RELEASE/PUBLISH PATH ON A GITHUB-HOSTED RUNNER.
@@ -511,10 +543,25 @@ jobs:
preferred_name="openclaw-npm-preflight-${RELEASE_TAG}"
rm -rf preflight-tarball
mkdir -p preflight-tarball
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${preferred_name}" \
--dir preflight-tarball; then
download_named_artifact() {
local artifact_name="$1"
for attempt in 1 2 3; do
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${artifact_name}" \
--dir preflight-tarball; then
return 0
fi
if [[ "$attempt" != "3" ]]; then
echo "::warning::Artifact download for ${artifact_name} failed on attempt ${attempt}; retrying."
sleep $((attempt * 10))
fi
done
return 1
}
if download_named_artifact "${preferred_name}"; then
echo "Downloaded ${preferred_name}."
return 0
fi
@@ -530,10 +577,7 @@ jobs:
exit 1
fi
fallback_name="${matches[0]}"
gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${fallback_name}" \
--dir preflight-tarball
download_named_artifact "${fallback_name}"
echo "Downloaded fallback preflight artifact ${fallback_name}."
}

View File

@@ -78,10 +78,15 @@ on:
required: false
default: ""
type: string
codex_plugin_spec:
description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref
required: false
default: ""
type: string
concurrency:
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
cancel-in-progress: false
cancel-in-progress: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -112,6 +117,7 @@ jobs:
qa_live_slack_enabled: ${{ steps.inputs.outputs.qa_live_slack_enabled }}
release_package_spec: ${{ steps.inputs.outputs.release_package_spec }}
package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }}
codex_plugin_spec: ${{ steps.inputs.outputs.codex_plugin_spec }}
steps:
- name: Require trusted workflow ref for release checks
env:
@@ -262,6 +268,7 @@ jobs:
RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_RELEASE_QA_SLACK_LIVE_CI_ENABLED || 'false' }}
RELEASE_PACKAGE_SPEC_INPUT: ${{ inputs.release_package_spec }}
RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }}
RELEASE_CODEX_PLUGIN_SPEC_INPUT: ${{ inputs.codex_plugin_spec }}
run: |
set -euo pipefail
qa_live_matrix_enabled=true
@@ -307,6 +314,10 @@ jobs:
if [[ "$release_profile" == "full" ]]; then
run_release_soak=true
fi
codex_plugin_spec="$RELEASE_CODEX_PLUGIN_SPEC_INPUT"
if [[ -z "${codex_plugin_spec// }" && "$RELEASE_PACKAGE_SPEC_INPUT" =~ ^openclaw@(.+)$ ]]; then
codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}"
fi
filter="$(printf '%s' "$RELEASE_LIVE_SUITE_FILTER_INPUT" | tr '[:upper:]' '[:lower:]')"
if [[ -n "${filter// }" ]]; then
@@ -387,6 +398,7 @@ jobs:
printf 'qa_live_slack_enabled=%s\n' "$qa_live_slack_enabled"
printf 'release_package_spec=%s\n' "$RELEASE_PACKAGE_SPEC_INPUT"
printf 'package_acceptance_package_spec=%s\n' "$RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT"
printf 'codex_plugin_spec=%s\n' "$codex_plugin_spec"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
@@ -403,6 +415,7 @@ jobs:
RELEASE_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ steps.inputs.outputs.codex_plugin_spec }}
run: |
{
echo "## Release checks"
@@ -432,6 +445,11 @@ jobs:
else
echo "- Package Acceptance package spec: prepared release artifact"
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`"
else
echo "- Codex plugin spec: packed from selected ref"
fi
if [[ "$RUN_RELEASE_SOAK" == "true" ]]; then
echo "- This run will execute blocking release validation plus exhaustive live/Docker soak coverage."
else
@@ -640,6 +658,7 @@ jobs:
include_live_suites: false
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }}
secrets: *live_e2e_release_secrets
package_acceptance_release_checks:
@@ -926,6 +945,20 @@ jobs:
--runtime-pair pi,codex \
--output-dir ".artifacts/qa-e2e/runtime-parity-standard"
- name: Run soak runtime parity tier
id: runtime_parity_soak_lane
if: ${{ always() && needs.resolve_target.outputs.run_release_soak == 'true' && steps.runtime_parity_lane.outcome != 'skipped' && steps.runtime_parity_lane.outcome != 'cancelled' }}
run: |
set -euo pipefail
pnpm openclaw qa suite \
--provider-mode mock-openai \
--runtime-parity-tier soak \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "openai/gpt-5.5-alt" \
--runtime-pair pi,codex \
--output-dir ".artifacts/qa-e2e/runtime-parity-soak"
- name: Generate runtime parity report
if: always()
run: |
@@ -946,6 +979,21 @@ jobs:
--summary .artifacts/qa-e2e/runtime-parity-standard/qa-suite-summary.json \
--output-dir .artifacts/qa-e2e/runtime-parity-standard-report
- name: Generate soak runtime parity report
if: ${{ always() && needs.resolve_target.outputs.run_release_soak == 'true' && steps.runtime_parity_soak_lane.outcome != 'skipped' && steps.runtime_parity_soak_lane.outcome != 'cancelled' }}
run: |
set -euo pipefail
summary=".artifacts/qa-e2e/runtime-parity-soak/qa-suite-summary.json"
if [[ ! -f "$summary" ]]; then
echo "No soak runtime parity summary was produced."
exit 0
fi
pnpm openclaw qa parity-report \
--repo-root . \
--runtime-axis \
--summary "$summary" \
--output-dir .artifacts/qa-e2e/runtime-parity-soak-report
- name: Upload runtime parity artifacts
if: always()
uses: actions/upload-artifact@v7

View File

@@ -153,10 +153,25 @@ jobs:
preflight_dir="${RUNNER_TEMP}/openclaw-npm-preflight-manifest"
rm -rf "${preflight_dir}"
mkdir -p "${preflight_dir}"
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${preferred_name}" \
--dir "${preflight_dir}"; then
download_named_artifact() {
local artifact_name="$1"
for attempt in 1 2 3; do
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${artifact_name}" \
--dir "${preflight_dir}"; then
return 0
fi
if [[ "$attempt" != "3" ]]; then
echo "::warning::Artifact download for ${artifact_name} failed on attempt ${attempt}; retrying."
sleep $((attempt * 10))
fi
done
return 1
}
if download_named_artifact "${preferred_name}"; then
echo "name=${preferred_name}" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -172,10 +187,7 @@ jobs:
exit 1
fi
fallback_name="${matches[0]}"
gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${fallback_name}" \
--dir "${preflight_dir}"
download_named_artifact "${fallback_name}"
echo "name=${fallback_name}" >> "$GITHUB_OUTPUT"
- name: Download full release validation manifest
@@ -336,6 +348,7 @@ jobs:
needs: [resolve_release_target]
runs-on: ubuntu-latest
timeout-minutes: 60
environment: npm-release
steps:
- name: Checkout release SHA
uses: actions/checkout@v6
@@ -438,7 +451,7 @@ jobs:
pending_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" 2>/dev/null || true)"
if [[ -z "${pending_json}" ]] || ! printf '%s' "${pending_json}" | jq -e 'length > 0' >/dev/null 2>&1; then
return 0
return 1
fi
approved=0
@@ -450,13 +463,15 @@ jobs:
gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" \
-F "environment_ids[]=${env_id}" \
-f state=approved \
-f comment="Approve release gate from OpenClaw Release Publish wrapper" >/dev/null
-f comment="Approve child release gate after parent release approval" >/dev/null
approved=1
done < <(printf '%s' "${pending_json}" | jq -r '.[] | select(.current_user_can_approve == true) | [.environment.id, .environment.name] | @tsv')
if [[ "${approved}" == "1" ]]; then
echo "${workflow}: approved available pending environment gates"
return 0
fi
return 1
}
print_failed_run_summary() {
@@ -499,7 +514,7 @@ jobs:
if [[ "$state" != "$last_state" ]]; then
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
print_pending_deployments "${workflow}" "${run_id}"
approve_pending_deployments "${workflow}" "${run_id}"
approve_pending_deployments "${workflow}" "${run_id}" || true
last_state="$state"
fi
sleep 30
@@ -546,6 +561,85 @@ jobs:
wait_run_pid="$!"
}
wait_for_job_success() {
local workflow="$1"
local run_id="$2"
local job_name="$3"
local jobs_json job_json run_status run_conclusion status conclusion url deadline
deadline=$((SECONDS + 900))
while true; do
jobs_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,conclusion,jobs)"
run_status="$(printf '%s' "$jobs_json" | jq -r '.status')"
run_conclusion="$(printf '%s' "$jobs_json" | jq -r '.conclusion // ""')"
job_json="$(printf '%s' "$jobs_json" | jq -c --arg name "$job_name" '.jobs[]? | select(.name == $name) | {status, conclusion, url}' | head -n 1)"
if [[ -n "$job_json" ]]; then
status="$(printf '%s' "$job_json" | jq -r '.status')"
conclusion="$(printf '%s' "$job_json" | jq -r '.conclusion // ""')"
url="$(printf '%s' "$job_json" | jq -r '.url // ""')"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" == "success" || "$conclusion" == "skipped" ]]; then
echo "${workflow} ${job_name} ${conclusion}: ${url}"
echo "- ${workflow} ${job_name}: ${conclusion} (${url})" >> "$GITHUB_STEP_SUMMARY"
return 0
fi
echo "${workflow} ${job_name} failed: ${conclusion} ${url}" >&2
print_failed_run_summary "${run_id}"
return 1
fi
echo "${workflow} ${job_name} still ${status}: ${url}"
elif [[ "$run_status" == "completed" ]]; then
if [[ "$run_conclusion" == "success" ]]; then
echo "${workflow} completed before ${job_name} was needed."
echo "- ${workflow} ${job_name}: not needed" >> "$GITHUB_STEP_SUMMARY"
return 0
fi
echo "${workflow} completed before ${job_name} with ${run_conclusion}." >&2
print_failed_run_summary "${run_id}"
return 1
else
echo "${workflow} waiting for ${job_name} to start: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fi
if (( SECONDS >= deadline )); then
echo "${workflow} ${job_name} did not complete within 15 minutes." >&2
return 1
fi
sleep 10
done
}
approve_child_publish_environment() {
local workflow="$1"
local run_id="$2"
local run_json status conclusion deadline
deadline=$((SECONDS + 900))
while true; do
if approve_pending_deployments "${workflow}" "${run_id}"; then
echo "- ${workflow}: child environment gate approved" >> "$GITHUB_STEP_SUMMARY"
return 0
fi
run_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,conclusion,url)"
status="$(printf '%s' "$run_json" | jq -r '.status')"
conclusion="$(printf '%s' "$run_json" | jq -r '.conclusion // ""')"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" == "success" ]]; then
echo "${workflow}: completed before child environment approval was needed"
return 0
fi
echo "${workflow}: completed before child environment approval with ${conclusion}" >&2
print_failed_run_summary "${run_id}"
return 1
fi
if (( SECONDS >= deadline )); then
echo "${workflow}: child environment approval was not available within 15 minutes." >&2
print_pending_deployments "${workflow}" "${run_id}"
return 1
fi
sleep 10
done
}
create_or_update_github_release() {
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
release_version="${RELEASE_TAG#v}"
@@ -645,10 +739,14 @@ jobs:
--workflow-ref "${CHILD_WORKFLOW_REF}"
--full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}"
--plugin-npm-run "${plugin_npm_run_id}"
--plugin-clawhub-run "${plugin_clawhub_run_id}"
--openclaw-npm-run "${openclaw_npm_run_id}"
--evidence-out "${evidence_path}"
)
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
verify_args+=(--plugin-clawhub-run "${plugin_clawhub_run_id}")
else
verify_args+=(--skip-clawhub)
fi
if [[ -n "${PLUGINS// }" ]]; then
verify_args+=(--plugins "${PLUGINS}")
fi
@@ -663,27 +761,97 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
}
append_release_proof_to_github_release() {
local release_version body_file notes_file tarball integrity telegram_line clawhub_line
release_version="${RELEASE_TAG#v}"
body_file="${RUNNER_TEMP}/release-body.md"
notes_file="${RUNNER_TEMP}/release-notes-with-proof.md"
tarball="$(npm view "openclaw@${release_version}" dist.tarball --json | jq -r '.')"
integrity="$(npm view "openclaw@${release_version}" dist.integrity --json | jq -r '.')"
gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json body --jq .body > "${body_file}"
if [[ -n "${NPM_TELEGRAM_RUN_ID// }" ]]; then
telegram_line="- npm Telegram beta E2E: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${NPM_TELEGRAM_RUN_ID}"
else
telegram_line="- npm Telegram beta E2E: not supplied"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
clawhub_line="- plugin ClawHub publish: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
else
clawhub_line="- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
fi
RELEASE_BODY_FILE="${body_file}" \
RELEASE_NOTES_FILE="${notes_file}" \
RELEASE_VERSION="${release_version}" \
RELEASE_TAG="${RELEASE_TAG}" \
RELEASE_REPO="${GITHUB_REPOSITORY}" \
RELEASE_TARBALL="${tarball}" \
RELEASE_INTEGRITY="${integrity}" \
RELEASE_PUBLISH_RUN_ID="${GITHUB_RUN_ID}" \
PREFLIGHT_RUN_ID="${PREFLIGHT_RUN_ID}" \
FULL_RELEASE_VALIDATION_RUN_ID="${FULL_RELEASE_VALIDATION_RUN_ID}" \
PLUGIN_NPM_RUN_ID="${plugin_npm_run_id}" \
OPENCLAW_NPM_RUN_ID="${openclaw_npm_run_id}" \
CLAWHUB_LINE="${clawhub_line}" \
TELEGRAM_LINE="${telegram_line}" \
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from "node:fs";
const bodyFile = process.env.RELEASE_BODY_FILE;
const notesFile = process.env.RELEASE_NOTES_FILE;
if (!bodyFile || !notesFile) {
throw new Error("Missing release notes file paths.");
}
const body = readFileSync(bodyFile, "utf8").trimEnd();
const section = [
"### Release verification",
"",
`- npm package: https://www.npmjs.com/package/openclaw/v/${process.env.RELEASE_VERSION}`,
`- registry tarball: ${process.env.RELEASE_TARBALL}`,
`- integrity: \`${process.env.RELEASE_INTEGRITY}\``,
`- full release CI report: https://github.com/openclaw/releases-private/blob/main/evidence/${process.env.RELEASE_VERSION}/release-evidence.md`,
`- release publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.RELEASE_PUBLISH_RUN_ID}`,
`- npm preflight: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PREFLIGHT_RUN_ID}`,
`- full release validation: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.FULL_RELEASE_VALIDATION_RUN_ID}`,
`- plugin npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PLUGIN_NPM_RUN_ID}`,
process.env.CLAWHUB_LINE,
`- OpenClaw npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.OPENCLAW_NPM_RUN_ID}`,
process.env.TELEGRAM_LINE,
].join("\n");
const withoutOldProof = body.replace(/\n?### Release verification\n[\s\S]*?(?=\n### |\n## |$)/, "");
writeFileSync(notesFile, `${withoutOldProof.trimEnd()}\n\n${section}\n`);
NODE
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --notes-file "${notes_file}"
echo "- Release proof: appended to GitHub release" >> "$GITHUB_STEP_SUMMARY"
}
{
echo "### Publish sequence"
echo
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Release approval: this workflow job"
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- OpenClaw npm publish: starts after plugin npm succeeds; final verification waits for ClawHub"
echo "- OpenClaw npm publish: starts after plugin npm succeeds"
else
echo "- OpenClaw npm publish: skipped by input"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
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}")
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
if [[ -n "${PLUGINS}" ]]; then
npm_args+=(-f plugins="${PLUGINS}")
clawhub_args+=(-f plugins="${PLUGINS}")
@@ -709,6 +877,7 @@ jobs:
-f preflight_only=false \
-f preflight_run_id="${PREFLIGHT_RUN_ID}" \
-f full_release_validation_run_id="${FULL_RELEASE_VALIDATION_RUN_ID}" \
-f release_publish_run_id="${GITHUB_RUN_ID}" \
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
echo "- OpenClaw npm run ID: \`${openclaw_npm_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
else
@@ -717,13 +886,19 @@ jobs:
clawhub_result=""
clawhub_pid=""
if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
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"
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
:
else
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
openclaw_result=""
@@ -760,6 +935,7 @@ jobs:
if [[ "${failed}" == "0" && -n "${openclaw_npm_run_id}" ]]; then
verify_published_release
append_release_proof_to_github_release
fi
if [[ "${failed}" != "0" ]]; then
exit 1

View File

@@ -20,6 +20,10 @@ on:
required: false
default: ""
type: string
release_publish_run_id:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
concurrency:
group: plugin-clawhub-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@@ -196,6 +200,37 @@ jobs:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
validate_release_publish_approval:
name: Validate release publish approval
needs: preview_plugins_clawhub
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "Plugin ClawHub publish dispatched by another workflow must include release_publish_run_id." >&2
exit 1
fi
echo "Direct Plugin ClawHub Release dispatch; relying on this workflow's clawhub-plugin-release environment approval."
exit 0
fi
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
echo "Plugin ClawHub publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
preview_plugin_pack:
needs: preview_plugins_clawhub
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
@@ -289,11 +324,12 @@ jobs:
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, preview_plugin_pack]
needs: [preview_plugins_clawhub, preview_plugin_pack, validate_release_publish_approval]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
actions: read
contents: read
id-token: write
strategy:

View File

@@ -32,6 +32,10 @@ on:
description: Comma-separated plugin package names to publish when publish_scope=selected
required: false
type: string
release_publish_run_id:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
concurrency:
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@@ -173,6 +177,37 @@ jobs:
exit 1
fi
validate_release_publish_approval:
name: Validate release publish approval
needs: preview_plugins_npm
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "Plugin npm publish dispatched by another workflow must include release_publish_run_id." >&2
exit 1
fi
echo "Direct Plugin NPM Release dispatch; relying on this workflow's npm-release environment approval."
exit 0
fi
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
echo "Plugin npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
preview_plugin_pack:
needs: preview_plugins_npm
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'
@@ -205,11 +240,12 @@ jobs:
run: bash scripts/plugin-npm-publish.sh --pack-dry-run "${{ matrix.plugin.packageDir }}"
publish_plugins_npm:
needs: [preview_plugins_npm, preview_plugin_pack]
needs: [preview_plugins_npm, preview_plugin_pack, validate_release_publish_approval]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: npm-release
permissions:
actions: read
contents: read
id-token: write
strategy:

View File

@@ -76,11 +76,9 @@ jobs:
- name: install.sh in Docker
run: |
docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
node:24-bookworm-slim \
bash -lc 'bash /tmp/install.sh --no-prompt --no-onboard --version latest && openclaw --version'
bash -lc 'bash /tmp/install.sh --version latest && openclaw --version'
- name: install-cli.sh in Docker
run: |

View File

@@ -84,6 +84,7 @@ Skills own workflows; root owns hard policy and routing.
## GitHub / PRs
- Use `$openclaw-pr-maintainer` immediately for maintainer-side OpenClaw issue/PR review, triage, duplicates, labels, comments, close, land, or evidence. Contributor PR creation/refresh follows the requested contributor workflow; linked refs alone do not require maintainer archive tooling.
- Pasted GitHub issue/PR: first `git status -sb`; if dirty, yell; then `git push` + `git pull --ff-only`.
- PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation.
- Bare issue/PR URL/number means review/report in chat. Suggest comment/close/merge when appropriate; mutate only when asked.
- No unsolicited PR comments/reviews/labels/retitles/rebases/fixups/landing. Exception: close/duplicate action that needs a reason comment after explicit close/sweep/landing request.
@@ -171,6 +172,7 @@ Skills own workflows; root owns hard policy and routing.
- "restart iOS/Android apps" = rebuild/reinstall/relaunch, not kill/launch.
- SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
- Mac gateway: dev watch = `pnpm gateway:watch`; managed installs = `openclaw gateway restart/status --deep`; logs = `./scripts/clawlog.sh`. No launchd/ad-hoc tmux.
- Mac app permission testing: stable app path + real signing identity required. No `--no-sign`, `SIGN_IDENTITY=-`, or raw debug binary; TCC prompts/listing won't stick.
- Version bump surfaces live in `$openclaw-release-maintainer`.
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.

View File

@@ -4,15 +4,187 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Discord: allow configuring a bounded `agentComponents.ttlMs` callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001.
- Plugin SDK: add row-level session workflow helpers and deprecate `loadSessionStore` so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva.
- Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.
- Plugins/SDK: add a general `embeddingProviders` capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.
- Dependencies: refresh provider, plugin, UI, and tooling packages, update `protobufjs` to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to `@agentclientprotocol/claude-agent-acp` 0.36.1.
- Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.
- QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.
- QA-Lab: include the optional 100-turn runtime parity soak in release-soak artifacts so long-run Codex/Pi transcript drift stays visible outside the default gate. (#80395) Thanks @100yenadmin.
- QA-Lab: add a live-only long-context progress watchdog scenario for Codex app-server timeout and stalled-run sentinels. (#80323) Thanks @100yenadmin.
- QA-Lab: tag gateway restart recovery and streaming final-integrity scenarios as live-only runtime parity lanes. (#80323) Thanks @100yenadmin.
- QA-Lab: add a personal-agent failure recovery scenario that checks honest partial status, retry boundaries, and local recovery artifacts. (#83872) Thanks @iFiras-Max1.
- QA-Lab: include an opt-in `update.run` package self-upgrade sentinel for destructive latest-package recovery checks.
- QA-Lab: add Codex plugin lifecycle and auth-profile fixture coverage for missing installs, pinned-version drift, first-turn install ordering, and doctor migration safety. (#80323, refs #80174) Thanks @100yenadmin.
- Models/perf: pre-warm the provider auth-state map at gateway startup so `/models` and every model-listing call short-circuits the per-provider plugin / external-CLI discovery on the hot path. Per-call cost drops from ~20 s to ~5 ms (~4,100×); the one-time startup warm resets and re-warms after hot reloads. (#84816) Thanks @sjf.
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
### Fixes
- Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.
- Auto-reply/ACP: wait for same-channel block reply delivery before starting tool work, while still honoring ACP dispatch aborts so stopped turns do not wait on slow channel sends. (#83722) Thanks @IWhatsskill.
- Codex/ACP: mark required child-run completions that only report progress, omit a final deliverable, or fail requester delivery as blocked while preserving real final reports. (#85110) Thanks @IWhatsskill.
- Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.
- Agents/Pi: treat accepted embedded `sessions_spawn` child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong.
- WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch.
- Install/update: reject OpenClaw GitHub source package targets early and point moving-main users at the dev/git install path instead of the broken npm source-install flow.
- Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
- Gateway/chat: surface message-tool-only room-event failures in chat diagnostics and session transcripts so suppressed source replies stay debuggable. Thanks @amknight.
- Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
- Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.
- Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.
- Gateway/status: surface inbound delivery telemetry counters and transport-liveness warnings in `openclaw status --all`. Fixes #49577. (#72724)
- Docker: prune package-excluded plugin source workspaces and dependency closures so runtime images do not keep packages for plugins that were not opted in.
- Providers/Ollama: treat Docker/OrbStack host aliases as local Ollama endpoints so `ollama-local` marker auth works when OpenClaw runs inside a VM/container and Ollama runs on the host. Fixes #84875.
- QA-Lab: keep explicitly searchable/deferred OpenClaw dynamic tool rows report-only by default so tool-coverage gates do not treat mock discovery gaps as hard product failures. (#80319) Thanks @100yenadmin.
- Agents/config: keep non-Google provider model refs from being rewritten by Google Gemini preview-id normalization. (#84762) Thanks @zhangguiping-xydt.
- Installer: require a real controlling terminal before launching onboarding so headless `curl | bash` installs finish cleanly after installing the CLI.
- Agents/Codex: promote a completed final assistant response when a prompt timeout races Codex app-server completion instead of returning an empty timeout envelope. Refs #84516.
- Agents: cap heartbeat model bleed context hints by the stored session window when runtime model metadata is unavailable, so overflow recovery advice does not suggest a larger window than the active session actually has.
- Control UI/Web Push: use `https://openclaw.ai` as the generated default VAPID subject instead of the old localhost mailbox so iOS PWA push setup uses an Apple-acceptable subject when `OPENCLAW_VAPID_SUBJECT` is unset. Fixes #83134. (#83317) Thanks @IWhatsskill.
- Agents/Pi: keep embedded session transcript writes from tripping false takeover detection after packaged npm onboarding agent turns.
- Codex/TUI: surface Codex-native post-turn compaction failures instead of continuing uncompacted, and keep successful native compaction serialized before local idle/next-turn handling. Fixes #84305. (#85160) Thanks @joshavant.
- Memory/search: stop recall tracking from writing dreaming side-effect artifacts when `dreaming.enabled=false`, while preserving normal search results. Fixes #84436. (#84444) Thanks @NianJiuZst.
- Diffs: render viewer toolbar icons from a closed icon-name map instead of HTML strings, removing the toolbar icon XSS sink. (#83955) Thanks @tanshanshan.
- QA: keep `pnpm qa:e2e` self-check runs inside the private QA runtime envelope even when inherited shell env disables bundled plugins.
- fix(config): validate browser sandbox bind sources [AI]. (#84799) Thanks @pgondhi987.
- doctor: constrain legacy plugin cleanup paths [AI]. (#84801) Thanks @pgondhi987.
- Update/doctor: prune stale local bundled plugin install records that point at old compiled bundled output so current bundled plugin schemas win after upgrade. (#84863) Thanks @fuller-stack-dev.
- Providers/Ollama: preserve native Ollama tool-call IDs across assistant replay so Gemini over Ollama Cloud can keep its hidden function-call thought-signature handle.
- Discord: keep session recovery and `/stop` abort ownership on the source dispatch lane while bound ACP turns continue routing to their target session, so stalled pre-run work and late replies are cleared instead of leaking after stop. Fixes #84477. (#85100) Thanks @joshavant.
- Codex app-server: mark missing turn completion after observed execution as replay-unsafe and release the session so follow-up turns can run. Fixes #84076. (#85107) Thanks @joshavant.
- Codex app-server: add a dedicated post-tool raw assistant completion idle timeout config so trusted heavy turns can wait longer after tool handoff without weakening final assistant release.
- Matrix: keep explicitly configured two-person rooms on the room route before stale `m.direct` or strict two-member DM fallback can bypass mention gating. Fixes #85017. (#85137) Thanks @joshavant.
- Agents/subagents: require explicit subagent allowlist targets to be configured agents so stale deleted-agent ids are omitted from `agents_list` and rejected by `sessions_spawn`. Fixes #84811. (#85154) Thanks @joshavant.
- PDF tool: time out idle remote PDF body reads after 120 seconds so stalled remote documents return an error instead of wedging the session. Fixes #68649. (#84768) Thanks @luoyanglang.
- Diagnostics/OpenTelemetry plugin: suppress handled OTLP exporter promise rejections so collector shutdowns no longer crash the Gateway. (#81085) Thanks @luoyanglang.
- Agents/exec: omit raw command text and env values from denied exec failure logs while keeping safe correlation metadata. Fixes #85049. (#85140) Thanks @joshavant.
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
- Agents/exec: preserve inherited XDG base-directory environment values for subprocesses while still rejecting agent-supplied XDG overrides. Fixes #84854. (#85139) Thanks @joshavant.
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
- Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale `dreaming-narrative-*` sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.
- Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.
- TUI: coalesce repeated idle Esc abort notices into a single `no active run xN` system row instead of appending duplicate rows.
- Telegram: honor `channels.telegram.pollingStallThresholdMs` in the default isolated polling path, restarting silent workers instead of leaving inbound updates wedged. Fixes #83950. (#84861) Thanks @joshavant.
- Slack: suppress reasoning payloads before reply delivery and dispatch accounting, so Slack monitor, slash-command, fallback, and direct reply paths do not leak model reasoning. Fixes #84319. (#84322) Thanks @ffluk3 and @joshavant.
- Slack: deliver native plugin approval prompts and updates when Slack native approvals are enabled, while keeping plugin approval authorization separate from exec approvers.
- Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.
- CLI/perf: keep `setup --help`, `onboard --help`, and `configure --help` out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn.
- CLI/perf: keep `agents --help` out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn.
- CLI/perf: keep `secrets --help` and `nodes --help` on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.
- CLI/perf: serve `doctor`, `gateway`, `models`, and `plugins` parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.
- Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.
- Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.
- Auth/Codex: load legacy OAuth sidecar credentials in the embedded runner's secrets-runtime auth loaders so Telegram replies, cron-triggered turns, and other isolated sub-agent lanes can reach the existing #83312 refresh-and-rewrite migration instead of failing with `No API key found for provider "openai-codex"` until the user runs `openclaw doctor`. Thanks @Totalsolutionsync and @romneyda.
- Codex/failover: classify `deactivated_workspace` as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.
- Exec: keep configured `tools.exec.pathPrepend` entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
## 2026.5.20
### Changes
- Exec approvals: remove the old `cat SKILL.md && printf ... && <skill-wrapper>` allowlist compatibility path so skill files must be loaded with the read tool and only the real skill executable is auto-allowed.
- Discord: let voice sessions follow configured Discord users into voice channels, with allowed-channel checks, multi-user handoff, bounded reconciliation, and DAVE recovery preservation. (#84264) Thanks @fuller-stack-dev.
- Discord/voice: include bounded `IDENTITY.md`, `USER.md`, and `SOUL.md` profile context in realtime voice session instructions by default, with `voice.realtime.bootstrapContextFiles: []` available to disable it. (#84499) Thanks @fuller-stack-dev.
- Dependencies: bump the bundled Codex harness to `@openai/codex` `0.132.0` and refresh the app-server model-list docs for the new catalog.
- CLI/policy: add the bundled Policy plugin for policy-backed channel conformance checks, doctor lint findings, and opt-in workspace repair. (#80407) Thanks @giodl73-repo.
- Agents/config: allow `agents.list[].experimental.localModelLean` so lean local-model mode can be enabled for one configured agent instead of globally. (#84073) Thanks @dutifulbob.
- Providers/xAI: add device-code OAuth login so remote and headless setups can authorize xAI without a localhost browser callback. (#84005) Thanks @fuller-stack-dev.
- Providers/OpenRouter: honor provider-level `params.provider` routing policy for OpenRouter requests, with model and agent params overriding the defaults. Thanks @amknight.
### Fixes
- CLI/tasks: include stale-running task maintenance decisions in `openclaw tasks maintenance --json` so retained and reconcile candidates explain backing-session, cron, CLI, and wedged-subagent state. (#84691) Thanks @efpiva.
- Codex app-server: keep system-prompt reports working when bootstrap hooks provide workspace files with only a path and content, so hook-supplied SOUL/IDENTITY/TOOLS/USER context still reports injected characters correctly. (#84736) Thanks @JARVIS-Glasses.
- Providers/MiniMax music: stop advertising `durationSeconds` control and remove prompt-injected duration hints, so `music_generate` reports MiniMax duration as an unsupported override instead of suggesting MiniMax can enforce track length. Fixes #84508. Thanks @neeravmakwana.
- Doctor: warn when sandbox tool policy hides configured MCP server tools before provider requests. (#84699) Thanks @nxmxbbd.
- WhatsApp: update Baileys to `7.0.0-rc12`.
- Build: suppress per-locale `rolldown-plugin-dts:fake-js` CommonJS dts warnings emitted while bundling the intentionally-inlined `zod/v4/locales/*.d.cts` files, so `pnpm build` output stays readable after the 0.25.1 plugin bump. Thanks @romneyda.
- CLI/nodes: route lazy plugin-registration logs to stderr for JSON-mode `openclaw nodes` commands so stdout stays parseable. (#84684) Thanks @TurboTheTurtle.
- Approvals: route manual `/approve` decisions through the trusted approval runtime so active exec and plugin approvals no longer look unknown or expired.
- Mac app: update the About settings copyright year to 2026. (#84385) Thanks @pejmanjohn.
- Dependencies: update `@openclaw/fs-safe` to `0.2.7` so OpenClaw's default Python-helper-off policy keeps best-effort Node write fallbacks for private stores, secret writes, run logs, and media attachments on Linux/macOS.
- Infra/secrets: restore the fail-closed contract for `tryReadSecretFileSync` so credential loaders that pass `rejectSymlink: true` (Telegram, LINE, Zalo, IRC, Nextcloud Talk tokens) refuse symlinked credential files instead of silently accepting them, and the infra-state CI shard's secret-file symlink test passes again. Thanks @romneyda.
- Browser: honor the configured image sanitization limit for screenshots and labeled snapshots so browser-captured images follow the same resize policy as other image results. (#84595)
- Doctor: remove unrecognized `models.providers.*.models[*].compat.thinkingFormat` values during `doctor --fix` so stale provider model config can validate after upgrade. Fixes #77803.
- Doctor: warn when `openclaw.json` stores plaintext secret-bearing config fields, including model provider API keys and sensitive provider headers. (#84718) Thanks @lukaIvanic.
- Status: show the configured default, session-selected model, reason, clear hint, and docs link when a session remains pinned to a model that differs from `agents.defaults.model.primary`.
- WebChat: clear stale typing indicators when session change events mark the active chat run complete.
- Mac app: keep local packaging signed with a stable app identity for permission testing and fix Control UI production builds under current Vite/Highlight.js exports.
- macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows.
- Cron: deliver preferred final assistant output for successful scheduled runs when trailing plain tool warnings remain in diagnostics instead of marking the run failed.
- fix(mattermost): fail closed on missing channel type [AI]. (#84091) Thanks @pgondhi987.
- Recheck rebuilt system.run argv [AI]. (#84090) Thanks @pgondhi987.
- CLI: keep the private QA subcommand out of exported command descriptors unless `OPENCLAW_ENABLE_PRIVATE_QA_CLI=1`, so root help and subcommand markers match runtime registration. (#84519)
- CLI/cron: bound `openclaw cron show` job lookup pagination so non-advancing or unbounded `cron.list` responses fail instead of hanging the command. Fixes #83856. (#83989)
- Agents/messages: stop message-tool-only turns after a successful source-channel `message` send while keeping transcript mirrors under the session write lock. (#84289)
- Agents: filter silent heartbeat response-tool transcript artifacts out of embedded context snapshots so later user turns are not polluted by heartbeat no-op messages. (#83477) Thanks @fuller-stack-dev.
- Agents/OpenAI: log repeated strict tool-schema downgrade diagnostics once per provider/model/tool signature, reducing duplicate debug noise while preserving `strict=false` fallback behavior. Fixes #82930. (#82933) Thanks @galiniliev.
- Agents/code mode: spell out the `exec` tool's JavaScript/TypeScript, no Node module, and catalog-bridge constraints in model-visible schema text so agents can use enabled tools without trial-and-error. (#84269) Thanks @Kaspre.
- Codex: give `image_generate` dynamic-tool calls a 120s default watchdog when no per-call or configured image timeout is set, so image generation no longer falls back to the generic 30s bridge timeout. (#84254) Thanks @moritzmmayerhofer.
- Codex: avoid duplicate dynamic tool terminal diagnostics while large diagnostic backlogs drain without blocking tool responses. (#82937) Thanks @galiniliev.
- CLI/message: include a stable top-level `messageId` in `openclaw message --json` output when channel sends return one. (#84191) Thanks @100menotu001.
- Cron: preserve legacy top-level array `jobs.json` stores when loading or adding scheduled jobs so old cron jobs are no longer treated as an empty store during upgrade. Fixes #60799. (#84433) Thanks @IWhatsskill.
- Gateway/agents: use an agent's `identity.name` in Gateway agent summaries when `agents.list[].name` is unset, so configured agent labels remain visible in clients. (#84355; refs #57835) Thanks @luoyanglang.
- Channels/replies: keep normal `/verbose` failed-tool progress compact in message-tool replies and prevent late text-only tool output from appearing after the final answer. (#84303) Thanks @VACInc.
- Plugins/hooks: apply a default 30-second timeout to `before_compaction` and `after_compaction` hooks so a hung plugin handler no longer blocks compaction completion. (#84153)
- Discord: preserve reusable presentation buttons through portable conversion and Discord component registration. (#84187) Thanks @100menotu001.
- Discord: preserve disabled presentation buttons when adapting and rendering Discord message controls. (#84188) Thanks @100menotu001.
- Twitch: add a test-only client-manager registry reset helper so non-isolated Twitch tests can clear cached managers between cases. Fixes #83887. (#84244) Thanks @hclsys.
- Cron: run main-session scheduled work on a cron-owned wake lane while preserving reply delivery context, so background cron turns no longer block human main-session chat. Fixes #82766. (#82767) Thanks @galiniliev.
- Cron: use structured embedded-run denial metadata for isolated scheduled tasks so blocked exec requests fail the job without treating ordinary assistant prose as a denial. (#84067) Thanks @abnershang.
- Cron: keep recovered tool warnings diagnostic for successful scheduled runs so final cron output is delivered instead of being replaced by a post-processing warning. (#84045) Thanks @abnershang.
- Plugins/perf: thread explicit plugin discovery results through `loadBundledCapabilityRuntimeRegistry`, `resolveBundledPluginSources`, and `listChannelCatalogEntries` so callers that already hold a discovery result skip redundant filesystem walks. Thanks @SebTardif.
- harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)
- CLI: preserve embedded equals signs in inline root option values instead of truncating after the second separator. (#83995) Thanks @ThiagoCAltoe.
- Matrix/config: accept `messages.queue.byChannel.matrix` queue overrides and keep queue provider schema/type keys aligned for Matrix, Google Chat, and Mattermost. Thanks @bdjben.
- CLI: format `openclaw acp client` failures through the shared error formatter so object-shaped errors stay readable instead of printing `[object Object]`. Fixes #83904. (#84080)
- Providers/Ollama: default unknown-capabilities models to tool-capable so discovered native Ollama models can use tools when `/api/show` omits capabilities. (#84055) Thanks @dutifulbob.
- Codex app-server: disable native Code Mode, user MCP, and app-backed plugin execution while OpenClaw sandboxing is active, routing shell access through `sandbox_exec`/`sandbox_process` instead. (#84388) Thanks @joshavant.
- Installer/Windows: launch `install.ps1` onboarding as an attached child process so fresh native Windows installs do not freeze visibly at `Starting setup...` or corrupt the wizard's terminal rendering.
- CLI/update: keep restart health checks working across one-version CLI/Gateway protocol skew and use the managed Gateway service Node for all follow-up commands even when the package root is unchanged, so `openclaw update` no longer silently switches the gateway to a different Node binary when multiple Node installations are present. Thanks @amknight.
- CLI/gateway: include the running Gateway version in `gateway status` JSON output, preserving existing server metadata while falling back to status RPC data for read probes. Fixes #56222. Thanks @galiniliev.
- Memory/search: close local embedding providers when active-memory searches time out so pending local model loads and embedding contexts are aborted and released. (#83858) Thanks @brokemac79.
- CLI/nodes: request pending node surface approval scopes before `openclaw nodes approve` so exec-capable node approval can use admin-scoped Gateway credentials instead of failing with `missing scope: operator.admin`. (#84392) Thanks @joshavant.
- Gateway: reject slow node event sends before outbound buffers grow unbounded and log the rejected payload diagnostic. (#84387) Thanks @samzong.
- Agents: include bounded trajectory queued-writer diagnostics in `pi-trajectory-flush` timeout warnings so flush stalls show pending writes, queued bytes, and append state. Fixes #82961. (#82962) Thanks @galiniliev.
- Agents/subagents: recover stale completion announces by retrying unsupported transcript-wait wakes without transcript waiting and forcing a message-tool handoff when the requester run is already stale. Fixes #83699. (#83700) Thanks @galiniliev.
- Agents/subagents: constrain wildcard subagent target allowlists to configured agents while preserving explicitly listed compatibility targets. Fixes #84040. (#84357) Thanks @joshavant.
- Providers/Anthropic: route Anthropic model refs selected with Claude CLI auth through the Claude CLI runtime so shorthand refs such as `anthropic/opus-4.7` no longer fall back to embedded Anthropic billing. Fixes #84222. (#84374) Thanks @joshavant.
- Agents: honor explicit `models.providers.<id>.timeoutSeconds` values above the default idle watchdog for cloud and self-hosted providers, so long first-token waits no longer fall back at ~120s when the provider timeout is higher. (#83979) Thanks @yujiawei.
- Agents/Codex: keep encrypted Responses reasoning replay provenance-bound so stale mirrored Codex transcripts drop invalid encrypted content before request assembly while preserving matching same-session replay. Fixes #83836. (#84367) Thanks @joshavant.
- Agents/subagents: skip stale embedded-run wake probes for dormant completion requesters, so late subagent completions go straight to requester-agent/direct handoff instead of producing `reason=no_active_run` queue noise. (#82964) Thanks @galiniliev.
- CLI: retry config snapshot reads after a transient failure so one rejected read no longer poisons later commands in the same process. (#83931) Thanks @honor2030.
- Media: decode URL path basenames before using them as remote media fallback filenames, so files like `My%20Report.pdf` are surfaced as `My Report.pdf`. Fixes #84050. (#84052) Thanks @jbetala7.
- WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to `channels.whatsapp.groups` without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.
- WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga.
- CLI/TUI: include gateway plugin slash commands in TUI autocomplete, so connected sessions can suggest plugin-owned commands exposed by the running Gateway. (#83640) Thanks @se7en-agent.
- Gateway/mobile: restore QR setup-code handoff of bounded operator tokens for iOS and Android onboarding while keeping admin and pairing scopes out of bootstrap. (#83684) Thanks @ngutman.
- iOS: repair Release archive compilation for the TestFlight build. (#84255) Thanks @ngutman.
- Agents/compaction: bound plugin-owned CLI transcript compaction with the host safety timeout so a hung context engine can no longer stall post-turn cleanup. (#84083) Thanks @100yenadmin.
- Control UI/usage: truncate long context skill, tool, and file names in the usage panel while keeping the full name available on hover. (#42197) Thanks @Rain120.
- Codex: respect explicit `models auth order set` and `config.auth.order` precedence over stale `lastGood` in `/codex account`, and show `no working credential` when every explicit-order profile is ineligible instead of marking a lower-ranked profile as active. Fixes #84386. (#84412) Thanks @openperf.
- Agents: honor `messages.suppressToolErrors` for mutating tool failures so configured chat surfaces do not receive separate warning payloads. (#81561) Thanks @moeedahmed.
- Agents/fallback: surface billing guidance for mixed rate-limit plus billing fallback exhaustion instead of generic failure copy. Fixes #79396. (#79489) Thanks @aayushprsingh.
## 2026.5.19
### Changes
- Agents: clarify that fixes should default to clean bounded refactors, lean internals, and explicit plugin SDK/API deprecation paths.
- Agents/tools: normalize Swagger/OpenAPI refs and OpenAPI schema annotations when preparing tool parameter schemas.
- Dependencies: update `@openclaw/proxyline` to 0.3.3.
- Dependencies: update Pi packages to 0.75.1 and raise the minimum supported Node.js 22 line to 22.19.
- Docker/Podman: add `OPENCLAW_IMAGE_APT_PACKAGES` as the runtime-neutral image build arg for extra apt packages while keeping `OPENCLAW_DOCKER_APT_PACKAGES` as a legacy fallback. (#62431) Thanks @urtabajev.
@@ -34,6 +206,7 @@ Docs: https://docs.openclaw.ai
- Agents/skills: tighten bundled skill prompts and metadata, quote skill descriptions, refresh current CLI/API guidance, and update embedded sherpa-onnx runtime downloads.
- Skills: update the Obsidian skill to target the official `obsidian` CLI and require its registered binary instead of the third-party `obsidian-cli`.
- Skills: add a Python debugging skill for pdb, breakpoint(), post-mortem inspection, and debugpy remote attach.
- Codex: add `/codex plugins list`, `enable`, and `disable` for managing configured native Codex plugins from chat without editing config by hand.
- Plugins/messages: add presentation capability limits for channel renderers, adapt rich message controls before native rendering, and mark legacy `interactive`/Slack directive producer APIs as deprecated.
- Plugins/subagents: store channel delivery routes as canonical session metadata and deprecate ad hoc subagent hook delivery-origin fields in favor of core route projection.
- Proxy: support HTTPS managed forward-proxy endpoints and scoped `proxy.tls.caFile` CA trust for proxy endpoint TLS. (#79171) Thanks @jesse-merhi.
@@ -57,6 +230,8 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/node: reject invalid explicit `node run --port` values instead of silently falling back to the configured or default port. Fixes #83923. Thanks @davinci282828.
- CLI: reject explicit port numbers above 65535 before they reach Gateway or Node bind paths. Fixes #83900. (#84008) Thanks @hclsys.
- Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu.
- Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss.
- Memory Wiki: preserve fs-safe diagnostics when bridge source page writes fail for non-symlink filesystem safety reasons, so directory collisions are reported with the underlying error code. (#83776) Thanks @TurboTheTurtle.
@@ -64,12 +239,16 @@ Docs: https://docs.openclaw.ai
- Telegram: keep queued forum-topic follow-up messages from inheriting superseded source abort signals, so later same-topic user turns can still run and reply after an active turn is replaced. (#83827) Thanks @VACInc.
- CLI/update: bypass npm freshness filters consistently during managed package and plugin installs so freshly published release plugins remain installable. Thanks @jalehman.
- CLI/update: guide root-owned npm install EACCES recovery by stopping the managed Gateway before manual package replacement, then reinstalling and restarting the service. Fixes #83747. (#83757) Thanks @brokemac79.
- Twitch: register refreshing chat tokens with Twurple's chat intent so automatic token refresh keeps chat access available. (#83750) Thanks @TurboTheTurtle.
- Agents/subagents: keep collect-mode announce queues batching unresolved-origin items with compatible same-route messages and resume collection after a true cross-channel drain when a later compatible batch remains. Fixes #83577.
- CLI/config: preserve numeric-looking record keys such as Discord guild IDs when creating missing config containers with `config set`. (#83769) Thanks @TurboTheTurtle.
- Skills: refresh existing session skill snapshots when watched skill roots change, so changed extra skill directories take effect without starting a new session. Fixes #83782. (#83800) Thanks @hclsys.
- Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.
- Providers/Anthropic: preserve Claude 4 image capability when configured model refs resolve through a stale local catalog row. (#83756) Thanks @TurboTheTurtle.
- Providers/DeepSeek: normalize MCP tool schemas with `anyOf`/`oneOf` unions before normal and compaction requests reach DeepSeek, preventing union-shaped parameters from being rejected. (#83766) Thanks @TurboTheTurtle.
- Control UI: render live tool progress from session-scoped `session.tool` Gateway events so externally started runs show their tool cards in the active session. (#83734) Thanks @TurboTheTurtle.
- Outbound: resolve send-capable channel plugins from the active runtime registry when the pinned startup registry only has setup metadata. (#83733) Thanks @TurboTheTurtle.
- Discord: preserve streamed reply previews when recovered tool-warning finals are delivered before or after the assistant's final reply. (#84169) Thanks @neeravmakwana.
- Control UI: keep the chat delete confirmation popover clamped inside the visible viewport on small screens. (#83804) Thanks @ThiagoCAltoe.
- Browser: enforce current-tab URL allowlist checks for `/act` evaluate/batch actions and `/highlight` routes while leaving tab-management actions unblocked. (#78523)
- CI: require real-behavior-proof verdict markers to come from the ClawSweeper GitHub App before accepting exact-head proof. (#83692)
@@ -85,11 +264,15 @@ Docs: https://docs.openclaw.ai
- Telegram: keep verbose tool progress visible without mirroring non-final progress into active session transcripts, preventing embedded provider replies from aborting mid-run. (#83631) Thanks @kurplunkin.
- Telegram: log successful outbound text and media deliveries with account, chat, message, operation, thread, reply, silent, and chunk metadata while keeping message bodies out of logs. Fixes #83196. (#83247) Thanks @jrwrest.
- Cron: link isolated scheduled task runs to their stable cron session so task status and cleanup can follow the backing agent run. (#83606) Thanks @jai.
- Codex app-server: mark Codex-native subagent task mirrors terminal when blocked or failed spawn-agent calls arrive with stale initializing child state, preventing task registry entries from staying running. Fixes #83852. (#83945) Thanks @joshavant.
- CLI: enforce the documented Node.js 22.19 runtime floor in the source launcher.
- Release stability: repair broad-gate regressions in requester-agent completion handoff, QA-Lab mock spawn attribution, Slack monitor test isolation, plugin uninstall peer fixtures, and Node-floor launcher contract coverage.
- Agents/replies: persist queued follow-up user messages and assistant error stubs only once across model-fallback retries, preventing repeated provider rejections from corrupted same-role session transcripts. Fixes #83404. (#83417) Thanks @yetval.
- Telegram: preserve reply-target context for bare mention replies on runtime-only turns so the model sees the replied-to message body. Fixes #83767. (#83953) Thanks @joshavant.
- ClawHub: preserve configured base URL path prefixes when building API request URLs, so self-hosted ClawHub instances mounted under a subpath keep routing correctly. (#83982) Thanks @ThiagoCAltoe.
- Slack: persist delivered inbound message IDs and fail closed when same-channel thread replies lose their thread context, preventing delayed duplicate replies and accidental channel-root posts. Fixes #83521. Thanks @shannon0430.
- Codex app-server: complete OpenClaw dynamic tool diagnostics at the request boundary so successful, failed, timed out, aborted, and blocked tool calls do not leave active tool state behind. Fixes #83474. Thanks @rozmiarD.
- Doctor/Codex: warn when Linux host policy blocks the Codex bwrap user or network namespace path used by sandboxed app-server turns, with Ubuntu/AppArmor repair guidance. Refs #83018.
- Gateway/config: keep config writes from failing on unrelated unresolved auth-profile SecretRefs while preserving live auth-profile runtime snapshots.
- Gateway/sessions: clear stored CLI provider resume bindings on non-subagent `/reset` so the next turn starts a fresh provider-side CLI conversation instead of resuming old context. (#83448) Thanks @jasonyliu.
- Doctor: preserve legacy whole-agent Claude CLI intent by moving matching Anthropic model selections to model-scoped runtime policy before removing stale runtime pins. Fixes #83491. Thanks @danielcrick.
@@ -101,6 +284,7 @@ Docs: https://docs.openclaw.ai
- Agents: guard final-delivery fresh session routing against mismatched logical sessions before reusing recovered delivery context. (#83928) Thanks @joshavant.
- Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.
- Media: install Sharp with the root package and fall back to sips, Windows native imaging, ImageMagick, GraphicsMagick, or ffmpeg for image resizing/conversion when Sharp is unavailable. Fixes #83401. Thanks @scotthuang.
- Channels/bundled: append `openclaw doctor --fix` guidance to the bundled-channel load warnings emitted on `ERR_MODULE_NOT_FOUND` / `MODULE_NOT_FOUND` (including those wrapped on `.cause` by the native-require loader), so users hitting unstaged plugin runtime deps (e.g. `nostr-tools`) see an actionable repair hint instead of a bare module-not-found warning. (#76974) Thanks @BSG2000.
- Telegram: deliver generated media completions back into forum topics by preserving topic IDs across requester-agent handoff. (#83556) Thanks @fuller-stack-dev.
- Gateway: defer update-check startup until after readiness so package update checks no longer block sidecar-ready startup, while preserving update broadcasts and shutdown cleanup. (#83520) Thanks @samzong.
- Telegram: keep `/btw` and read-only status commands from aborting active runs, and avoid retaining raw update payloads in timed-out spool tombstones. Refs #83272.
@@ -229,6 +413,8 @@ Docs: https://docs.openclaw.ai
- Agents/OpenAI: preserve deterministic tool payload ordering for prompt-cache reuse across OpenAI Responses and chat completions calls. (#82940) Thanks @galiniliev.
- ACP/Codex: honor terminal ACP turn results so failed Codex/acpx runs are not recorded as successful after only progress text. Fixes #79522. Thanks @dudaefj.
- Telegram: warn when a media group drops photos that fail to download, including albums where every photo is skipped. Fixes #55216. (#82987) Thanks @eldar702.
- Agents/diagnostics: treat repeated same-handle embedded-run cleanup as idempotent while preserving true replacement-handle mismatch diagnostics. Fixes #82959. (#82960) Thanks @galiniliev.
- Agents/subagents: preserve high-priority `AGENTS.md` policy in bootstrap context when oversized files are trimmed, and warn agents to read the full policy file before relying on scoped rules. Fixes #82920. (#82921) Thanks @galiniliev.
- Agents/skills: apply the full effective tool policy pipeline to inline `command-dispatch: tool` skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525)
- Codex: avoid spawning native hook relay subprocesses for post-tool/finalize events with no registered hook handlers while preserving pre-tool safety and approval relays. Fixes #76552. (#78004) Thanks @evgyur.
- Channel accounts: keep top-level default channel accounts visible when named accounts are added alongside default credential material, so mixed legacy/new account configs keep resolving `default` instead of silently dropping it.
@@ -265,6 +451,7 @@ Docs: https://docs.openclaw.ai
- Signal: preserve mixed-case group IDs through routing and session persistence so group auto-replies keep delivering after updates. Fixes #82827.
- Agents/tools: keep the `message` tool available in embedded runs when it is explicitly allowed through `tools.alsoAllow` or runtime tool allowlists, so channel plugins with custom reply delivery can still use configured message sends. Fixes #82833. Thanks @cn1313113.
- WhatsApp: honor forced document delivery for outbound image, GIF, and video media so `forceDocument`/`asDocument` sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.
- WhatsApp: reject symlinked Web credential files across auth checks and socket startup so unsafe `creds.json` paths cannot be read through. Thanks @mcaxtr.
- WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as `file.pdf` and `file.csv` instead of an extensionless `file`. Thanks @mcaxtr.
- Process/diagnostics: report active lane blockers in lane wait warnings so `queueAhead=0` no longer hides commands waiting behind active work. Fixes #82791. (#82792) Thanks @galiniliev.
- Process/diagnostics: stop counting the active processing turn as queued backlog in liveness warnings so transient max-only event-loop spikes do not surface as gateway warnings.
@@ -644,6 +831,7 @@ Docs: https://docs.openclaw.ai
- Require canonical node platform IDs [AI]. (#81880) Thanks @pgondhi987.
- Agents/Azure OpenAI Responses: default unset Azure OpenAI API versions to `preview` so `/openai/v1/responses` calls use Azure's current Responses API route. (#82026) Thanks @leoge007.
- Control UI/WebChat: compact the desktop chat header controls into a single aligned row so the session, model, thinking, and action controls no longer waste vertical space. Thanks @BunsDev.
- Control UI/settings: widen the Personal quick-settings card to a 3/1 desktop split and keep Appearance/Automations below it on narrower layouts. Thanks @BunsDev.
- Agents/model catalog: reuse manifest model-id normalization metadata while loading persisted read-only catalog rows, avoiding repeated metadata scans.
- Agents: retry empty final turns for generic `anthropic-messages` providers instead of limiting non-visible recovery to Kimi, so custom/proxied Anthropic-compatible routes can recover with a visible answer. Addresses #46080. Thanks @wmgx, @w1tv, and @iFwu.
- Agents/replies: strip workflow `<function_response>` scaffolding from user-visible sanitizer paths so raw tool output does not leak into chat history, transcript mirrors, or channel replies. Fixes #47444. Thanks @5toCode.

View File

@@ -72,7 +72,8 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile \
--config.supportedArchitectures.os=linux \
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
--config.supportedArchitectures.libc=glibc
--config.supportedArchitectures.libc=glibc && \
pnpm store add source-map@0.6.1
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Matrix's native downloader can hit transient release CDN errors while
@@ -116,8 +117,8 @@ ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
# Prune dev dependencies and strip build-only metadata before copying
# runtime assets into the final image.
# Prune dev dependencies, omitted plugin runtime packages, and build-only
# metadata before copying runtime assets into the final image.
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
@@ -127,8 +128,8 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
--config.supportedArchitectures.os=linux \
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
--config.supportedArchitectures.libc=glibc && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
node scripts/postinstall-bundled-plugins.mjs && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
node scripts/check-package-dist-imports.mjs /app

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Peter Steinberger
Copyright (c) 2026 OpenClaw Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -2,6 +2,398 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.5.20</title>
<pubDate>Thu, 21 May 2026 21:19:52 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052090</sparkle:version>
<sparkle:shortVersionString>2026.5.20</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.20</h2>
<h3>Changes</h3>
<ul>
<li>Exec approvals: remove the old <code>cat SKILL.md && printf ... && <skill-wrapper></code> allowlist compatibility path so skill files must be loaded with the read tool and only the real skill executable is auto-allowed.</li>
<li>Discord: let voice sessions follow configured Discord users into voice channels, with allowed-channel checks, multi-user handoff, bounded reconciliation, and DAVE recovery preservation. (#84264) Thanks @fuller-stack-dev.</li>
<li>Discord/voice: include bounded <code>IDENTITY.md</code>, <code>USER.md</code>, and <code>SOUL.md</code> profile context in realtime voice session instructions by default, with <code>voice.realtime.bootstrapContextFiles: []</code> available to disable it. (#84499) Thanks @fuller-stack-dev.</li>
<li>Dependencies: bump the bundled Codex harness to <code>@openai/codex</code> <code>0.132.0</code> and refresh the app-server model-list docs for the new catalog.</li>
<li>CLI/policy: add the bundled Policy plugin for policy-backed channel conformance checks, doctor lint findings, and opt-in workspace repair. (#80407) Thanks @giodl73-repo.</li>
<li>Agents/config: allow <code>agents.list[].experimental.localModelLean</code> so lean local-model mode can be enabled for one configured agent instead of globally.</li>
<li>Providers/xAI: add device-code OAuth login so remote and headless setups can authorize xAI without a localhost browser callback. (#84005) Thanks @fuller-stack-dev.</li>
<li>Providers/OpenRouter: honor provider-level <code>params.provider</code> routing policy for OpenRouter requests, with model and agent params overriding the defaults. Thanks @amknight.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>CLI/tasks: include stale-running task maintenance decisions in <code>openclaw tasks maintenance --json</code> so retained and reconcile candidates explain backing-session, cron, CLI, and wedged-subagent state. (#84691) Thanks @efpiva.</li>
<li>Codex app-server: keep system-prompt reports working when bootstrap hooks provide workspace files with only a path and content, so hook-supplied SOUL/IDENTITY/TOOLS/USER context still reports injected characters correctly. (#84736) Thanks @JARVIS-Glasses.</li>
<li>Providers/MiniMax music: stop advertising <code>durationSeconds</code> control and remove prompt-injected duration hints, so <code>music_generate</code> reports MiniMax duration as an unsupported override instead of suggesting MiniMax can enforce track length. Fixes #84508. Thanks @neeravmakwana.</li>
<li>Doctor: warn when sandbox tool policy hides configured MCP server tools before provider requests. (#84699) Thanks @nxmxbbd.</li>
<li>WhatsApp: update Baileys to <code>7.0.0-rc12</code>.</li>
<li>Build: suppress per-locale <code>rolldown-plugin-dts:fake-js</code> CommonJS dts warnings emitted while bundling the intentionally-inlined <code>zod/v4/locales/*.d.cts</code> files, so <code>pnpm build</code> output stays readable after the 0.25.1 plugin bump. Thanks @romneyda.</li>
<li>CLI/nodes: route lazy plugin-registration logs to stderr for JSON-mode <code>openclaw nodes</code> commands so stdout stays parseable. (#84684) Thanks @TurboTheTurtle.</li>
<li>Approvals: route manual <code>/approve</code> decisions through the trusted approval runtime so active exec and plugin approvals no longer look unknown or expired.</li>
<li>Mac app: update the About settings copyright year to 2026. (#84385) Thanks @pejmanjohn.</li>
<li>Dependencies: update <code>@openclaw/fs-safe</code> to <code>0.2.7</code> so OpenClaw's default Python-helper-off policy keeps best-effort Node write fallbacks for private stores, secret writes, run logs, and media attachments on Linux/macOS.</li>
<li>Infra/secrets: restore the fail-closed contract for <code>tryReadSecretFileSync</code> so credential loaders that pass <code>rejectSymlink: true</code> (Telegram, LINE, Zalo, IRC, Nextcloud Talk tokens) refuse symlinked credential files instead of silently accepting them, and the infra-state CI shard's secret-file symlink test passes again. Thanks @romneyda.</li>
<li>Browser: honor the configured image sanitization limit for screenshots and labeled snapshots so browser-captured images follow the same resize policy as other image results. (#84595)</li>
<li>Doctor: remove unrecognized <code>models.providers.*.models[*].compat.thinkingFormat</code> values during <code>doctor --fix</code> so stale provider model config can validate after upgrade. Fixes #77803.</li>
<li>Doctor: warn when <code>openclaw.json</code> stores plaintext secret-bearing config fields, including model provider API keys and sensitive provider headers. (#84718) Thanks @lukaIvanic.</li>
<li>Status: show the configured default, session-selected model, reason, clear hint, and docs link when a session remains pinned to a model that differs from <code>agents.defaults.model.primary</code>.</li>
<li>WebChat: clear stale typing indicators when session change events mark the active chat run complete.</li>
<li>Mac app: keep local packaging signed with a stable app identity for permission testing and fix Control UI production builds under current Vite/Highlight.js exports.</li>
<li>macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows.</li>
<li>Cron: deliver preferred final assistant output for successful scheduled runs when trailing plain tool warnings remain in diagnostics instead of marking the run failed.</li>
<li>fix(mattermost): fail closed on missing channel type [AI]. (#84091) Thanks @pgondhi987.</li>
<li>Recheck rebuilt system.run argv [AI]. (#84090) Thanks @pgondhi987.</li>
<li>CLI: keep the private QA subcommand out of exported command descriptors unless <code>OPENCLAW_ENABLE_PRIVATE_QA_CLI=1</code>, so root help and subcommand markers match runtime registration. (#84519)</li>
<li>CLI/cron: bound <code>openclaw cron show</code> job lookup pagination so non-advancing or unbounded <code>cron.list</code> responses fail instead of hanging the command. Fixes #83856. (#83989)</li>
<li>Agents/messages: stop message-tool-only turns after a successful source-channel <code>message</code> send while keeping transcript mirrors under the session write lock. (#84289)</li>
<li>Agents: filter silent heartbeat response-tool transcript artifacts out of embedded context snapshots so later user turns are not polluted by heartbeat no-op messages. (#83477) Thanks @fuller-stack-dev.</li>
<li>Agents/OpenAI: log repeated strict tool-schema downgrade diagnostics once per provider/model/tool signature, reducing duplicate debug noise while preserving <code>strict=false</code> fallback behavior. Fixes #82930. (#82933) Thanks @galiniliev.</li>
<li>Agents/code mode: spell out the <code>exec</code> tool's JavaScript/TypeScript, no Node module, and catalog-bridge constraints in model-visible schema text so agents can use enabled tools without trial-and-error. (#84269) Thanks @Kaspre.</li>
<li>Codex: give <code>image_generate</code> dynamic-tool calls a 120s default watchdog when no per-call or configured image timeout is set, so image generation no longer falls back to the generic 30s bridge timeout. (#84254) Thanks @moritzmmayerhofer.</li>
<li>Codex: avoid duplicate dynamic tool terminal diagnostics while large diagnostic backlogs drain without blocking tool responses. (#82937) Thanks @galiniliev.</li>
<li>CLI/message: include a stable top-level <code>messageId</code> in <code>openclaw message --json</code> output when channel sends return one. (#84191) Thanks @100menotu001.</li>
<li>Cron: preserve legacy top-level array <code>jobs.json</code> stores when loading or adding scheduled jobs so old cron jobs are no longer treated as an empty store during upgrade. Fixes #60799. (#84433) Thanks @IWhatsskill.</li>
<li>Gateway/agents: use an agent's <code>identity.name</code> in Gateway agent summaries when <code>agents.list[].name</code> is unset, so configured agent labels remain visible in clients. (#84355; refs #57835) Thanks @luoyanglang.</li>
<li>Channels/replies: keep normal <code>/verbose</code> failed-tool progress compact in message-tool replies and prevent late text-only tool output from appearing after the final answer. (#84303) Thanks @VACInc.</li>
<li>Plugins/hooks: apply a default 30-second timeout to <code>before_compaction</code> and <code>after_compaction</code> hooks so a hung plugin handler no longer blocks compaction completion. (#84153)</li>
<li>Discord: preserve disabled presentation buttons when adapting and rendering Discord message controls. (#84188) Thanks @100menotu001.</li>
<li>Twitch: add a test-only client-manager registry reset helper so non-isolated Twitch tests can clear cached managers between cases. Fixes #83887. (#84244) Thanks @hclsys.</li>
<li>Cron: run main-session scheduled work on a cron-owned wake lane while preserving reply delivery context, so background cron turns no longer block human main-session chat. Fixes #82766. (#82767) Thanks @galiniliev.</li>
<li>Cron: use structured embedded-run denial metadata for isolated scheduled tasks so blocked exec requests fail the job without treating ordinary assistant prose as a denial. (#84067) Thanks @abnershang.</li>
<li>Cron: keep recovered tool warnings diagnostic for successful scheduled runs so final cron output is delivered instead of being replaced by a post-processing warning. (#84045) Thanks @abnershang.</li>
<li>Plugins/perf: thread explicit plugin discovery results through <code>loadBundledCapabilityRuntimeRegistry</code>, <code>resolveBundledPluginSources</code>, and <code>listChannelCatalogEntries</code> so callers that already hold a discovery result skip redundant filesystem walks. Thanks @SebTardif.</li>
<li>harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.</li>
<li>Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.</li>
<li>CLI/channels: preserve the first line of <code>openclaw channels logs</code> output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed <code>readLogSlice</code> behavior in <code>src/logging/log-tail.ts</code>.</li>
<li>Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)</li>
<li>CLI: preserve embedded equals signs in inline root option values instead of truncating after the second separator. (#83995) Thanks @ThiagoCAltoe.</li>
<li>Matrix/config: accept <code>messages.queue.byChannel.matrix</code> queue overrides and keep queue provider schema/type keys aligned for Matrix, Google Chat, and Mattermost. Thanks @bdjben.</li>
<li>CLI: format <code>openclaw acp client</code> failures through the shared error formatter so object-shaped errors stay readable instead of printing <code>[object Object]</code>. Fixes #83904. (#84080)</li>
<li>Providers/Ollama: default unknown-capabilities models to tool-capable so discovered native Ollama models can use tools when <code>/api/show</code> omits capabilities. (#84055) Thanks @dutifulbob.</li>
<li>Installer/Windows: launch <code>install.ps1</code> onboarding as an attached child process so fresh native Windows installs do not freeze visibly at <code>Starting setup...</code> or corrupt the wizard's terminal rendering.</li>
<li>CLI/update: keep restart health checks working across one-version CLI/Gateway protocol skew and use the managed Gateway service Node for all follow-up commands even when the package root is unchanged, so <code>openclaw update</code> no longer silently switches the gateway to a different Node binary when multiple Node installations are present. Thanks @amknight.</li>
<li>CLI/gateway: include the running Gateway version in <code>gateway status</code> JSON output, preserving existing server metadata while falling back to status RPC data for read probes. Fixes #56222. Thanks @galiniliev.</li>
<li>Memory/search: close local embedding providers when active-memory searches time out so pending local model loads and embedding contexts are aborted and released. (#83858) Thanks @brokemac79.</li>
<li>CLI/nodes: request pending node surface approval scopes before <code>openclaw nodes approve</code> so exec-capable node approval can use admin-scoped Gateway credentials instead of failing with <code>missing scope: operator.admin</code>. (#84392) Thanks @joshavant.</li>
<li>Gateway: reject slow node event sends before outbound buffers grow unbounded and log the rejected payload diagnostic. (#84387) Thanks @samzong.</li>
<li>Agents: include bounded trajectory queued-writer diagnostics in <code>pi-trajectory-flush</code> timeout warnings so flush stalls show pending writes, queued bytes, and append state. Fixes #82961. (#82962) Thanks @galiniliev.</li>
<li>Agents/subagents: recover stale completion announces by retrying unsupported transcript-wait wakes without transcript waiting and forcing a message-tool handoff when the requester run is already stale. Fixes #83699. (#83700) Thanks @galiniliev.</li>
<li>Agents/subagents: constrain wildcard subagent target allowlists to configured agents while preserving explicitly listed compatibility targets. Fixes #84040. (#84357) Thanks @joshavant.</li>
<li>Providers/Anthropic: route Anthropic model refs selected with Claude CLI auth through the Claude CLI runtime so shorthand refs such as <code>anthropic/opus-4.7</code> no longer fall back to embedded Anthropic billing. Fixes #84222. (#84374) Thanks @joshavant.</li>
<li>Agents: honor explicit <code>models.providers.<id>.timeoutSeconds</code> values above the default idle watchdog for cloud and self-hosted providers, so long first-token waits no longer fall back at ~120s when the provider timeout is higher. (#83979) Thanks @yujiawei.</li>
<li>Agents/Codex: keep encrypted Responses reasoning replay provenance-bound so stale mirrored Codex transcripts drop invalid encrypted content before request assembly while preserving matching same-session replay. Fixes #83836. (#84367) Thanks @joshavant.</li>
<li>Agents/subagents: skip stale embedded-run wake probes for dormant completion requesters, so late subagent completions go straight to requester-agent/direct handoff instead of producing <code>reason=no_active_run</code> queue noise. (#82964) Thanks @galiniliev.</li>
<li>CLI: retry config snapshot reads after a transient failure so one rejected read no longer poisons later commands in the same process. (#83931) Thanks @honor2030.</li>
<li>Media: decode URL path basenames before using them as remote media fallback filenames, so files like <code>My%20Report.pdf</code> are surfaced as <code>My Report.pdf</code>. Fixes #84050. (#84052) Thanks @jbetala7.</li>
<li>WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to <code>channels.whatsapp.groups</code> without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.</li>
<li>WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga.</li>
<li>CLI/TUI: include gateway plugin slash commands in TUI autocomplete, so connected sessions can suggest plugin-owned commands exposed by the running Gateway. (#83640) Thanks @se7en-agent.</li>
<li>Gateway/mobile: restore QR setup-code handoff of bounded operator tokens for iOS and Android onboarding while keeping admin and pairing scopes out of bootstrap. (#83684) Thanks @ngutman.</li>
<li>iOS: repair Release archive compilation for the TestFlight build. (#84255) Thanks @ngutman.</li>
<li>Agents/compaction: bound plugin-owned CLI transcript compaction with the host safety timeout so a hung context engine can no longer stall post-turn cleanup. (#84083) Thanks @100yenadmin.</li>
<li>Control UI/usage: truncate long context skill, tool, and file names in the usage panel while keeping the full name available on hover. (#42197) Thanks @Rain120.</li>
<li>Codex: respect explicit <code>models auth order set</code> and <code>config.auth.order</code> precedence over stale <code>lastGood</code> in <code>/codex account</code>, and show <code>no working credential</code> when every explicit-order profile is ineligible instead of marking a lower-ranked profile as active. Fixes #84386. (#84412) Thanks @openperf.</li>
<li>Agents: honor <code>messages.suppressToolErrors</code> for mutating tool failures so configured chat surfaces do not receive separate warning payloads. (#81561) Thanks @moeedahmed.</li>
<li>Agents/fallback: surface billing guidance for mixed rate-limit plus billing fallback exhaustion instead of generic failure copy. Fixes #79396. (#79489) Thanks @aayushprsingh.</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.20/OpenClaw-2026.5.20.zip" length="54396392" type="application/octet-stream" sparkle:edSignature="Ufz+twYjgj5NDg29tG3Ttx/JNyT3/a3EKLciBGvsa38C6Dwqp4yFYC5jSBiSlubwBXhrq8OQDMgavMKtSsclBQ=="/>
</item>
<item>
<title>2026.5.19</title>
<pubDate>Wed, 20 May 2026 21:27:21 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026051990</sparkle:version>
<sparkle:shortVersionString>2026.5.19</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.19</h2>
<h3>Changes</h3>
<ul>
<li>Agents: clarify that fixes should default to clean bounded refactors, lean internals, and explicit plugin SDK/API deprecation paths.</li>
<li>Dependencies: update <code>@openclaw/proxyline</code> to 0.3.3.</li>
<li>Dependencies: update Pi packages to 0.75.1 and raise the minimum supported Node.js 22 line to 22.19.</li>
<li>Docker/Podman: add <code>OPENCLAW_IMAGE_APT_PACKAGES</code> as the runtime-neutral image build arg for extra apt packages while keeping <code>OPENCLAW_DOCKER_APT_PACKAGES</code> as a legacy fallback. (#62431) Thanks @urtabajev.</li>
<li>Gateway/ACPX: attribute startup probe, config, runtime, and resource-count costs in restart traces without changing readiness behavior. (#83300) Thanks @samzong.</li>
<li>Gateway: overlap startup logging and plugin-service startup with channel sidecars to reduce restart ready latency while preserving <code>/readyz</code> sidecar gating. (#83301) Thanks @samzong.</li>
<li>Plugins/admin-http-rpc: allow trusted admin HTTP RPC clients to start and wait for web QR login flows. (#83259) Thanks @liorb-mountapps.</li>
<li>Mac app: redesign Settings pages with consistent card layouts, cached navigation, cleaner permissions/voice/skills/cron/exec/debug panes, and steadier spacing around the native sidebar.</li>
<li>Mac app: refine Voice & Talk recognition-language and wake-phrase settings so they use the same compact card rows as the rest of Settings.</li>
<li>Skills: rename the repo-local Codex closeout review skill and helper to <code>autoreview</code> while preserving the Codex-first fallback behavior.</li>
<li>Skills: add a meme-maker skill for curated template search, local SVG/PNG rendering, Imgflip hosted rendering, and Know Your Meme provenance links.</li>
<li>Skills CLI: allow <code>openclaw skills install</code> and <code>openclaw skills update</code> to target shared managed skills with <code>--global</code>. (#74466) Thanks @Marvae.</li>
<li>Browser: surface pending and recently handled modal dialogs in snapshots, return <code>blockedByDialog</code> when an action opens a modal, and allow <code>browser dialog --dialog-id</code> to answer pending dialogs.</li>
<li>Browser CLI: add <code>openclaw browser evaluate --timeout-ms</code> so long-running page functions can extend both the evaluate action and request timeout budgets. (#83447) Thanks @eefreenyc.</li>
<li>Codex app-server: scope OpenClaw prompt guidance by runtime surface so native Codex keeps Codex-owned base/personality instructions while OpenClaw contributes only runtime context, delivery guidance, and explicitly scoped command hints. (#83454) Thanks @100yenadmin.</li>
<li>Docker/Podman: add <code>OPENCLAW_IMAGE_PIP_PACKAGES</code> for opt-in Python package installation in local image builds. (#83771) Thanks @stephenredmond-straiteis.</li>
<li>Agents/tools: shorten built-in tool descriptions and schema hints across media, messaging, sessions, cron, Gateway, web, image/PDF, TTS, nodes, and plan tools while preserving routing guardrails.</li>
<li>Skills: add node inspector debugging, fused diagram generation, and throwaway spike workflow skills.</li>
<li>CLI/plugins: add <code>defineToolPlugin</code> plus <code>openclaw plugins build</code>, <code>validate</code>, and <code>init</code> for typed simple tool plugins with generated manifest metadata, optional tool declarations, and context factories.</li>
<li>Agents/skills: tighten bundled skill prompts and metadata, quote skill descriptions, refresh current CLI/API guidance, and update embedded sherpa-onnx runtime downloads.</li>
<li>Skills: update the Obsidian skill to target the official <code>obsidian</code> CLI and require its registered binary instead of the third-party <code>obsidian-cli</code>.</li>
<li>Skills: add a Python debugging skill for pdb, breakpoint(), post-mortem inspection, and debugpy remote attach.</li>
<li>Codex: add <code>/codex plugins list</code>, <code>enable</code>, and <code>disable</code> for managing configured native Codex plugins from chat without editing config by hand.</li>
<li>Plugins/messages: add presentation capability limits for channel renderers, adapt rich message controls before native rendering, and mark legacy <code>interactive</code>/Slack directive producer APIs as deprecated.</li>
<li>Plugins/subagents: store channel delivery routes as canonical session metadata and deprecate ad hoc subagent hook delivery-origin fields in favor of core route projection.</li>
<li>Proxy: support HTTPS managed forward-proxy endpoints and scoped <code>proxy.tls.caFile</code> CA trust for proxy endpoint TLS. (#79171) Thanks @jesse-merhi.</li>
<li>QA-Lab: add first-hour 20-turn and optional 100-turn runtime parity scenarios, with tier metadata for standard and soak QA gates. Fixes #80338; refs #80337. Thanks @100yenadmin.</li>
<li>QA-Lab: add <code>openclaw qa suite --runtime-parity-tier</code> and wire the standard Codex-vs-Pi tier into release checks separately from optional/live-only/soak lanes. Fixes #80337. Thanks @100yenadmin.</li>
<li>QA-Lab: add a live-only Codex Pi-shaped Read vocabulary canary so runtime parity catches native workspace-read prompt compatibility drift. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add live-only harness self-health scenarios for plugin hook crashes, manifest contract errors, and WebChat direct-reply self-message routing. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add runtime tool fixture scenarios and coverage reporting for Codex-native workspace tools, OpenClaw dynamic tools, and optional plugin-backed tools. Fixes #80173. Thanks @100yenadmin.</li>
<li>QA-Lab: expose runtime tool fixture coverage through <code>openclaw qa coverage --tools</code>, with optional suite-summary evaluation for parity gate artifacts. Thanks @100yenadmin.</li>
<li>QA-Lab: schedule a live-frontier Codex-vs-Pi runtime token-efficiency artifact lane in the all-lanes QA workflow. Fixes #80175. Thanks @100yenadmin.</li>
<li>QA-Lab: hard-gate required OpenClaw dynamic runtime-tool drift in the standard Codex-vs-Pi tier with a blocking release-check verifier and publish the tool coverage report artifact. Fixes #80339; refs #80319. Thanks @100yenadmin.</li>
<li>QA-Lab: add the personal-agent approval-denial scenario so the benchmark pack verifies denied local reads stop cleanly without tool progress or fixture leaks. (#83150) Thanks @iFiras-Max1.</li>
<li>QA-Lab: extend the personal-agent benchmark pack with a local task followthrough scenario for proof-backed pending, blocked, and done status reporting. Thanks @iFiras-Max1.</li>
<li>QA-Lab: add a report-only dreaming shadow-trial scenario so candidate memory promotion can be evaluated without mutating <code>MEMORY.md</code>. Thanks @iFiras-Max1.</li>
<li>Gateway/performance: add <code>pnpm test:restart:gateway</code> benchmark tooling for repeated restart readiness, downtime, trace, and resource-slope evidence. (#83299) Thanks @samzong.</li>
<li>Android: switch Talk Mode to realtime Gateway relay voice sessions with streaming mic input, realtime audio playback, tool-result bridging, and on-screen transcripts. (#83130) Thanks @sliekens.</li>
<li>Gateway/config: expose config lookup reload metadata so tools can distinguish restart-required, hot-reloadable, and no-op fields before applying config edits. Fixes #81409. (#81612) Thanks @LLagoon3.</li>
<li>Telegram: add allowlisted native DM draft previews for transient tool progress while keeping final answers on the normal persistent delivery path. (#83622) Thanks @akrimm702.</li>
<li>QA-Lab: add a personal-agent share-safe diagnostics artifact scenario so support handoffs keep useful status while omitting raw personal content. Thanks @iFiras-Max1.</li>
<li>QA-Lab: add a personal-agent no-fake-progress scenario so completion claims stay tied to local evidence instead of unsupported external progress. (#83824) Thanks @iFiras-Max1.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>CLI: reject explicit port numbers above 65535 before they reach Gateway or Node bind paths. Fixes #83900. (#84008) Thanks @hclsys.</li>
<li>Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu.</li>
<li>Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss.</li>
<li>Memory Wiki: preserve fs-safe diagnostics when bridge source page writes fail for non-symlink filesystem safety reasons, so directory collisions are reported with the underlying error code. (#83776) Thanks @TurboTheTurtle.</li>
<li>Telegram: keep forum topics from blocking sibling topic traffic by routing inbound serialization, media/text buffers, and account API queues on topic-aware lanes. (#83829)</li>
<li>Telegram: keep queued forum-topic follow-up messages from inheriting superseded source abort signals, so later same-topic user turns can still run and reply after an active turn is replaced. (#83827) Thanks @VACInc.</li>
<li>CLI/update: bypass npm freshness filters consistently during managed package and plugin installs so freshly published release plugins remain installable. Thanks @jalehman.</li>
<li>CLI/update: guide root-owned npm install EACCES recovery by stopping the managed Gateway before manual package replacement, then reinstalling and restarting the service. Fixes #83747. (#83757) Thanks @brokemac79.</li>
<li>Twitch: register refreshing chat tokens with Twurple's chat intent so automatic token refresh keeps chat access available. (#83750) Thanks @TurboTheTurtle.</li>
<li>Agents/subagents: keep collect-mode announce queues batching unresolved-origin items with compatible same-route messages and resume collection after a true cross-channel drain when a later compatible batch remains. Fixes #83577.</li>
<li>Skills: refresh existing session skill snapshots when watched skill roots change, so changed extra skill directories take effect without starting a new session. Fixes #83782. (#83800) Thanks @hclsys.</li>
<li>Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.</li>
<li>Providers/Anthropic: preserve Claude 4 image capability when configured model refs resolve through a stale local catalog row. (#83756) Thanks @TurboTheTurtle.</li>
<li>Providers/DeepSeek: normalize MCP tool schemas with <code>anyOf</code>/<code>oneOf</code> unions before normal and compaction requests reach DeepSeek, preventing union-shaped parameters from being rejected. (#83766) Thanks @TurboTheTurtle.</li>
<li>Control UI: render live tool progress from session-scoped <code>session.tool</code> Gateway events so externally started runs show their tool cards in the active session. (#83734) Thanks @TurboTheTurtle.</li>
<li>Outbound: resolve send-capable channel plugins from the active runtime registry when the pinned startup registry only has setup metadata. (#83733) Thanks @TurboTheTurtle.</li>
<li>Discord: preserve streamed reply previews when recovered tool-warning finals are delivered before or after the assistant's final reply. (#84169) Thanks @neeravmakwana.</li>
<li>Control UI: keep the chat delete confirmation popover clamped inside the visible viewport on small screens. (#83804) Thanks @ThiagoCAltoe.</li>
<li>Browser: enforce current-tab URL allowlist checks for <code>/act</code> evaluate/batch actions and <code>/highlight</code> routes while leaving tab-management actions unblocked. (#78523)</li>
<li>CI: require real-behavior-proof verdict markers to come from the ClawSweeper GitHub App before accepting exact-head proof. (#83692)</li>
<li>Models: show the effective OpenAI/Codex auth profile in <code>/models</code> provider headers instead of falling back to the OpenAI env-key label. (#83697) Thanks @yu-xin-c.</li>
<li>CLI: include active bundled loopback MCP tools in CLI system prompts and reset provider-side CLI sessions when that prompt-visible tool surface changes. (#83785) Thanks @TurboTheTurtle.</li>
<li>Browser: keep a profile <code>cdpPort</code> when its <code>cdpUrl</code> omits a port, while still letting explicitly written URL ports win. (#82166) Thanks @Marvae.</li>
<li>Agents/image generation: allow distinct <code>image_generate</code> prompts to start separate session-backed background tasks while same-prompt retries still return the active task status. (#83614) Thanks @Elarwei001.</li>
<li>Gateway/WebChat: honor configured <code>channels.webchat.textChunkLimit</code> and <code>chunkMode</code> overrides when chunking WebChat replies. (#83713)</li>
<li>Control UI: stop the chat reading indicator from sticking after an assistant response finishes. (#83515) Thanks @njuboy11.</li>
<li>Skills: reject empty or whitespace-only skill names and descriptions during quick validation. (#27061)</li>
<li>Sessions: skip trailing custom transcript entries when checking tail assistant replies so embedded CLI gap-fill does not duplicate canonical assistant output. (#83635) Thanks @yaoyi1222.</li>
<li>Memory Wiki: keep <code>wiki_lint</code> tool output path-safe by reporting vault-internal lint reports as relative paths in tool text and details while preserving absolute report paths for CLI/file callers. (#83439) Thanks @LLagoon3.</li>
<li>Telegram: keep verbose tool progress visible without mirroring non-final progress into active session transcripts, preventing embedded provider replies from aborting mid-run. (#83631) Thanks @kurplunkin.</li>
<li>Telegram: log successful outbound text and media deliveries with account, chat, message, operation, thread, reply, silent, and chunk metadata while keeping message bodies out of logs. Fixes #83196. (#83247) Thanks @jrwrest.</li>
<li>Cron: link isolated scheduled task runs to their stable cron session so task status and cleanup can follow the backing agent run. (#83606) Thanks @jai.</li>
<li>Codex app-server: mark Codex-native subagent task mirrors terminal when blocked or failed spawn-agent calls arrive with stale initializing child state, preventing task registry entries from staying running. Fixes #83852. (#83945) Thanks @joshavant.</li>
<li>CLI: enforce the documented Node.js 22.19 runtime floor in the source launcher.</li>
<li>Release stability: repair broad-gate regressions in requester-agent completion handoff, QA-Lab mock spawn attribution, Slack monitor test isolation, plugin uninstall peer fixtures, and Node-floor launcher contract coverage.</li>
<li>Agents/replies: persist queued follow-up user messages and assistant error stubs only once across model-fallback retries, preventing repeated provider rejections from corrupted same-role session transcripts. Fixes #83404. (#83417) Thanks @yetval.</li>
<li>Telegram: preserve reply-target context for bare mention replies on runtime-only turns so the model sees the replied-to message body. Fixes #83767. (#83953) Thanks @joshavant.</li>
<li>ClawHub: preserve configured base URL path prefixes when building API request URLs, so self-hosted ClawHub instances mounted under a subpath keep routing correctly. (#83982) Thanks @ThiagoCAltoe.</li>
<li>Slack: persist delivered inbound message IDs and fail closed when same-channel thread replies lose their thread context, preventing delayed duplicate replies and accidental channel-root posts. Fixes #83521. Thanks @shannon0430.</li>
<li>Codex app-server: complete OpenClaw dynamic tool diagnostics at the request boundary so successful, failed, timed out, aborted, and blocked tool calls do not leave active tool state behind. Fixes #83474. Thanks @rozmiarD.</li>
<li>Gateway/config: keep config writes from failing on unrelated unresolved auth-profile SecretRefs while preserving live auth-profile runtime snapshots.</li>
<li>Gateway/sessions: clear stored CLI provider resume bindings on non-subagent <code>/reset</code> so the next turn starts a fresh provider-side CLI conversation instead of resuming old context. (#83448) Thanks @jasonyliu.</li>
<li>Doctor: preserve legacy whole-agent Claude CLI intent by moving matching Anthropic model selections to model-scoped runtime policy before removing stale runtime pins. Fixes #83491. Thanks @danielcrick.</li>
<li>Discord/OpenAI: keep realtime Discord voice sessions hearing follow-up turns with OpenAI realtime and prebuffer assistant playback to avoid choppy starts. (#80505) Thanks @Solvely-Colin.</li>
<li>LM Studio: resolve env-template API keys like <code>${LMSTUDIO_API_KEY}</code> through the standard SecretInput path instead of sending the raw template as the bearer token, and preserve header-auth and discovery-key precedence when the template is unset. Fixes #80495. (#80568) Thanks @MonkeyLeeT.</li>
<li>Discord/subagents: route the initial reply from thread-bound delegated sessions into the bound Discord thread instead of the parent channel. Fixes #83170. (#83172) Thanks @100menotu001.</li>
<li>Gateway/sessions: rotate failed agent sessions when their transcript file is missing instead of wedging per-channel lanes. Fixes #83488. (#83553) Thanks @LLagoon3.</li>
<li>Agents: refresh final-delivery routing from fresh session state before declaring a no-send failure, keeping recovered runs on the normal durable delivery path. (#83835) Thanks @joshavant.</li>
<li>Agents: guard final-delivery fresh session routing against mismatched logical sessions before reusing recovered delivery context. (#83928) Thanks @joshavant.</li>
<li>Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.</li>
<li>Media: install Sharp with the root package and fall back to sips, Windows native imaging, ImageMagick, GraphicsMagick, or ffmpeg for image resizing/conversion when Sharp is unavailable. Fixes #83401. Thanks @scotthuang.</li>
<li>Telegram: deliver generated media completions back into forum topics by preserving topic IDs across requester-agent handoff. (#83556) Thanks @fuller-stack-dev.</li>
<li>Gateway: defer update-check startup until after readiness so package update checks no longer block sidecar-ready startup, while preserving update broadcasts and shutdown cleanup. (#83520) Thanks @samzong.</li>
<li>Telegram: keep <code>/btw</code> and read-only status commands from aborting active runs, and avoid retaining raw update payloads in timed-out spool tombstones. Refs #83272.</li>
<li>Agents: log strict-agentic execution contract diagnostics only when the planning-only retry path actually triggers.</li>
<li>Agents: stop embedded session takeover and session write-lock errors from consuming model fallbacks while preserving provider fallback metadata. Fixes #83510. Thanks @luyao618.</li>
<li>Agents/video: hide <code>video_generate</code> reference-audio parameters unless a registered video provider supports audio inputs.</li>
<li>Plugins: fall back to npm for official ClawHub updates when artifact downloads are unavailable, including beta-to-default fallback and dry-run version reporting.</li>
<li>Plugins/xAI: echo PKCE challenge fields during OAuth authorization-code token exchange for xAI token-endpoint compatibility. (#83499) Thanks @fuller-stack-dev.</li>
<li>Codex app-server: hydrate current inbound image attachments before queued runs so Responses-backed agents receive Discord and other channel images as native vision input. Fixes #83466. Thanks @iannwu.</li>
<li>Codex app-server: keep native code mode available without forcing code-mode-only so OpenClaw dynamic tool turns complete through the app-server tool bridge. Fixes #83109. Thanks @daswass.</li>
<li>Codex app-server: expose OpenClaw's sandbox-routed shell as <code>sandbox_exec</code>/<code>sandbox_process</code> for non-Docker sandbox backends so SSH sandbox agents keep a correctly routed shell path without shadowing Codex native shell. Fixes #80322. Thanks @keramblock.</li>
<li>Release stability: recover stale session diagnostics and Codex OAuth fallback state so stuck runs and reused refresh tokens clear without blocking follow-up work. (#83503) Thanks @100yenadmin.</li>
<li>Messages/TTS: apply TTS directives before message-tool sends reach core, gateway, or plugin delivery so opt-in message-tool rooms and proactive sends attach voice notes instead of leaking raw tags. Fixes #81598. Thanks @CG-Intelligence-Agent-Jack and @CoronovirusG10.</li>
<li>Messages/Codex: keep Codex direct/source chats on message-tool visible delivery by default while documenting and testing <code>messages.visibleReplies: "automatic"</code> as the old-mode opt-out; channel wildcard model overrides now apply to direct chats before harness delivery defaults.</li>
<li>Memory/QMD: keep archived session transcript hits visible after QMD export while preserving normal <code>.md</code> session ids that only resemble archive names. (#83518; fixes #83506) Thanks @tanshanshan.</li>
<li>Codex app-server: preserve network access for sandboxed Codex code-mode turns when the OpenClaw sandbox allows outbound egress. Fixes #83347. Thanks @YusukeIt0.</li>
<li>Codex app-server: honor writable Docker bind mounts for sandboxed workspace-write turns while disabling native Code Mode when container-path aliases or read-only bind shadows cannot be represented safely host-side. Fixes #83737. (#83849) Thanks @joshavant.</li>
<li>QA-Lab: keep the OTLP smoke decoder independent of removed OpenTelemetry generated-root internals.</li>
<li>Messages: default group/channel visible replies to automatic final delivery again, keeping <code>message_tool</code> opt-in for ambient/shared rooms and tool-reliable models.</li>
<li>CLI/TUI: force standalone <code>/exit</code> runs to terminate after <code>runTui</code> returns so onboarding-launched TUI children do not stay alive invisibly. (#83501) Thanks @fuller-stack-dev.</li>
<li>Agents/code mode: honor per-agent code-mode config in schema, runtime catalog activation, and model payload filtering. Fixes #83388. Thanks @Kaspre.</li>
<li>Agents/code mode: preserve agent, session, run, and channel context in <code>before_tool_call</code> hooks for top-level <code>exec</code>/<code>wait</code> dispatches. Fixes #83387.</li>
<li>QQBot: shorten C2C typing indicators to a 10-second window renewed every 5 seconds, capped to keep a final passive-reply slot available. (#83469)</li>
<li>Replies: keep final payload delivery after live preview updates so channels can finalize or send the completed answer instead of losing preview-only drafts. (#83468)</li>
<li>Discord: deliver final replies in progress-mode preview streams instead of deduplicating the final visible message. (#83443) Thanks @compoodment.</li>
<li>Providers/Xiaomi: replay MiMo Anthropic-compatible <code>reasoning_content</code> as provider-required thinking blocks even when OpenClaw thinking is disabled, fixing follow-up tool turns for <code>mimo-v2-flash</code>. Fixes #83407. Thanks @Xgenious7.</li>
<li>Agents/exec approvals: forward approval-runtime credentials on agent-owned Gateway approval calls so approved async commands complete through the existing runtime path instead of stalling on unauthenticated follow-up calls. Thanks @IWhatsskill, @Patrick-Erichsen, and @jesse-merhi.</li>
<li>Gateway/skills: preflight remote macOS skill-bin refreshes with a WebSocket connectivity check so stale node sessions skip quickly instead of logging slow <code>system.which</code> timeout warnings.</li>
<li>CLI/config: keep broken discovered plugins that are not referenced by active config from failing <code>openclaw config validate</code>, while preserving fatal errors for explicitly configured plugin entries.</li>
<li>GitHub Copilot: drop unsafe native Responses reasoning replay items with non-replayable IDs before dispatch, preventing affected Copilot sessions from failing with <code>invalid_request_body</code>. Fixes #83220. Thanks @galiniliev.</li>
<li>Agents/Codex: fail closed when an explicitly requested Codex harness is not registered instead of silently trying configured model fallbacks. Fixes #83349. Thanks @r2-vibes.</li>
<li>QA-Lab: make runtime tool coverage fail on missing required tool exercise instead of treating pass/pass parity envelope drift as missing coverage.</li>
<li>Core/plugins: harden clawpatch-reported edge cases across gateway auth cleanup, Claude session id paths, plugin activation policy, apply-patch hunk handling, diagnostic redaction, and plugin metadata validation.</li>
<li>UI: show reasoning choices as plain labels instead of leaking internal override wording in session and chat pickers.</li>
<li>Mac app: avoid repeating the Configuration heading inside channel quick settings.</li>
<li>Mac app: keep the Settings sidebar always visible and remove the redundant titlebar hide/show control.</li>
<li>Mac app: normalize Settings pane content margins so pages share the same left and right rail.</li>
<li>Mac app: prefer explicit private/Tailscale/LAN Gateway endpoints over SSH tunnels, preserve legacy loopback tunnel configs, persist transport choices, and show captured SSH stderr when tunneling really fails.</li>
<li>Gateway/sessions: keep ACP/acpx and runtime child sessions visible in configured-only session lists when their owner or parent session belongs to a configured agent.</li>
<li>Mac app: keep app-level menu commands and Dashboard failure states reachable when the remote Gateway is disconnected.</li>
<li>Mac app: allow longer Gateway and Context errors to wrap in the menu instead of truncating the useful failure detail.</li>
<li>Mac app: tighten remote Gateway fields in Settings so the Connection pane keeps readable labels and full action button text.</li>
<li>Mac app: keep custom Settings card rows left-aligned and full-width so Discovery and status sections no longer appear centered or detached.</li>
<li>Mac app: align Location permission controls to the same trailing column as the rest of Settings.</li>
<li>Mac app: add Dashboard, Chat, Canvas, and Settings shortcuts to the Dock icon menu.</li>
<li>Mac app: replace the Settings window's native split-view sidebar with an explicit layout so page content keeps its leading gutter when the sidebar is shown or hidden.</li>
<li>Mac app: render channel quick config as aligned Settings rows and hide schema-only variants that cannot be edited safely from the quick pane.</li>
<li>Gateway/webchat: hide internal runtime-context and other <code>display: false</code> transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.</li>
<li>CLI/help: keep <code>gateway</code>, <code>doctor</code>, <code>status</code>, and <code>health</code> help registration out of action/runtime imports so subcommand <code>--help</code> stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.</li>
<li>CLI/help: show plugin-owned command help based on the active memory slot so LanceDB memory users see <code>ltm</code> instead of unavailable <code>memory</code> commands. Fixes #83745. (#83841) Thanks @joshavant.</li>
<li>Cron/Discord: keep explicit announce runs in message-tool-only source-reply mode so scheduled agent turns post once instead of also echoing through automatic visible replies. Fixes #83261. Thanks @Theralley.</li>
<li>Telegram: preserve forum-topic origin targets in inbound, audio-preflight, and skipped-message hook contexts so follow-up delivery stays bound to the originating topic. Fixes #83302. Thanks @M00zyx.</li>
<li>Telegram: retry HTTP 421 Misdirected Request send failures on a fresh fallback transport so transient edge-node routing errors no longer drop outbound replies. Fixes #48892. (#48908) Thanks @MarsDoge.</li>
<li>Telegram: fail topic sends closed when Telegram reports <code>message thread not found</code> instead of retrying without <code>message_thread_id</code> into the base chat. Refs #83302.</li>
<li>Config/subagents: remove ignored agent-model <code>timeoutMs</code> keys, keep subagent model config to primary/fallback selection, and clean shipped stale config through doctor. Fixes #83291. Thanks @giodl73-repo.</li>
<li>Mac app: align the Sessions settings pane with the standard Settings page gutter and row spacing.</li>
<li>OpenAI/Codex: stop rejecting available <code>openai-codex</code> GPT-5.1, GPT-5.2, and GPT-5.3 model refs during config validation, while keeping removed Spark aliases suppressed. Fixes #83303.</li>
<li>Plugins/xAI: complete OAuth-backed xAI login and sidecar auth fixes, including guarded loopback callback CORS handling, video generation polling/defaults, and native-host User-Agent attribution. (#83322) Thanks @Jaaneek.</li>
<li>Codex app-server: preserve streamed native command output in mirrored transcripts and trajectory exports when final snapshots omit aggregated output. (#83200) Thanks @rozmiarD.</li>
<li>Codex app-server: fail closed when chat or sender policy denies tools, disabling native code, app, environment, and user MCP surfaces for restricted turns. (#82374) Thanks @VACInc.</li>
<li>Codex app-server: keep recent context-engine messages when oversized projected history is truncated, so short follow-ups in long channel sessions do not fall back to stale earlier turns. (#83127) Thanks @VACInc.</li>
<li>Codex app-server: keep OpenClaw session spawning searchable while steering Codex-native delegation through native subagents, avoiding duplicate direct subagent surfaces. (#83329) Thanks @fuller-stack-dev.</li>
<li>Codex app-server: recover stale childless Codex-native subagent task mirrors during maintenance and allow their registry rows to be cancelled without an OpenClaw child session. (#82836) Thanks @yshimadahrs-ship-it and @joshavant.</li>
<li>Feishu: return bound subagent delivery origins from session thread setup so Feishu subagent completions route back to the same DM or topic. (#83190) Thanks @100menotu001.</li>
<li>CLI/update: tailor post-update Gateway recovery hints by platform, showing systemd, LaunchAgent, Scheduled Task, or generic service-manager guidance instead of macOS-only recovery text. (#83096) Thanks @rubencu.</li>
<li>Plugins: apply a default 15-second timeout to legacy <code>before_agent_start</code> hooks so hung plugin handlers no longer block agent startup. Fixes #48534. (#83136) Thanks @therahul-yo.</li>
<li>Feishu: refresh inbound session delivery context for DM, group, and broadcast turns so later replies do not inherit stale WebChat routing. Fixes #78274.</li>
<li>Agents/subagents: require the initial subagent registry save before reporting spawn accepted, returning a spawn error instead of losing an untracked run when the registry write fails. (#83146) Thanks @yetval.</li>
<li>QA-Lab/qa-channel: attach redacted agent tool-start traces to outbound <code>QaBusMessage</code> records so scenarios can assert actual tool use instead of relying only on reply text. Fixes #67637. Thanks @100yenadmin.</li>
<li>QA-Lab: fail live runtime parity reports when assistant-message usage is missing, preventing <code>0 vs 0</code> live token rows from being reported as passing proof. Fixes #80411. Thanks @100yenadmin.</li>
<li>QA-Lab: add a runtime token-efficiency sidecar report that classifies Codex savings separately from regressions and fails only positive Codex-over-Pi live token deltas above threshold. Fixes #81093. Thanks @100yenadmin.</li>
<li>QA-Lab: fail Codex-backed OpenAI live runtime-pair runs before launching isolated workers when no portable Codex auth is available, while staging API-key fallbacks and configured Codex keys for isolated QA agents. Fixes #80412. Thanks @100yenadmin.</li>
<li>QA-Lab: refresh parity gates, mock frontier fixtures, model scenarios, and workflow artifact lanes to compare GPT-5.5 against Claude Opus 4.7. Fixes #74262. Thanks @100yenadmin.</li>
<li>QA-Lab: make mock parity dispatch provider-aware for source discovery and subagent scenarios so OpenAI and Anthropic lanes no longer share identical canned plans. Fixes #64879. Thanks @100yenadmin.</li>
<li>QA-Lab: stop returning Control UI bearer tokens from unauthenticated bootstrap payloads and bind Docker harness ports to loopback-only host addresses. (#66355) Thanks @pgondhi987.</li>
<li>Mac app: avoid a SwiftUI metadata crash when rendering the Cron Jobs settings pane.</li>
<li>Agents/subagents: preserve run-mode keep subagent registry entries past the session sweep TTL, so kept subagent runs remain visible after cleanup completes. Fixes #83132. (#83168) Thanks @yetval.</li>
<li>Agents/OpenAI streams: yield via <code>setTimeout(0)</code> instead of <code>setImmediate</code> between bursty Responses chunks so abort timers can fire during the yield, keeping cancel-on-timeout responsive on hot streams. Refs #82462.</li>
<li>Agents/Codex: keep legacy <code>oauthRef</code>-backed OAuth profiles usable while <code>openclaw doctor --fix</code> migrates them back to inline credentials, without creating new sidecar credentials. (#83312) Thanks @joshavant.</li>
<li>Agents/Codex: load the selected provider owner alongside the Codex harness runtime so <code>openai-codex</code> models resolve when plugin allowlists scope runtime loading. Fixes #83380. (#83519) Thanks @joshavant.</li>
<li>Telegram: fail stalled isolated-ingress handlers into tombstones and abort same-lane reply work before restarting, so later same-chat updates drain after a hung turn. Fixes #83272. (#83505) Thanks @joshavant.</li>
<li>CLI/config: send SecretRef diagnostics to stderr so JSON command stdout remains parseable.</li>
<li>CLI/doctor: seed Control UI allowed origins when migrating legacy non-loopback gateway bind host aliases like <code>0.0.0.0</code>. Fixes #83286. Thanks @giodl73-repo.</li>
<li>CLI/plugins: ship the bundled memory CLI as a package entry so package-installed <code>openclaw memory</code> commands register correctly.</li>
<li>CLI/update: defer doctor-time plugin package installs during package swaps and seed post-core repair from the updated install registry, preventing duplicate reinstall failures.</li>
<li>CLI/update: preserve old-parent-readable config metadata during legacy package handoffs, fall back only to official <code>@openclaw/*</code> npm plugin packages when ClawHub plugin artifacts are unavailable, and keep managed service package roots authoritative during updates.</li>
<li>Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.</li>
<li>Gateway/restart: keep ordinary unmanaged SIGUSR1/config restarts in-process instead of detach-spawning an orphaned child, preserving custom supervisor PID tracking while leaving update restarts on the fresh-process path. Fixes #65668.</li>
<li>CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.</li>
<li>Telegram: keep isolated long polling below the hard <code>getUpdates</code> request guard so idle bot accounts with high <code>timeoutSeconds</code> do not false-disconnect and restart-loop. Fixes #83264. Thanks @riccodecarvalho.</li>
<li>Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing <code>thought_signature</code> 400s. Fixes #72879. (#80358) Thanks @abnershang.</li>
<li>Telegram: skip transcript-only delivery mirrors and gateway-injected rows when resolving latest assistant text, preventing retained previews from replacing final replies with stale fragments. Fixes #83159. (#83362) Thanks @joshavant.</li>
<li>Memory/QMD: keep lexical search on raw hyphenated queries while normalizing semantic QMD sub-searches, avoiding fallback to the builtin index for dashed identifiers and dates. Fixes #81328.</li>
<li>Memory-core: distinguish sqlite-vec load failures from missing semantic vector embeddings in degraded <code>memory index</code> warnings, so vector recall diagnostics point at unresolved dimensions instead of blaming sqlite-vec when the store is ready. Fixes #75624. (#83056) Thanks @xuruiray and @Noah3521.</li>
<li>Agents/subagents: preserve sandbox-peer controller ownership while routing completion announcements back to the originating run session, keeping subagent control and completion delivery scoped correctly. Fixes #80201. (#80242) Thanks @Jerry-Xin.</li>
<li>Gateway: continue restarting remaining channels when one hot-reload channel restart fails, while still reporting aggregate reload failure and rolling back plugin pre-replace stops. Fixes #83054. Thanks @zqchris.</li>
<li>Gateway/plugins: bind admin HTTP RPC dispatch to the accepting gateway instance so multi-gateway processes cannot execute plugin HTTP control-plane calls against another live gateway. Fixes #83486. (#83487) Thanks @coygeek.</li>
<li>Telegram: keep hot-reload restarts from marking polling accounts manually stopped and restart isolated ingress cleanly after worker shutdown, preserving Telegram replies across config reloads. Fixes #83008. (#83410) Thanks @joshavant.</li>
<li>Telegram/Ollama: pass current Telegram image attachments into native PI/Ollama vision turns so live photo prompts reach Ollama as native images. Fixes #83023. (#83516) Thanks @joshavant.</li>
<li>Gateway/secrets: split the lightweight secrets runtime state and auth-store cache from the full secrets runtime and take a startup fast path when the gateway startup config has no SecretRef values, speeding up secrets startup while preserving cleanup and refresh semantics.</li>
<li>Codex app-server: rotate oversized native Codex threads before resume and cap dynamic tool-result text entering native Codex sessions, preventing stale oversized context from surviving OpenClaw compaction. (#82981) Thanks @hansolo949.</li>
<li>Gateway/restart: drain pending replies and active chat runs during restart shutdown before sockets and channels close, aborting timed-out chat runs through the normal cleanup path. (#69121) Thanks @alexlomt.</li>
<li>Agents/Codex: use the Codex runtime context window for OpenAI-model preflight compaction and memory flush checks, so GPT-5.5 Codex sessions compact before hitting the smaller native context limit. Fixes #82982. Thanks @vliuyt.</li>
<li>QA-Lab: clean orphaned gateway temp roots when a suite parent exits and wait on gateway plus transport readiness after config restarts, reducing stale <code>qa-channel</code> noise from interrupted runs. Fixes #65506. Thanks @100yenadmin.</li>
<li>QA-Lab: wake qa-bus long polls that arrive with stale future cursors after a bus restart, preserving reconnect readiness for harness clients. (#67142) Thanks @hxy91819.</li>
<li>QA-Lab: stage Multipass transfer scripts under OpenClaw's preferred temp root instead of raw OS temp paths, keeping the VM runner inside temp-path guardrails. (#64098) Thanks @ImLukeF.</li>
<li>Agents/replies: keep surviving reply media and append a warning when other media references fail, so partial media normalization no longer drops failures silently. Thanks @Jerry-Xin.</li>
<li>Config/models: accept <code>thinkingFormat: "together"</code> in model compat config so Together routes can opt into the Together-specific thinking response shape.</li>
<li>Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.7.1, bringing Codex hook approval compatibility, pre-tool command wrapping fixes, and Rolldown/Vitest output compaction improvements into the OpenClaw plugin.</li>
<li>Agents/OpenAI: stop post-processing GPT-5 final replies with hardcoded brevity caps, preserving full channel responses instead of appending synthetic ellipses, and log when strict-agentic GPT-5 execution activates. Fixes #82910.</li>
<li>Mac app: refine the Settings General and Connection panes with cleaner status panels, card rows, and a single native titlebar sidebar toggle.</li>
<li>Agents/media: deliver failed async image, music, and video generation completions directly when requester-session completion handoff fails, so channel users see provider errors instead of silent fallback stalls.</li>
<li>Browser/CDP: keep loopback proxy bypass active across both <code>NO_PROXY</code> casings and redact home-relative Chrome MCP profile paths in attach-failure diagnostics.</li>
<li>Agents/music: steer song, jingle, beat, anthem, and instrumental requests toward <code>music_generate</code> audio creation instead of lyric-only replies, and reserve <code>lyrics</code> for exact sung words.</li>
<li>Codex app-server: record native Codex tool calls and results into trajectory artifacts so debug/trajectory exports capture the full Codex-native tool history, not just OpenClaw-bridged turns. Thanks @vyctorbrzezowski.</li>
<li>Codex/app-server: keep bound conversation sessions on the owning agent runtime so native Codex control and follow-up turns do not fall back to the default agent client. Fixes #82954. (#82993)</li>
<li>CLI/infer: run gateway model probes in fresh explicit sessions so one-shot provider checks do not inherit default agent transcript state. (#82861) Thanks @Kaspre.</li>
<li>Providers/Together: send video-generation requests to Together's v2 video API even when shared text-model config still points at the v1 base URL. (#82992)</li>
<li>Browser CLI: preserve browser-level options on nested commands, skip option values during lazy command registration, and keep long-running wait/download/dialog hooks open for their advertised wait window.</li>
<li>CLI/sessions: accept <code>openclaw sessions list</code> as an alias for <code>openclaw sessions</code>, matching other list-style commands. Fixes #81139. (#81163) Thanks @YB0y.</li>
<li>Channels/stream previews: widen compact progress draft lines and cut prose at word boundaries while preserving command/path suffixes, with <code>streaming.progress.maxLineChars</code> for channel-specific tuning.</li>
<li>CLI/plugins: have <code>openclaw plugins doctor</code> warn when a configured runtime needs a missing owner plugin, sharing the same install mapping as <code>openclaw doctor --fix</code>. Fixes #81326. (#81674) Thanks @Zavianx.</li>
<li>Agents/Codex: route OpenAI runs that resolve to <code>openai-codex</code> through the Codex provider and bootstrap OpenClaw's stored OAuth profile into the Codex harness when the harness owns transport, so <code>openai/*</code> model refs no longer fail with <code>No API key found for openai-codex</code> despite an existing Codex OAuth profile. (#82864) Thanks @ragesaq.</li>
<li>Agents/ACP: distinguish prompt-submitted and runtime-active child stalls from true interactive waits, including redacted proxy-env diagnostics for Codex ACP no-output runs. Fixes #44810.</li>
<li>Agents/memory: explain that memory-triggered compaction exposes only <code>read</code> and append-only <code>write</code> when configured core tools are unavailable in <code>tools.allow</code> warnings. Fixes #82941. Thanks @galiniliev.</li>
<li>Agents/OpenAI: preserve deterministic tool payload ordering for prompt-cache reuse across OpenAI Responses and chat completions calls. (#82940) Thanks @galiniliev.</li>
<li>ACP/Codex: honor terminal ACP turn results so failed Codex/acpx runs are not recorded as successful after only progress text. Fixes #79522. Thanks @dudaefj.</li>
<li>Telegram: warn when a media group drops photos that fail to download, including albums where every photo is skipped. Fixes #55216. (#82987) Thanks @eldar702.</li>
<li>Agents/diagnostics: treat repeated same-handle embedded-run cleanup as idempotent while preserving true replacement-handle mismatch diagnostics. Fixes #82959. (#82960) Thanks @galiniliev.</li>
<li>Agents/subagents: preserve high-priority <code>AGENTS.md</code> policy in bootstrap context when oversized files are trimmed, and warn agents to read the full policy file before relying on scoped rules. Fixes #82920. (#82921) Thanks @galiniliev.</li>
<li>Agents/skills: apply the full effective tool policy pipeline to inline <code>command-dispatch: tool</code> skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525)</li>
<li>Codex: avoid spawning native hook relay subprocesses for post-tool/finalize events with no registered hook handlers while preserving pre-tool safety and approval relays. Fixes #76552. (#78004) Thanks @evgyur.</li>
<li>Channel accounts: keep top-level default channel accounts visible when named accounts are added alongside default credential material, so mixed legacy/new account configs keep resolving <code>default</code> instead of silently dropping it.</li>
<li>Agents/CLI: reject empty successful CLI subprocess replies as <code>empty_response</code> and keep them out of shared auth-profile health, so blank Claude CLI results no longer become green no-payload turns. Fixes #83231. (#83421) Thanks @joshavant.</li>
<li>Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram <code>/verbose</code> stays visible when command events arrive only at completion.</li>
<li>Codex/Telegram: deliver Codex verbose tool summaries in direct message-tool-only turns while suppressing message-send and activity-log noise. (#83186) Thanks @kurplunkin.</li>
<li>Mac app: make Channels settings open faster by deferring config-schema work, avoiding startup channel probes, caching decoded channel status rows, and showing only compact quick settings instead of the full generated channel schema.</li>
<li>Control UI: include the Control UI and Gateway protocol versions in protocol-mismatch errors so stale app/dashboard pairings identify which side needs rebuilding or restarting.</li>
<li>Gateway/protocol: restore Gateway WS protocol v4 and keep <code>message.action</code> room-event metadata on the existing <code>inboundTurnKind</code> wire field while preserving internal inbound-event classification.</li>
<li>Agents/tools: prefer non-webchat session-key routes when the message tool has stale webchat context, so message-tool-only replies keep delivering to the originating channel. Fixes #82911. (#83004) Thanks @joshavant.</li>
<li>Channels: keep direct-message last-route writes on isolated <code>per-channel-peer</code> sessions instead of contaminating the agent main session with channel delivery context. Fixes #36614. Thanks @aspenas.</li>
<li>Mac app: move the Settings sidebar toggle into the native titlebar and tighten the General pane width.</li>
<li>Mac app: keep visited Settings panes mounted so switching tabs no longer blanks and reloads their content.</li>
<li>Mac app: make Config settings open from shallow schema lookups and load selected paths on demand instead of fetching and rendering the full generated config schema up front.</li>
<li>Codex: sanitize inline image payloads before Codex app-server and OpenAI Responses replay, and clear poisoned Codex thread bindings after invalid image errors. Fixes #82878.</li>
<li>Providers/GitHub Copilot: request identity-encoded Copilot API responses across token exchange, catalog, model calls, usage, and embeddings so compressed Business-account error payloads no longer reach JSON parsers as gzip bytes. Fixes #82871. Thanks @tonyfe01.</li>
<li>Telegram: redact nested raw-update identifiers and user metadata before verbose raw update logging, preserving useful update/message ids without exposing chat, user, command, or profile details. (#82945) Thanks @galiniliev and @joshavant.</li>
<li>Telegram: preserve replied-to bot messages, captions, and media metadata in group reply chains so follow-up replies understand what the user is reacting to. (#82863)</li>
<li>Providers/Together: update PI runtime packages to 0.74.1 and emit Together-style <code>reasoning.enabled</code>/<code>max_tokens</code> controls for reasoning-capable OpenAI-completions models.</li>
<li>Agents/diagnostics: split slow embedded-run <code>attempt-dispatch</code> startup summaries into workspace, prompt, runtime-plan, and final dispatch subspans so traces identify the delayed setup phase. Fixes #82782. (#82783) Thanks @galiniliev.</li>
<li>Agents/Codex: flatten nested tool-result middleware blocks into bounded text so successful message sends are no longer replaced with <code>Tool output unavailable due to post-processing error</code>. Fixes #82912. Thanks @joeykrug.</li>
<li>CLI/media: accept HTTP(S) URLs in <code>openclaw infer image describe --file</code>, fetching remote images through the guarded media path instead of treating URLs as local files. Fixes #82837. (#82854) Thanks @neeravmakwana.</li>
<li>Agents/subagents: keep session-backed parent runs active when the child wait call times out before the child session has actually settled, so late subagent completions are reconciled instead of being lost. Fixes #82787. Thanks @ramitrkar-hash.</li>
<li>Control UI: advertise shared Gateway protocol constants in browser connect frames, fixing protocol mismatch handshakes after protocol constant drift. Fixes #82882. Thanks @galiniliev.</li>
<li>Gateway: add rollback protocol-mismatch diagnostics, including client protocol ranges in Gateway logs and deep status/doctor hints for stale client processes. Fixes #82841. (#82908)</li>
<li>Agents/subagents: keep successful keep-mode completion payloads pending after final-delivery retry exhaustion, so requester recovery no longer loses final subagent results. Fixes #82583. (#82999) Thanks @joshavant.</li>
<li>Gateway/auth: allow same-host trusted-proxy callers to use the documented local direct <code>gateway.auth.password</code> fallback after revisiting the #78684 fail-closed policy, while keeping token fallback rejected and forwarded-header requests on the trusted-proxy path. Fixes #82607. (#82953) Thanks @joshavant.</li>
<li>Agents/subagents: wait for queued completion handoffs to reach the parent transcript before marking them announced, preventing busy parent runs from cleaning up before observing child results. Fixes #82913. (#83039) Thanks @joshavant.</li>
<li>Agents/subagents: route group/channel subagent completions through message-tool-only handoffs when required and keep active-requester wake failures from dropping completion delivery. Fixes #82803. Thanks @galiniliev, @yozakura-ava, and @moeedahmed.</li>
<li>Memory-core: scan persisted memory source sessions on startup, comparing on-disk transcripts against the index and marking only missing/newer/resized files dirty for incremental sync. Fixes #82341. (#82341) Thanks @giodl73-repo.</li>
<li>Telegram: keep the top-level default account in the account list when named accounts or bindings are added alongside top-level credentials, preserving default polling while still letting named-only configs resolve to a single account. Fixes #82794. (#82794) Thanks @giodl73-repo.</li>
<li>CLI/models: reuse command-scoped plugin metadata across model listing, provider catalog, auth, and synthetic-auth checks, restoring fast <code>openclaw models</code> runs for plugin-heavy installs. Fixes #82881. (#83033) Thanks @joshavant.</li>
<li>CLI/channels: show configured official external channels such as Discord in <code>openclaw channels list</code> when their plugin package is missing, including the install and doctor repair command instead of reporting no configured channels. Fixes #82813.</li>
<li>Signal: preserve mixed-case group IDs through routing and session persistence so group auto-replies keep delivering after updates. Fixes #82827.</li>
<li>Agents/tools: keep the <code>message</code> tool available in embedded runs when it is explicitly allowed through <code>tools.alsoAllow</code> or runtime tool allowlists, so channel plugins with custom reply delivery can still use configured message sends. Fixes #82833. Thanks @cn1313113.</li>
<li>WhatsApp: honor forced document delivery for outbound image, GIF, and video media so <code>forceDocument</code>/<code>asDocument</code> sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.</li>
<li>WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as <code>file.pdf</code> and <code>file.csv</code> instead of an extensionless <code>file</code>. Thanks @mcaxtr.</li>
<li>Process/diagnostics: report active lane blockers in lane wait warnings so <code>queueAhead=0</code> no longer hides commands waiting behind active work. Fixes #82791. (#82792) Thanks @galiniliev.</li>
<li>Process/diagnostics: stop counting the active processing turn as queued backlog in liveness warnings so transient max-only event-loop spikes do not surface as gateway warnings.</li>
<li>Agents/replies: classify provider conversation-state rejections and return a clear message-channel error instead of auto-resetting or falling back to a generic runner failure. (#82616) Thanks @dutifulbob.</li>
<li>Browser plugin: trust managed Chrome CDP diagnostics when launch HTTP probes race cold-start readiness, avoiding false startup failures. Fixes #82904. (#82986) Thanks @kmanan and @hclsys.</li>
<li>Android: prompt before replacing a changed Gateway TLS thumbprint, showing the old and new SHA-256 fingerprints so users can accept expected certificate rotations instead of hard failing on pin mismatch. (#83077) Thanks @sliekens.</li>
<li>CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.</li>
<li>Agents/failover: classify Moonshot/Kimi exhausted-balance HTTP 429 payloads as billing instead of generic rate limits, preserving billing guidance and fallback behavior. Fixes #43447. (#83079) Thanks @leno23.</li>
<li>Plugin SDK: bundle <code>openclaw/plugin-sdk/zod</code> into the published package artifact and verify the packed zod subpath stays self-contained, so pnpm global installs can register plugins without a package-local <code>zod</code> symlink. Fixes #78398. (#78515) Thanks @ggzeng.</li>
<li>Providers/Google: drop compaction-truncated Gemini thought signatures before replay so malformed Base64 no longer aborts the next assistant turn. (#82995) Thanks @wAngByg.</li>
<li>Gateway/mobile: allow paired iOS and Android clients to refresh same-family OS metadata on authenticated reconnect instead of requiring a new approval. (#83490) Thanks @ngutman.</li>
<li>WhatsApp: treat <code>upload-file</code> as a supported media send intent by lowering path/URL uploads through the channel's normal send-media transport. (#81883) Thanks @ngutman.</li>
<li>iOS: end Live Activities when OpenClaw is connected, idle, or disconnected, and show compact attention states for approval-required reconnects. (#83597) Thanks @ngutman.</li>
<li>Control UI: hide child nav items when collapsing the active sidebar group. Fixes #42167. (#42223) Thanks @Aroool.</li>
<li>CI/proof: skip the real-behavior-proof gate for private org maintainers by minting a least-privilege (<code>members: read</code>) GitHub App token and checking active membership in the <code>maintainer</code> team, instead of treating <code>author_association=CONTRIBUTOR</code> as definitively external. (#83418) Thanks @romneyda.</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.19/OpenClaw-2026.5.19.zip" length="54062201" type="application/octet-stream" sparkle:edSignature="7bVi6rv+TjhrUfi32V62BW2VgyV17jm7x+H6p10PRClCdXKZjhM7AX6MyvAz2+e7kzXIknj1Y9X7q43/E9fBBw=="/>
</item>
<item>
<title>2026.5.18</title>
<pubDate>Mon, 18 May 2026 22:41:13 +0000</pubDate>
@@ -218,526 +610,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.18/OpenClaw-2026.5.18.zip" length="53924201" type="application/octet-stream" sparkle:edSignature="cU0TfUmBZbVOpgwou+GS7RQiDhEGVUxjK+bwsl1RXiqvJi9ErsYebZIxVayH8++v5PeycoK5+LQF5gLiXQa2AA=="/>
</item>
<item>
<title>2026.5.12</title>
<pubDate>Fri, 15 May 2026 13:25:16 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026051290</sparkle:version>
<sparkle:shortVersionString>2026.5.12</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.12</h2>
<h3>Changes</h3>
<ul>
<li>Amazon Bedrock: externalize the Bedrock and Bedrock Mantle provider packages so core installs no longer pull AWS SDK dependencies unless those providers are installed.</li>
<li>Plugins: externalize Slack, OpenShell sandbox, and Anthropic Vertex so their runtime dependency cones install only when those plugins are installed.</li>
<li>Control UI/WebChat: add a persisted auto-scroll mode selector so users can keep the current near-bottom behavior, always follow streaming output, or turn automatic streaming scroll off and use the New messages button manually. Fixes #7648 and #81287. Thanks @BunsDev.</li>
<li>ACP: add <code>acp.fallbacks</code> so ACP turns can try configured backup runtime backends when the primary backend is unavailable before any output is emitted. (#69542) Thanks @kaseonedge.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Doctor/Codex: stop warning that the message tool is unavailable for source-reply paths where OpenClaw grants <code>message</code> at runtime, keeping update and doctor output aligned with the OpenAI happy path. Thanks @pashpashpash.</li>
<li>Channels/Weixin: bump the external Weixin catalog entry to <code>@tencent-weixin/openclaw-weixin@2.4.3</code> with the matching package integrity. (#81730) Thanks @scotthuang.</li>
<li>Agents/subagents: apply <code>agents.defaults.subagents.model</code> before target agent primary models during <code>sessions_spawn</code>, so model-scoped runtimes such as <code>claude-cli</code> stay attached to default child runs. Fixes #81395. (#81783) Thanks @joshavant.</li>
<li>Telegram: keep Bot API polling alive during main event-loop stalls by moving ingress to an isolated worker with a durable local spool. Fixes #81132. (#81746) Thanks @joshavant.</li>
<li>Telegram: preserve rendered HTML formatting through lazy cron announce delivery so Markdown links stay clickable instead of falling back to literal anchor tags. Fixes #81742. (#81758)</li>
<li>Telegram: skip unmentioned group media before download when <code>requireMention</code> is active, avoiding failed media-download replies for messages that should be ignored. Fixes #81181. (#81785) Thanks @joshavant.</li>
<li>CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help.</li>
<li>Gateway/session history: carry monotonic transcript message sequence through live updates and refresh SSE history when stale sequence input would otherwise append bad incremental state. (#81474) Thanks @samzong.</li>
<li>Security/sandbox: include Windows <code>USERPROFILE</code> in the sandbox blocked home roots so credential-bearing binds (such as <code>.codex</code>, <code>.openclaw</code>, or <code>.ssh</code> under the Windows user profile) are denied even when <code>HOME</code> points at a different shell home. (#63074) Thanks @luoyanglang.</li>
<li>Models config/auth: stop inferring provider env-var markers from broad <code>^[A-Z_][A-Z0-9_]*$</code> strings, and resolve config-backed provider <code>apiKey</code> values only through structured env SecretRefs (<code>secrets.providers[id]</code> / <code>secrets.defaults</code>), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom.</li>
<li>Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd.</li>
<li>CLI/onboarding: forward provider-specific auth flags (e.g. <code>--openai-api-key</code>) through the onboarding wizard so they reach provider auth methods via <code>ctx.opts</code>, letting <code>--openai-api-key "$OPENAI_API_KEY"</code> skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf.</li>
<li>CLI/migrate: drop trailing periods from Codex migrate item messages and <code>REASON_CODE_MESSAGES</code> strings so plan/result rows read as labels instead of sentence fragments. (#81705) Thanks @sjf.</li>
<li>Slack: treat malformed private-file redirect <code>Location</code> headers as unfollowable redirects instead of failing Slack media downloads.</li>
<li>Plugins: discover provider plugins from <code>setup.providers[].envVars</code> credentials during provider discovery while keeping the deprecated <code>providerAuthEnvVars</code> fallback. (#81542) Thanks @JARVIS-Glasses.</li>
<li>Docs/Codex harness: clarify that per-agent <code>CODEX_HOME</code> isolates <code>~/.codex</code> while inherited <code>HOME</code> intentionally keeps <code>.agents</code> discovery and subprocess user-home state available.</li>
<li>Auth: reclaim dead-owner stale file locks before retrying locked writes, so crashed OAuth refreshes no longer wedge <code>auth-profiles.json</code> until manual cleanup.</li>
<li>CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping <code>openclaw plugins list</code> descriptions readable.</li>
<li>Process execution: collapse case-insensitive duplicate child environment keys on Windows so caller-provided overrides such as <code>PATH</code> cannot be shadowed by host <code>Path</code>.</li>
<li>Gateway/diagnostics: suppress cold-start liveness warnings during the startup grace window while still sampling liveness metrics. Fixes #79915. (#81699) Thanks @joshavant.</li>
<li>Codex harness: keep <code>oauthRef</code>-backed Codex OAuth profiles usable and stop high-confidence app-server OAuth refresh invalidation from retry-spamming raw token-refresh errors without turning entitlement or usage-limit payloads into re-auth prompts.</li>
<li>Browser CLI: request the existing <code>operator.admin</code> gateway scope explicitly for browser control commands, avoiding unnecessary scope-upgrade approval loops. Fixes #81555. (#81716) Thanks @joshavant.</li>
<li>Gateway/diagnostics: suppress cold-start liveness warnings during the startup grace window while still sampling liveness metrics. Fixes #79915. (#81699) Thanks @joshavant.</li>
<li>Plugin SDK: restore the deprecated <code>openclaw/plugin-sdk/memory-core</code> package subpath as an alias of <code>memory-host-core</code>, so published memory companion plugins that still import it resolve on current hosts.</li>
<li>Control UI/i18n: use the installed workspace pi runtime for locale refreshes, update the fallback package pin, prefer the Anthropic CI provider when available, and skip invalid provider credentials instead of failing main.</li>
<li>Codex harness: classify native app-server token-refresh logout and relogin failures as authentication refresh errors, so users get re-authentication guidance instead of a raw runtime failure.</li>
<li>Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly.</li>
<li>Agents: preserve source-reply delivery metadata when merging tool-returned media into the final reply, keeping message-tool-only replies deliverable and mirrored. Thanks @pashpashpash and @vincentkoc.</li>
<li>Replies: treat rich presentation, interactive controls, and channel-native payload data as outbound content across follow-up, heartbeat, cron, ACP, and block-streaming delivery paths, preventing card/button-only replies from being dropped as empty.</li>
<li>WebChat/TUI: route Codex <code>tools.message</code> source replies to the active internal UI turn and mirror them to session history, so message-tool-only harness replies, including rich presentation and button-only replies, no longer disappear while WebChat and TUI remain non-targetable outbound channels. (#81586) Thanks @pashpashpash.</li>
<li>Replies: deliver rich-only block replies even when block-streaming coalescing is enabled, keeping card and button payloads from being dropped by the text coalescer. Thanks @pashpashpash.</li>
<li>macOS/companion: require system TLS trust before pinning a first-use direct <code>wss://</code> gateway certificate and honor <code>gateway.remote.tlsFingerprint</code> as the explicit pin for remote node-mode sessions, so fresh endpoints fail closed when macOS cannot trust the certificate unless configured out of band. Fixes #50642. Thanks @BunsDev.</li>
<li>Update: snapshot config before update-time repair and restart writes, preserve plugin install records through doctor cleanup, and keep update-time config size drops from blocking the update while pointing users to the pre-update backup. Fixes #80077. (#80257) Thanks @Jerry-Xin and @vincentkoc.</li>
<li>Sessions/status: classify ACP spawn-child sessions as <code>kind: "spawn-child"</code> instead of <code>"direct"</code> in <code>openclaw sessions</code> and status output; extract the duplicated session-kind classifier into a shared helper (<code>src/sessions/classify-session-kind.ts</code>) so both surfaces stay in sync. Fixes catalog #19. (#79544)</li>
<li>Sessions/Gateway: report <code>agentRuntime.id: "acpx"</code> (or stored backend id) with <code>source: "session-key"</code> for ACP control-plane session rows in <code>openclaw sessions --json</code>, <code>openclaw status</code>, and Gateway session RPC responses instead of the incorrect <code>"auto"</code> / <code>"pi"</code> implicit fallback. Fixes catalog #18. (#79550)</li>
<li>Telegram: delete tool-progress-only draft bubbles before rotating to the real answer, preventing orphaned progress messages in streamed replies.</li>
<li>Codex app-server: keep per-agent <code>CODEX_HOME</code> isolation without rewriting <code>HOME</code> by default, so Codex-run subprocesses can still find normal user-home config, tokens, and CLI state unless the launch explicitly overrides <code>HOME</code>. Thanks @pashpashpash.</li>
<li>iMessage: stop sending visible <code><media:image></code> placeholder text for media-only native image sends while preserving the internal echo key that prevents self-echo duplicate replies. (#81209) Thanks @homer-byte.</li>
<li>Agents/sessions: create configured agent main sessions before first <code>sessions_send</code> or gateway send, so agent-to-agent messages no longer fail when the target agent has not started yet.</li>
<li>gateway: pass Talk session scope to resolver [AI]. (#81379) Thanks @pgondhi987.</li>
<li>Gateway protocol: require v4 clients and stream explicit chat <code>deltaText</code>/<code>replace</code> frames so SDK clients can consume assistant updates without local diffing. (#80725) Thanks @samzong.</li>
<li>GitHub Copilot: exchange OAuth tokens for Copilot API tokens on image understanding requests and route Gemini image payloads through Chat Completions, fixing Copilot Gemini image descriptions. (#80393, #80442) Thanks @afunnyhy.</li>
<li>Gateway: hide pending Node pairing commands, capabilities, and permissions until approval, and refresh the live approved surface when pairings change. (#80741) Thanks @samzong.</li>
<li>Plugins/Feishu/WhatsApp/Line: enforce inbound media size caps while reading download streams, avoiding full buffering of oversized attachments. (#81044, #81050) Thanks @samzong.</li>
<li>Plugins/install: limit install-time code safety scans to plugin-owned runtime entrypoints while keeping dependency manifest denylist checks, so trusted packages with large dependency trees no longer get blocked or warned on third-party runtime internals.</li>
<li>Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601)</li>
<li>Installer: honor <code>--no-git-update</code> for existing git checkouts before resolving release refs, preventing pinned source installs from moving during reinstall.</li>
<li>Plugins/install: refresh OpenClaw-managed peer dependency pins when installed plugin peer ranges change, while preserving user-owned dependency pins.</li>
<li>Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987.</li>
<li>Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd.</li>
<li>Plugins/memory: prefer the npm-installed memory-lancedb plugin over the bundled fallback during duplicate resolution, keeping Active Memory's <code>memory_recall</code> tool visible after managed installs. Fixes #81193. Thanks @julio-arcila.</li>
<li>Plugins/uninstall: prune managed third-party peer dependencies after their owning npm plugin is removed, without blocking plugin cleanup on peer-prune failures.</li>
<li>Docker: pin setup-time container paths so stale host <code>.env</code> OpenClaw paths cannot leak into Linux containers. Fixes #80381. (#81105) Thanks @brokemac79.</li>
<li>Channels/WeCom: refresh the official onboarding install to <code>@wecom/wecom-openclaw-plugin@2026.5.7</code> and update existing managed npm installs instead of failing on the package directory. Fixes #79884. (#80390) Thanks @brokemac79.</li>
<li>Anthropic: reseed Claude CLI fresh-session retries from bounded OpenClaw transcript history after session rotation, preventing conversation amnesia. Fixes #80905. (#80934) Thanks @bitloi.</li>
<li>Require explicit browser device pairing [AI]. (#81289) Thanks @pgondhi987.</li>
<li>Require Control UI pairing before proxy-scoped access [AI]. (#81288) Thanks @pgondhi987.</li>
<li>Installer: honor <code>--version</code> for git installs and install from the checked-in lockfile, preventing recent dependency pins from tripping pnpm's minimum-release-age gate during tag installs.</li>
<li>Agents: deliver same-process subagent completion handoffs through the in-process agent dispatcher instead of opening a Gateway RPC loopback.</li>
<li>Harden trusted-proxy source validation [AI]. (#81290) Thanks @pgondhi987.</li>
<li>Agents: add permissive item schemas to array tool parameters before provider submission, preventing OpenAI-compatible schema validation from rejecting plugin tools that omit <code>items</code>. Fixes #81175. (#81217) Thanks @JARVIS-Glasses.</li>
<li>Agents: escalate LLM idle watchdog timeouts through profile rotation and configured model fallback instead of leaving agent turns stuck after a silent model stream. Fixes #76877. (#80449) Thanks @jimdawdy-hub.</li>
<li>Discord voice: treat OpenAI Realtime startup auth failures as fatal, suppress duplicate realtime error logs, and stop autoJoin from retrying the same broken voice channel until credentials are fixed.</li>
<li>ACPX: stop forwarding unsupported timeout config options to Claude ACP while preserving OpenClaw's own turn timeout. (#80812) Thanks @sxxtony.</li>
<li>Session transcripts: redact sensitive message content in the centralized JSONL append path so CLI turns, gateway transcript injection, transcript mirrors, and guarded tool results use the same configured redaction behavior. Fixes #73565. Refs #73563. (#79645) Thanks @Ziy1-Tan.</li>
<li>Channels/iMessage: ignore Apple link-preview plugin payload attachments when users paste URLs, keeping the URL text while avoiding phantom media context. (#79374) Thanks @homer-byte.</li>
<li>Telegram: detect polling stalls from <code>getUpdates</code> liveness only, so outbound API calls no longer mask dead inbound polling; log polling-cycle starts after transport rebuilds. Fixes #78473.</li>
<li>fix: scan plugin runtime entries during install [AI]. (#80998) Thanks @pgondhi987.</li>
<li>fix(plugins): scan installed dependency runtime code [AI]. (#81066) Thanks @pgondhi987.</li>
<li>Inherit tool restrictions for delegated sessions [AI]. (#80979) Thanks @pgondhi987.</li>
<li>Telegram: discard legacy long-poll update offsets that cannot be tied to the current bot token, so token rotation no longer leaves bots silently skipping new messages. (#80671) Thanks @sxxtony.</li>
<li>browser: enforce navigation checks for act interactions [AI]. (#81070) Thanks @pgondhi987.</li>
<li>Validate node exec event provenance [AI]. (#81071) Thanks @pgondhi987.</li>
<li>Gateway: keep active reply runs visible to stuck-session diagnostics and clear no-active-work recovery state, preventing stale queued lanes after compaction or tool failures. Fixes #80677. (#81302)</li>
<li>Codex app-server: rotate incompatible context-engine-managed native threads so Lossless-managed sessions do not resume stale hidden Codex history. (#81223) Thanks @jalehman.</li>
<li>Codex cron: execute scheduled command-style automation payloads before workspace bootstrap or memory review, preserving existing isolated cron jobs after Codex harness migration. (#81510) Thanks @jalehman.</li>
<li>Plugin LLM completions: honor Codex agent-runtime policy for canonical OpenAI model refs, so context-engine summarizers can use Codex OAuth instead of requiring direct <code>OPENAI_API_KEY</code> auth. (#81511) Thanks @jalehman.</li>
<li>Gateway/OpenAI HTTP: return OpenAI-compatible 400 errors for invalid sampling params and provider validation failures instead of collapsing them to 500s. (#81275) Thanks @Lellansin.</li>
<li>Telegram: publish plugin and skill command description localizations to native command menus while filtering unsupported locale codes and preserving Telegram command limits. (#81351) Thanks @jzakirov.</li>
<li>Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.</li>
<li>Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987.</li>
<li>Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987.</li>
<li>Update: suppress the false newer-config warning during restart health probing after an update handoff, while keeping future-version mutation guards intact. (#78652)</li>
<li>Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma.</li>
<li>Codex runtime: allow the official installed <code>@openclaw/codex</code> package to use its private task-runtime and MCP projection SDK helpers, fixing <code>MODULE_NOT_FOUND</code> during migrated OpenAI/Codex beta runs.</li>
<li>Codex migration: make Enter activate the highlighted checkbox row before continuing, so <code>Skip for now</code> and bulk-selection rows work even when planned items start preselected.</li>
<li>Codex harness: keep auth-profile-backed media tools such as <code>image_generate</code> available when OpenAI auth lives in the agent's auth-profile store instead of environment variables.</li>
<li>WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.</li>
<li>Require auth for sandbox browser CDP relay [AI]. (#81002) Thanks @pgondhi987.</li>
<li>fix: detect carried exec command forms [AI]. (#81000) Thanks @pgondhi987.</li>
<li>Reject truncated exec approval commands [AI]. (#81001) Thanks @pgondhi987.</li>
<li>Enforce inline shell wrapper payload matching [AI]. (#80978) Thanks @pgondhi987.</li>
<li>fix(node-pairing): replace changed pending requests [AI]. (#80894) Thanks @pgondhi987.</li>
<li>Rate limit Google Chat webhook requests [AI]. (#80974) Thanks @pgondhi987.</li>
<li>Docker: mount the auth-profile secret key directory so OAuth-backed auth profiles survive container rebuilds. (#80991)</li>
<li>Onboarding: accept Codex auth profiles for canonical OpenAI model checks, avoiding false missing-auth warnings. (#80913) Thanks @rubencu.</li>
<li>fix(feishu): normalize webhook rate-limit client keys [AI]. (#80975) Thanks @pgondhi987.</li>
<li>fix(auth): prevent bootstrap pairing scope changes [AI]. (#80976) Thanks @pgondhi987.</li>
<li>Validate Control UI loopback retry endpoints [AI]. (#80900) Thanks @pgondhi987.</li>
<li>Harden exported markdown link rendering [AI]. (#80902) Thanks @pgondhi987.</li>
<li>fix(gateway): honor minimal discovery mode for wide-area DNS-SD [AI]. (#80903) Thanks @pgondhi987.</li>
<li>slack: enforce reaction notification policy [AI]. (#80907) Thanks @pgondhi987.</li>
<li>Enforce gateway command scopes by caller context [AI]. (#80891) Thanks @pgondhi987.</li>
<li>Telegram/groups: in single-account setups, treat an explicit empty <code>accounts.<id>.groups: {}</code> map the same as undefined so the root <code>channels.telegram.groups</code> allowlist still applies, instead of silently dropping every group update under the default <code>groupPolicy: "allowlist"</code>. Multi-account semantics are unchanged so per-account explicit-empty groups still scope-disable a single account without affecting siblings; the explicit way to block all groups for any account remains <code>groupPolicy: "disabled"</code>. Fixes #79427. (#81030) Thanks @kinjitakabe.</li>
<li>Codex (app-server): project user-configured <code>mcp.servers</code> into new Codex thread configs, matching the codex-cli runtime's existing <code>-c mcp_servers=...</code> behavior so app-server-runtime agents see the same user MCP servers the CLI runtime already exposes. Plugin-curated apps remain attached via the separate <code>apps</code> config patch. Fixes #80814. Thanks @kinjitakabe.</li>
<li>Enforce Slack plugin approval button authorization [AI]. (#80899) Thanks @pgondhi987.</li>
<li>Recognize PowerShell -ec inline commands [AI]. (#80893) Thanks @pgondhi987.</li>
<li>fix(qqbot): authorize approval button callbacks [AI]. (#80892) Thanks @pgondhi987.</li>
<li>Telegram: render supported HTML tags in streamed and durable replies instead of showing literal markup. (#80977)</li>
<li>Scrub streamable MCP redirect headers [AI]. (#80906) Thanks @pgondhi987.</li>
<li>fix(memory-wiki): require admin scope for ingest [AI]. (#80897) Thanks @pgondhi987.</li>
<li>memory-wiki: require write scope for Obsidian search [AI]. (#80904) Thanks @pgondhi987.</li>
<li>WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.</li>
<li>WhatsApp: externalize the channel as a ClawHub/npm plugin outside the core npm runtime bundle, and bump Baileys to <code>7.0.0-rc11</code> so libsignal resolves from the registry instead of a GitHub tarball.</li>
<li>WhatsApp: keep optional audio decoding dependencies local to the external plugin so the core npm install no longer pulls WhatsApp-only media helpers.</li>
<li>Build: skip copied metadata for bundled plugins that are excluded from build entries, preventing update/status rebuilds from advertising missing QQ Bot runtime files. (#80925)</li>
<li>Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual <code>└─ </code> prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.</li>
<li>Auto-reply: surface a visible error when the configured model backend fails and fallback produces no visible reply, while preserving intentional silent turns and side-effect-only deliveries. (#80917) Thanks @dutifulbob.</li>
<li>Agents/exec: skip redundant heartbeat wake-ups for subagent session exec completions, preventing spurious LLM invocations on parent sessions. Fixes #66748. (#66749) Thanks @ggzeng.</li>
<li>Provider streams: keep OpenAI-compatible SSE and JSON fallback streams draining across split chunks and fail Azure Responses streams with a bounded first-event diagnostic instead of stalling. Refs #80926. (#80927) Thanks @galiniliev and @CaptainTimon.</li>
<li>Agents: rewrite generic provider internal errors with support request IDs into user-friendly transient error copy. (#49401) Thanks @y471823206.</li>
<li>WhatsApp: finish handling pending debounced inbound messages before closing the socket. (#81246) Thanks @mcaxtr.</li>
<li>CLI/commitments: write <code>--json</code> output to stdout instead of diagnostic logs so automation can parse commitment list and dismiss results. (#81215) Thanks @giodl73-repo.</li>
<li>Update: allow pnpm GitHub-source OpenClaw updates to approve the OpenClaw package build, so source installs complete their prepare/prepack lifecycle. (#81294) Thanks @fuller-stack-dev.</li>
<li>Telegram: preserve supported HTML tags in visible replies and durable mirrors so formatted messages render correctly instead of degrading to escaped text. (#80977) Thanks @obviyus.</li>
<li>Plugins/runtime: attribute deprecated runtime config load/write warnings to the plugin id and source that triggered them so logs and plugin doctor runs are actionable. Refs #81394. (#81425) Thanks @BKF-Gitty.</li>
<li>Agents/cron: honor a cron payload's explicit <code>timeoutSeconds</code> for the LLM idle watchdog even when it numerically equals <code>agents.defaults.timeoutSeconds</code>, preserving explicit per-run timeout intent and preventing stalled streaming replies from being cut to the implicit 120s cap. (#79426) Thanks @legolaz8451.</li>
<li>Codex app-server: keep the short post-tool completion watchdog armed across dynamic tool completion bookkeeping so embedded Codex runs fail fast and release their session lane when Codex goes quiet after a tool result. (#81697) Thanks @mbelinky.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Gateway/OpenAI HTTP: honor <code>max_completion_tokens</code> and <code>max_tokens</code> on inbound <code>/v1/chat/completions</code> requests so client-provided token caps reach the upstream provider via <code>streamParams.maxTokens</code>, with <code>max_completion_tokens</code> taking precedence when both are sent. Thanks @Lellansin.</li>
<li>Models/OpenAI CLI auth: make <code>openclaw models auth login --provider openai</code> start the ChatGPT/Codex account login by default, while <code>--method api-key</code> remains the explicit OpenAI API-key setup path.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside explicit SDK OAuth auth-result config patches, so provider helpers emit <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside SDK OAuth auth-result default config patches, so helper-built provider auth flows emit <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids returned by direct <code>openclaw models auth login --set-default</code> provider auth flows before writing config, so Gemini testing targets <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in per-agent config defaults and auth patches, so agent-specific emitted config keeps targeting <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows when API-key onboarding only reapplies the agent default, so emitted config keeps testing <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in <code>config set</code> mutation output for agent overrides and provider catalog rows, so current config emits <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: canonicalize provider-qualified retired Gemini 3 Pro Preview refs during Google forward-compatible model resolution, so emitted config uses <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>Google/Gemini: normalize proxy-prefixed retired Gemini 3 Pro Preview catalog rows, so emitted configs use <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside per-agent model overrides before writing config, so agent-specific config emits <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in subagent, heartbeat, compaction, and subagent-tool model config during writes, so current config keeps emitting <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Docs/subagents: document <code>agents.defaults.subagents.announceTimeoutMs</code> in the sub-agent and configuration references. (#75509) Thanks @akrimm702.</li>
<li>Cron: add direct <code>cron.get</code>, <code>openclaw cron get <id></code>, and agent-tool <code>get</code> support for inspecting one stored cron job by id. (#75117) Thanks @samzong.</li>
<li>Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC.</li>
<li>ACP: expose Gateway session lineage metadata through ACP session listings and session info snapshots so clients can render subagent graphs without private Gateway side channels. (#73458) Thanks @samzong.</li>
<li>Channels/iMessage: add <code>openclaw channels status --channel <name></code> filtering and document the BlueBubbles-to-imsg cutover path so operators can probe iMessage without starting both channel monitors. (#80706) Thanks @omarshahine.</li>
<li>CI: add a non-blocking <code>plugin-inspector-advisory</code> artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate.</li>
<li>Runtime/Fly: detect Fly Machines as container environments from their runtime env vars, so gateway bind and Bonjour defaults match remote container launches. (#80209) Thanks @liorb-mountapps.</li>
<li>Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to <code>/edit</code> with <code>image_urls</code> array, enforce NB2 edit geometry using <code>aspect_ratio</code> and <code>resolution</code> params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007.</li>
<li>Control UI: show a plain HTML recovery panel when the app module never registers, giving blank dashboard pages a retry path and browser-extension troubleshooting link. Fixes #44107. Thanks @BunsDev.</li>
<li>Docs: rename the broad tools nav to Capabilities, keep automation and agent coordination as sections, and keep the tools overview focused on tools, skills, and plugins. https://docs.openclaw.ai/tools</li>
</ul>
<ul>
<li>Build: enable additional low-churn oxlint rules for promise, TypeScript, and runtime footgun checks.</li>
<li>Build: enable stricter Vitest lint rules for focused, disabled, conditional, hook, matcher, and expectation hazards.</li>
<li>Build: pin explicit oxfmt defaults in the shared formatter config to keep formatting behavior stable across upgrades.</li>
<li>TypeScript: enable stricter compiler checks for implicit returns, side-effect imports, overrides, and unused production code.</li>
<li>Logging: add targeted model transport, payload, SSE, and code-mode diagnostics with redacted URL handling.</li>
<li>Agents: allow <code>session.agentToAgent.maxPingPongTurns</code> up to 20 while keeping the default at 5 for longer agent-to-agent reply chains. Fixes #52382. (#52400) Thanks @thirumaleshp.</li>
<li>Agents: add per-agent <code>tools.message.crossContext</code> overrides so sandboxed/public agents can restrict message sends to the current conversation without changing the global bot policy.</li>
<li>Agents: add per-agent <code>tools.message.actions.allow</code> overrides so sandboxed/public agents can expose and enforce send-only message tools.</li>
<li>Agents: omit the sandbox workspace marker from compact command progress previews while keeping internal sandbox diagnostics unchanged.</li>
<li>Agents: widen progress draft command preview lines by 50% so Discord inline tool updates preserve more useful command context.</li>
<li>Codex app-server: retire timed-out app-server clients after bounded turn interrupts so Discord agents do not reuse a CPU-spinning Codex process after an attempt timeout.</li>
<li>Codex app-server: default migrated native plugin destructive-action policy to enabled while preserving explicit global and per-plugin false overrides.</li>
<li>Build: upgrade workspace package management to pnpm 11 and keep Docker, install, update, and release workflows on the pnpm 11 config surface. (#79414) Thanks @altaywtf.</li>
<li>Build: align Telegram QA workflows and git source installs with the pnpm 11 workspace build allowlist surface. (#80588) Thanks @altaywtf.</li>
<li>Models: add provider-level <code>localService</code> startup for on-demand local model servers before OpenAI-compatible requests, including one-shot model probes.</li>
<li>Agents: trim default system prompt guidance and send-only message tool schemas to reduce prompt tokens while preserving GPT-5 personality guidance.</li>
<li>Context: add <code>/context map</code> to send a treemap image of the current session context contributors. (#79867)</li>
<li>Slack: add <code>unfurlLinks</code> and <code>unfurlMedia</code> config for bot <code>chat.postMessage</code> replies, including per-account overrides, so Slack link and media previews can be suppressed without workspace-wide settings. Fixes #48435. (#80145) Thanks @esegev1 and @HemantSudarshan.</li>
<li>Slack: add explicit <code>replyBroadcast</code> support for text and Block Kit thread replies so agents can opt into Slack's parent-channel <code>reply_broadcast</code> behavior. (#64365) Thanks @tony88331.</li>
<li>Slack: preserve mention target/source metadata in inbound prompt context so agents can distinguish direct bot mentions from implicit thread wakes that mention someone else. Fixes #79025. (#75356) Thanks @tmimmanuel.</li>
<li>Slack: canonicalize outbound delivery-mirror routes for native DM channel IDs to the peer user session so <code>message.send</code> calls to <code>D...</code> targets do not split the same Slack DM thread into a channel session. Fixes #80091. (#80111) Thanks @bek91.</li>
<li>Plugin SDK: deprecate public subpaths that existed for at least one month and have no bundled extension production imports, keep legacy barrel/test/zod subpath package exports for backwards compatibility, and track both sets in the SDK surface report.</li>
<li>Plugin SDK: deprecate public subpaths currently used by only one or two bundled plugin owners, keeping them importable while steering new plugin code to focused shared SDK seams or plugin-owned APIs.</li>
<li>Plugin SDK: remove the owner-specific <code>provider-auth-login</code> public subpath after moving Chutes, GitHub Copilot, and OpenAI Codex auth flows back to provider-owned modules.</li>
<li>Plugin SDK: remove provider-specific model, stream, and xAI compatibility helpers from public exports after moving bundled callers to provider-owned modules.</li>
<li>Plugin SDK: expose runtime-supplied active model metadata to native plugin tool factories for diagnostics and plugin-owned policy decisions. Fixes #77857. Thanks @jamiezigelbaum.</li>
<li>QA/Mantis: add Telegram live PR evidence automation with Convex-leased credentials, Crabbox transcript capture, motion GIF previews, and inline PR comments.</li>
<li>QA/Mantis: add a Telegram desktop scenario builder that leases Crabbox, installs native Telegram Desktop, configures an OpenClaw Telegram gateway with leased bot credentials, and records VNC screenshot/video artifacts.</li>
<li>Discord/voice: add realtime voice diagnostics for speaker turns, playback resets, barge-in detection, and audio cutoff analysis.</li>
<li>Talk: add <code>talk.realtime.instructions</code> so operators can append realtime voice style instructions while preserving OpenClaw's built-in agent-consult guidance. (#79081) Thanks @VACInc.</li>
<li>Discord/voice: default test and source installs to the pure-JS <code>opusscript</code> decoder by ignoring optional native <code>@discordjs/opus</code> builds, avoiding slow native addon compiles outside dedicated voice-performance lanes.</li>
<li>Discord/voice: add an opt-in native <code>@discordjs/opus</code> install script and decoder preference for live voice-performance lanes without charging unrelated Docker/tests for native addon builds.</li>
<li>Discord/voice: add <code>voice.allowedChannels</code> to restrict voice joins and bot voice-state moves to configured channels while preserving open voice behavior when unset.</li>
<li>Gateway/skills: add an opt-in private skill archive upload install path gated by <code>skills.install.allowUploadedArchives</code>, so trusted Gateway clients can stage and install zip-backed skills only when operators explicitly enable the code-install surface. (#74430) Thanks @samzong.</li>
<li>Codex app-server: enable Codex native code-mode-only for harness threads so deferred OpenClaw dynamic tools run through Codex's own searchable code execution surface instead of a PI-style wrapper.</li>
<li>Dependencies: refresh workspace pins and patch targets, including ACPX <code>@agentclientprotocol/claude-agent-acp</code> <code>0.33.1</code>, Codex ACP <code>0.14.0</code>, Baileys <code>7.0.0-rc10</code>, Google GenAI <code>2.0.1</code>, OpenAI <code>6.37.0</code>, AWS SDK <code>3.1045.0</code>, Kysely <code>0.29.0</code>, Tlon skill <code>0.3.6</code>, Aimock <code>1.19.5</code>, and tsdown <code>0.22.0</code>.</li>
<li>Dependencies: refresh workspace pins for Anthropic SDK, Smithy shared ini loading, Playwright, YAML, Aimock, TypeScript native preview, Vitest, Oxlint/Oxfmt, Vite, and pnpm 11.1.0.</li>
<li>Dependencies: hard-pin non-peer direct dependency specs across bundled packages and add a changed-check guard so runtime installs resolve the exact versions tested by maintainers.</li>
<li>Dependencies: move embedded Pi packages to the <code>@earendil-works</code> namespace, refresh Twitch Twurple packages, and move <code>@openclaw/fs-safe</code> from the GitHub release pin to the published npm package.</li>
<li>Build: route Testbox changed-check delegation through Crabbox and remove the OpenClaw-specific Blacksmith Testbox helper scripts.</li>
<li>Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle.</li>
<li>Agents/process: tell agents to inspect background sessions with <code>process log</code> before sending interactive input and to use <code>waitingForInput</code>/<code>stdinWritable</code> hints from <code>log</code>/<code>poll</code>.</li>
<li>CLI/onboarding: improve setup, onboarding, configure, and channel command wayfinding so terminal flows explain the next useful command instead of relying on terse setup labels.</li>
<li>Agents/Codex: remove the configurable Codex dynamic-tools profile so Codex app-server always owns workspace, edit, patch, exec, process, and plan tools while OpenClaw integration tools remain available.</li>
<li>macOS app: update the Peekaboo bridge dependency to Peekaboo 3.0.0.</li>
<li>Dependencies: refresh workspace pins and move the WhatsApp plugin from <code>@whiskeysockets/baileys</code> to <code>baileys</code> while keeping the <code>7.0.0-rc10</code> runtime.</li>
<li>Plugin SDK: add bundled-plugin session actions, <code>sendSessionAttachment</code>, and Cron-backed <code>scheduleSessionTurn</code>/tag cleanup under the grouped session namespace. Replaces #75578/#75581/#75588 and part of #73384/#74483. Thanks @100yenadmin.</li>
<li>Plugin SDK/media-understanding: add <code>extractStructuredWithModel(...)</code> plus the optional provider-side <code>extractStructured(...)</code> seam so trusted plugins can run bounded image-first structured extraction with optional supplemental text context through provider-owned runtimes such as Codex.</li>
<li>Exec approvals: add <code>tools.exec.commandHighlighting</code> so parser-derived command highlighting in approval prompts can be enabled globally or per agent. (#79348) Thanks @jesse-merhi.</li>
<li>Codex app-server: mirror native Codex subagent spawn lifecycle events into Task Registry so app-server child agents appear in task/status surfaces without relying on transcript text. (#79512) Thanks @mbelinky.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>CLI/media: render terminal QR codes with full-block characters by default so the bundled <code>qrcode</code> terminal renderer does not emit a pathologically dense ANSI final row in compact half-block mode that breaks scanning in some terminals. Fixes #77820. Thanks @KrasimirKralev.</li>
<li>Agents/compaction: read post-compaction AGENTS.md refresh context from the queued run workspace instead of the runner process cwd, so CLI-backed follow-up turns re-inject the correct workspace startup rules after compaction. Fixes #70541. (#75532) Thanks @vyctorbrzezowski.</li>
<li>Agents/read tool: treat positive offsets beyond EOF as empty ranges instead of surfacing the upstream read error, so stale pagination cursors no longer crash tool calls while unrelated read failures still fail loud. Fixes #62466. (#75536) Thanks @vyctorbrzezowski.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview refs left in Google API-key onboarding model allowlists and fallbacks, so setup-emitted config keeps testing <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
<li>Telegram/context: bound selected topic context to the active session so messages from before <code>/new</code> or <code>/reset</code> are not replayed into later turns. (#80848) Thanks @VACInc.</li>
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids when resolving exact configured proxy-provider refs, so <code>kilocode/google/gemini-3-pro-preview</code> resolves to <code>kilocode/google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
<li>CLI: strip generic OSC terminal escape payloads from sanitized output fields, preventing clipboard/title escape bodies from leaking into commitment tables and other terminal-safe text. Thanks @shakkernerd.</li>
<li>Codex app-server: match connector-backed plugin approval elicitations by stable connector id so enabled destructive actions no longer fall through to display-name-only rejection.</li>
<li>Build: replace selected build utility <code>tsx</code> preloads with Node native type stripping so Node 26 build paths no longer emit <code>DEP0205</code> module loader deprecation warnings. (#78584) Thanks @keshavbotagent.</li>
<li>Media generation: honor configured music and video generation timeouts when tool calls omit <code>timeoutMs</code>, matching image generation behavior. (#80687)</li>
<li>CLI/update/status: label beta-channel plugin fallback and model-pricing refresh failures as warnings, keeping mixed beta/latest plugin cohorts visible without making core update or Gateway reachability look failed. Fixes #80689. Thanks @BKF-Gitty.</li>
<li>Doctor/plugins: relink managed npm plugin <code>openclaw</code> peer dependencies during <code>doctor --fix</code>, while refusing to follow package-local <code>node_modules</code> symlinks outside the plugin package. (#77412) Thanks @TheCrazyLex.</li>
<li>iMessage: route inbound tapbacks as reaction system events instead of normal messages, defaulting to bot-authored-message notifications while allowing <code>reactionNotifications: "off" | "own" | "all"</code> overrides. Fixes #60274; refs #39031 and #39322. Thanks @hyperclaw.</li>
<li>Control UI/performance: scope Nodes polling to the active Nodes tab, debounce stale session-list reconciliation, and bound chat-side session refreshes so long-running dashboards avoid background reload churn. Thanks @BunsDev.</li>
<li>Plugins/channels: explain bundled channel entry files that reach the legacy plugin loader as setup-runtime loader mismatches instead of generic missing-register failures. Thanks @chinar-amrutkar.</li>
<li>Plugins/session-end: fire a typed <code>session_end</code> plugin hook with reason <code>shutdown</code> (or <code>restart</code> when a restart is expected) for every session that was still active when the gateway process stops. Previously SIGTERM/SIGINT/restart paths closed the gateway without enumerating active sessions, leaving downstream <code>session_end</code> plugins (e.g. claude-mem) with ghost rows accumulating across restarts. The new shutdown finalizer drains an in-memory tracker that is populated by <code>session_start</code> and forgotten by replace / reset / delete / compaction emitters, so previously-finalized sessions are never double-fired. The drain is bounded to a 2 s total budget so a slow plugin cannot block process exit. Adds <code>"shutdown"</code> and <code>"restart"</code> to <code>PluginHookSessionEndReason</code>. Fixes #57790. Thanks @pandadev66.</li>
<li>Codex app-server: clamp Codex code-mode sandboxing to workspace-write when an OpenClaw sandbox is active, preventing Docker gateway socket access from becoming a danger-full-access Codex turn.</li>
<li>TUI: exit immediately on Ctrl+C/SIGINT after gateway disconnect and bound shutdown drain so terminal teardown cannot strand sessions. Fixes #75379. (#75381) Thanks @udaymanish6.</li>
<li>Matrix: default outbound markdown tables to bullet lists instead of fenced code blocks. Fixes #78990. (#80890) Thanks @kinjitakabe.</li>
<li>Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev.</li>
<li>Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns <code>chat content is empty</code> after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS.</li>
<li>Tools/media: preserve implicit allow-all semantics from <code>tools.alsoAllow</code>-only policies when preconstructing built-in media generation and PDF tools, so configured media tools become live without forcing <code>tools.allow: ["*", ...]</code>. Fixes #77841. Thanks @trialanderrorstudios.</li>
<li>Codex/Telegram: separate code-mode tool progress from final replies, render bridged tool calls with native tool labels, and repair persisted missing tool results for safer follow-up turns. (#80663) Thanks @jalehman.</li>
<li>Memory/search: load the platform-specific <code>sqlite-vec-<platform>-<arch></code> variant directly when the meta <code>sqlite-vec</code> package is missing from a global install, so vector recall keeps working on <code>npm install -g openclaw@latest</code> upgrades where optionalDependencies left only the platform variant on disk. Fixes #77838. Thanks @corevibe555 and @Simon2256928.</li>
<li>Cron: keep long manual cron runs active in the task registry until completion, preventing transient <code>lost</code> markers before durable recovery reconciles. Fixes #78233. (#78243) Thanks @Feelw00.</li>
<li>Doctor/GitHub CLI: surface a <code>GH_CONFIG_DIR</code> hint when the GitHub skill is usable but <code>gh</code> auth lives under a different operator HOME than the agent process, without warning for disabled or filtered skills. Fixes #78063. (#78095) Thanks @tmimmanuel.</li>
<li>Gateway: dedupe concurrent <code>send</code>, <code>poll</code>, and <code>message.action</code> requests while delivery is still in flight, preventing duplicate outbound work for the same idempotency key. (#68341) Thanks @thesomewhatyou.</li>
<li>Cron: keep main-session <code>systemEvent</code> heartbeat wakes on their bound session route for both direct and queued wake paths by dropping inherited explicit heartbeat destinations when forcing <code>target: "last"</code>. Fixes #73900. Thanks @richardmqq.</li>
<li>Telegram: honor forced document delivery for video media so <code>--force-document</code> sends MP4s as documents instead of typed videos. Fixes #80389. (#80405) Thanks @jbetala7.</li>
<li>Gateway: clear speculative node wake state when APNs registration is missing, preventing unregistered or mistyped node IDs from retaining wake throttle entries. Fixes #68847. (#68848) Thanks @Feelw00.</li>
<li>Auto-reply: keep late follow-up queue drain finalizers from deleting a replacement queue registered after <code>/stop</code>, preventing immediate follow-up messages from being orphaned. Fixes #68838. (#68839) Thanks @Feelw00.</li>
<li>Feishu: make manual App ID/App Secret setup the default channel-binding path while keeping QR scan-to-create as an optional best-effort flow, and document the manual fallback for domestic Feishu mobile clients that do not react to the QR code. Fixes #80591. Thanks @wei-wei-zhao.</li>
<li>Memory: cap dreaming promotion writes to <code>MEMORY.md</code> by compacting oldest auto-promoted sections while preserving user-authored notes, keeping active memory below the bootstrap budget. Fixes #73691. (#74088) Thanks @YB0y.</li>
<li>Telegram: show resolved thinking defaults in native <code>/status</code> and <code>/think</code> menus while preserving explicit session overrides. (#80341) Thanks @VACInc.</li>
<li>Channels: cache selected channel registry lookups against the active fallback snapshot so pinned-empty registries refresh native command and alias routing after active registry swaps. (#80333) Thanks @samzong.</li>
<li>Codex app-server: reuse native Codex CLI OAuth for isolated app-server harness login, refresh, and app inventory cache keys so ChatGPT-authenticated Codex runs no longer fall back to unauthenticated OpenAI API calls. (#79877) Thanks @jeffjhunter.</li>
<li>Gateway: scope <code>sessions.resolve</code> sessionId and label store loads to the requested agent so large unrelated agent stores are not parsed for scoped lookups. Fixes #51264. (#79474) Thanks @samzong.</li>
<li>Gateway: share serialized streaming event envelopes across eligible WebSocket and node subscribers while preserving per-client sequence numbers. (#80299) Thanks @samzong.</li>
<li>Gateway: consolidate duplicate <code>openclaw doctor</code> service config panels while preserving the declined-repair <code>--force</code> hint. Fixes #80287. (#78688) Thanks @YB0y.</li>
<li>Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc.</li>
<li>WhatsApp: route opening-phase Baileys 428 connectionClosed through the WhatsApp reconnect policy and keep post-open 428 closes retryable, so transient setup socket closes retry with WhatsApp diagnostics instead of escaping as a bare <code>channel exited</code> error. Fixes #75736; mitigates #77443. Thanks @dataCenter430.</li>
<li>Agents: disable Pi's default filesystem resource discovery for embedded runs while keeping OpenClaw inline extension factories active, avoiding Windows event-loop stalls during first WhatsApp-triggered agent startup. Fixes #77443. Thanks @dataCenter430.</li>
<li>Providers/self-hosted: read model-scoped llama.cpp runtime context from <code>/props.default_generation_settings.n_ctx</code> while keeping top-level <code>n_ctx</code> as a fallback, so session budgeting reflects the loaded context window. Fixes #73664. (#74057) Thanks @brokemac79.</li>
<li>Memory: reject symlinked directory components in configured extra memory paths before reading Markdown files. (#80331) Thanks @samzong.</li>
<li>Sessions/transcripts: replace whole-file <code>readFile</code> scans with shared streaming helpers (<code>streamSessionTranscriptLines</code> and <code>streamSessionTranscriptLinesReverse</code>) for idempotency lookup, latest/tail assistant text reads, delivery-mirror dedupe, and compaction fork loading, so long-running sessions no longer materialize the full transcript in memory. Forward scans use <code>readline</code> over a bounded <code>createReadStream</code>; reverse scans read bounded chunks from the file end and decode complete JSONL lines newest-first without a fixed tail cap. Synthetic 200 MiB transcript: peak RSS delta drops from +252 MiB to +27 MiB while preserving malformed-line tolerance and idempotency-key return semantics. Fixes #54296. Thanks @jack-stormentswe.</li>
<li>Browser/CDP: filter browser-internal targets from raw CDP and persistent Playwright tab selection so navigation opens real page tabs. Fixes #55734. Thanks @Demine4.</li>
<li>WhatsApp: apply hot-reloaded <code>dmPolicy</code> and <code>allowFrom</code> settings to the active Web listener before processing new inbound DMs. Fixes #80538. Thanks @Ampaskopi129.</li>
<li>Plugins: let <code>openclaw doctor --fix</code> repair managed plugin installs whose package entrypoints fail package-directory boundary validation after local state moves. Fixes #80592. Thanks @wei-wei-zhao.</li>
<li>Voice-call: resume voice-originated exec approval follow-ups as internal non-delivery turns instead of rejecting them as <code>unknown channel: voice</code>. Fixes #80540. Thanks @patrickmch.</li>
<li>Control UI: preserve the composer draft when Stop is tapped during an active chat run, preventing accidental prompt loss on mobile. Fixes #80586. Thanks @KCALLC.</li>
<li>Infra/retry: keep jittered retry delays at or above server-supplied Retry-After lower bounds when the hint can be honored. Fixes #68541. (#68543) Thanks @Feelw00.</li>
<li>Docs: clarify that <code>/model provider/model</code> is an exact session route, while duplicate bare model ids only use configured fallback order on non-session override paths. Refs #80562. Thanks @gaodaabao.</li>
<li>Redact persisted secret-shaped payloads [AI]. (#79006) Thanks @pgondhi987.</li>
<li>Agents: label <code>.openclaw/sandboxes</code> exec workdirs as sandbox runs in compact tool summaries instead of showing the full path.</li>
<li>OpenAI Codex: surface browser OAuth and device-code login failures instead of treating failed logins as empty successful auth results. Refs #80363.</li>
<li>CLI agents: carry runtime-only current-turn sender/reply context into CLI model prompts while keeping prompt-build hook input and transcript text clean.</li>
<li>Control UI: keep workspace file presence checks from treating <code>fs-safe</code> stat helper failures as missing files, restoring Agents file status for existing Windows workspace files. Fixes #79953. Thanks @lovelefeng-glitch.</li>
<li>Microsoft Foundry: report an explicit error when the Azure subscription prompt returns an id that is not present in the enabled subscription list, instead of continuing from an unsafe subscription assertion. (#62742) Thanks @oliviareid-svg.</li>
<li>fix(matrix): gate name-based allowlist resolution [AI]. (#79007) Thanks @pgondhi987.</li>
<li>Slack: include the bot's own root/parent message in new thread sessions so in-thread replies reach the agent with the parent text the user is responding to, instead of only <code>reply_to_id</code> metadata. Fixes #79338. Thanks @sxxtony.</li>
<li>Docker: keep image builds on the source pnpm workspace policy so pnpm 11 can prune production dependencies without a Docker-only workspace rewrite.</li>
<li>Agents/compaction: restore info-level gateway logs for embedded compaction start, completion, and incomplete outcomes. (#71961) Thanks @rubencu.</li>
<li>Telegram: build reply-aware inbound turns through the shared channel context path so agents see the current reply target inline with the current message.</li>
<li>Telegram: recover legacy message cache files that mixed JSON-array and line-delimited entries so restarted gateways preserve reply-window context. (#80567)</li>
<li>Telegram: update the reply-context cache when messages are edited, so streamed bot replies appear in later agent context with their final text instead of the first draft.</li>
<li>Skills/Windows: normalize compacted skill prompt locations to forward slashes after home-prefix compaction so Windows skill paths remain readable by model file tools. (#52200) Thanks @chienchandler.</li>
<li>Control UI/Windows: update <code>@openclaw/fs-safe</code> so agent workspace file presence checks fall back correctly on Windows, preventing existing AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md, and MEMORY.md files from showing as missing. Fixes #79953. Thanks @lovelefeng-glitch.</li>
<li>Memory: skip managed dreaming cron reconciliation warnings for ordinary cron and heartbeat hook contexts that cannot manage Gateway cron. (#77027) Thanks @rubencu.</li>
<li>Cron: treat Codex app-server turn acceptance, CLI process spawn, and tool starts as execution milestones, preventing isolated runs from tripping the early startup watchdog after work has begun.</li>
<li>Codex app-server: treat current-turn <code><turn_aborted></code> raw markers as terminal so interrupted native-tool turns release Discord agent sessions instead of waiting for the outer timeout.</li>
<li>Yuanbao: bump <code>openclaw-plugin-yuanbao</code> to 2.13.1 to support <code>sourceReplyDeliveryMode: "automatic"</code> for group chat. (#79814) Thanks @loongfay.</li>
<li>Memory: keep <code>memory_search</code> result <code>corpus</code> labels aligned with the hit source, so session transcript hits surface as <code>sessions</code> and memory-file hits stay <code>memory</code>. Fixes #72885. (#71898, #72886) Thanks @rubencu.</li>
<li>Codex app-server: default native plugin app tool approvals to automatic so non-destructive read tools run when destructive actions are disabled.</li>
<li>Plugins: allow untracked local source plugins in the global extensions directory to load TypeScript package entries while keeping managed installs strict about compiled runtime output. Fixes #80503. Thanks @Kaspre.</li>
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so <code>google/gemini-3.1-pro-preview</code> is used for testing instead of <code>google/gemini-3-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids inside saved model allowlists and fallback chains, so proxy routes like <code>openrouter/google/gemini-3-pro-preview</code> are persisted as Gemini 3.1 Pro Preview.</li>
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids in configured proxy/provider-auth model catalogs, so regenerated config keeps testing <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while onboarding provider catalog presets, so setup-emitted proxy configs test <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows during generic config writes, so unrelated config changes keep testing <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Models: keep configured fallback chains ahead of configured primary models for override selections with duplicate model ids, preventing fallback jumps to the wrong provider. Fixes #80562.</li>
<li>Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates.</li>
<li>Gateway/agents: keep stale <code>sessions_send</code> ACP manager and <code>web_fetch</code> runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72.</li>
<li>Gateway/install: preserve service environment value-source metadata in <code>openclaw gateway install</code>, so systemd reinstall paths keep env-file-backed secrets out of inline unit metadata. Refs #77406, #77427. Thanks @stainlu and @brokemac79.</li>
<li>Auto-reply/reset: include inbound sender context in bare <code>/new</code> and <code>/reset</code> model prompts while keeping startup instructions out of transcript prompts, so agents see sender identity on the first reset turn. Fixes #77360. Thanks @srb11e.</li>
<li>Gateway: avoid synchronous restart-sentinel state probes during post-attach startup, preventing slow Windows or redirected state directories from blocking channel turns. Fixes #79264. Thanks @liyi58.</li>
<li>Agents/auth: update successful model auth profile status with one locked store write, reducing post-model reply latency from duplicate <code>auth-profiles.json</code> saves. Thanks @mcaxtr.</li>
<li>Agents/image: honor explicit <code>image</code> tool model overrides even when <code>agents.defaults.imageModel</code> is unset, restoring one-off vision calls for configured multimodal providers. Fixes #79341. Thanks @haumanto.</li>
<li>Doctor/update: leave live systemd gateway units unchanged during noninteractive update-mode service repair, so update-time doctor does not silently overwrite operator-owned unit directives. Refs #80462.</li>
<li>Update: accept optional leading <code>v</code> prefixes when verifying exact npm package install targets, so <code>openclaw update --tag v2026...</code> does not roll back after installing the matching bare package version. Refs #74069; #80480. Thanks @Kaspre.</li>
<li>Doctor: treat missing plugin ids in <code>plugins.deny</code> as stale config warnings instead of fatal validation errors, and remove them during stale plugin cleanup so update repair does not restore last-known-good config for deny-only stale plugin refs. Refs #77802. Thanks @Kaspre.</li>
<li>Codex app-server: preserve prompt-local current-turn context through context-engine prompt projection, so replied-to Telegram messages stay visible to the Codex model input.</li>
<li>Telegram: pass agent-scoped media roots through gateway message actions so workspace-local media from the active agent is not rejected as cross-agent access. Thanks @frankekn.</li>
<li>CLI/gateway: keep <code>gateway status --deep</code> plugin-aware so configured plugin manifest warnings, including missing channel config metadata, stay visible during install and update smoke checks.</li>
<li>Doctor/status: clarify gateway token source conflict warnings and suppress them inside the managed Gateway service credential context.</li>
<li>Feishu: accept Schema 2 card callbacks whose operator identity is nested under <code>operator.user_id</code>, so card buttons dispatch instead of being dropped as malformed. Fixes #71670. (#71787) Thanks @rubencu.</li>
<li>Feishu: fall back to a top-level group send when normal group quoted replies target a withdrawn or missing message, preventing replies from disappearing silently while preserving native topic safety. Fixes #79349. Thanks @arlen8411.</li>
<li>Doctor: stop flagging the live compatibility agent directory as orphaned when the configured default agent is not <code>main</code>. Fixes #74313. (#74438) Thanks @carlos4s.</li>
<li>Auth/Claude CLI: persist fresher managed external CLI OAuth credentials back to <code>auth-profiles.json</code>, preventing stale <code>anthropic:claude-cli</code> profiles from repeatedly bootstrapping and flooding debug logs. Fixes #80129. Thanks @Caulderein.</li>
<li>Context: render <code>/context map</code> only from actual run context and persist Codex app-server run reports without counting deferred tool-search schemas as prompt-loaded tool schemas.</li>
<li>Codex app-server: report Codex-native tool execution to diagnostics so long-running native <code>bash</code>, web, file, and MCP tools no longer look like stale embedded runs to the watchdog. (#80217)</li>
<li>Codex app-server: refresh Codex account rate limits after subscription usage-limit failures so Discord and other channel replies can show the next reset time instead of saying Codex returned none. Thanks @pashpashpash.</li>
<li>Agents/auth: let Codex-backed OpenAI agent turns use <code>auth.order.openai</code> entries for Codex-compatible OAuth and API-key profiles while keeping existing <code>openai-codex</code> profile ordering valid.</li>
<li>Codex app-server: emit async <code>after_tool_call</code> observations for native tool completions not covered by the native hook relay so observability plugins can record Codex-native tools. (#80372) Thanks @VACInc.</li>
<li>Tasks: route group and channel task completions through the requester session so the parent agent can send the visible summary instead of stopping at a generic task-status line. Fixes #77251. (#77365) Thanks @funmerlin.</li>
<li>Telegram: preserve blank lines between manually indented bullet blocks and following numbered sections in rendered replies. Fixes #76998. Thanks @evgyur.</li>
<li>Agents/sandbox: allow read-only sandbox sessions to read the <code>/agent</code> workspace mount while keeping write/edit/apply_patch workspace-only guarded, restoring <code>read /agent/...</code> for <code>workspaceAccess: "ro"</code>. Fixes #39497. Thanks @stainlu and @teosborne.</li>
<li>Slack: pass configured agent identity through draft preview sends so partial streaming replies keep custom username/avatar on the initial Slack message. Fixes #38235. (#38237) Thanks @lacymorrow.</li>
<li>Slack: support <code>allowBots: "mentions"</code> for bot-authored messages that mention the receiving bot, matching the documented Discord-style mode without accepting every bot message. Fixes #43587. (#43588) Thanks @raw34.</li>
<li>Slack: refresh private file URLs with <code>files.info</code> when inbound DM file events omit or stale attachment URLs, preventing file attachments from being dropped before media hydration. Fixes #50129. (#50200) Thanks @smartchainark.</li>
<li>Slack: add scoped message-tool formatting hints so agents use Markdown for plain sends and direct mrkdwn for Block Kit fields. Fixes #34609. (#50979) Thanks @carrotRakko.</li>
<li>Slack: describe <code>download-file</code> file ids separately from message timestamps and return a targeted recovery error when agents pass <code>messageId</code> instead of <code>fileId</code>. (#74155) Thanks @jarvis-ai-gregmoser.</li>
<li>Slack: retain processed room messages for <code>requireMention=false</code> channels so always-on Slack rooms keep recent conversation context between turns. (#38658) Thanks @syedamaann.</li>
<li>Slack: compile interactive reply directives for direct outbound sends without bypassing the <code>interactiveReplies</code> capability gate, preserving Block Kit for Slack CLI and cron deliveries. (#78220) Thanks @kazamak.</li>
<li>Slack: keep DM last-route updates scoped to the active non-main DM session, including threaded DM turns, so isolated Slack DM sessions do not overwrite the shared main route. (#73085) Thanks @clawSean.</li>
<li>Slack/ACP: route Slack channel and DM messages through configured ACP bindings when no runtime binding exists, keeping bound thread replies pinned to the persistent ACP session and dropping unavailable configured targets instead of falling back to <code>main</code>. (#73101) Thanks @Raasl.</li>
<li>Slack: mark unresolved thread replies as ambiguous and skip them instead of treating them as root channel messages, keeping thread continuation on the SDK-backed participation store. (#75630) Thanks @soichiyo.</li>
<li>Slack: let same-channel message tool sends opt out of inherited thread context with <code>topLevel: true</code> or <code>threadId: null</code>, allowing agents to post a new parent-channel message from inside a Slack thread. Fixes #79807. Thanks @vexclawx31.</li>
<li>Slack: prefer full rich-text block content over truncated socket-mode message previews so long inbound Slack messages reach agents intact. Fixes #79027. Thanks @BobAccentWebDev.</li>
<li>Slack: include structured Slack API error details in setup, probe, streaming, and reply logs while preserving token redaction. (#53966) Thanks @deucemask.</li>
<li>Gateway/agents: keep structured reasons when active-run queueing fails and deprecate the legacy boolean queue helper, so steering and subagent wake diagnostics distinguish completed, non-streaming, and compacting runs. Fixes #80156. Thanks @markus-lassfolk.</li>
<li>System events: dedupe keyed events across the queue while preserving unkeyed, delivery-route, and trust-boundary event identity. (#73040) Thanks @statxc.</li>
<li>Agents/UI: compact exec and tool progress rows by hiding redundant shell tool names, replacing known workspace paths with short context markers, and preserving Discord trace scrubbing for compact command lines.</li>
<li>ACPX: run and await the embedded ACP backend startup probe by default so the gateway <code>ready</code> signal no longer fires before the acpx runtime has either become usable or reported a probe failure; set <code>OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=0</code> to restore lazy startup. Fixes #79596. Thanks @bzelones.</li>
<li>Gateway/status: surface model-pricing bootstrap and refresh failures as degraded health/status warnings while keeping Gateway liveness healthy. Fixes #79599. Thanks @bzelones.</li>
<li>OpenAI-compatible models: strip prior assistant reasoning fields from replayed Chat Completions history by default, preventing oMLX/vLLM Qwen follow-up turns from rejecting or stalling on stale <code>reasoning</code> payloads. Fixes #46637. Thanks @zipzagster and @lexhoefsloot.</li>
<li>CLI/onboarding: give non-Azure custom providers a safe generated context window and heal legacy 4k wizard entries without overwriting explicit valid small model limits, preventing first-turn compaction loops. Fixes #79428. (#79911) Thanks @Jefsky.</li>
<li>OpenAI-compatible models: add <code>compat.strictMessageKeys</code> to strip Chat Completions replay messages to <code>role</code> and <code>content</code> for strict providers that reject OpenAI-style tool and metadata keys. Fixes #50374. Thanks @choutos.</li>
<li>Bedrock Mantle: add <code>plugins.entries.amazon-bedrock-mantle.config.discovery.enabled=false</code> to suppress automatic Mantle discovery and IAM bearer-token generation while keeping the plugin enabled. Fixes #67288. Thanks @kanekoh.</li>
<li>Ollama: stop native <code>/api/chat</code> requests from copying catalog <code>contextWindow</code> or <code>maxTokens</code> into <code>options.num_ctx</code> unless <code>params.num_ctx</code> is explicitly configured, avoiding pathological prompt-ingestion latency on local large-context models. Fixes #62267. Thanks @BenSHPD.</li>
<li>Ollama: keep the model idle watchdog enabled for <code>*:cloud</code> models routed through a local Ollama host, so cloud-backed tool-loop stalls fail over visibly instead of inheriting local-model no-idle behavior. Fixes #79350. Thanks @geek111.</li>
<li>Voice/Ollama: honor routed voice agent <code>tools.allow</code> for classic embedded voice responses, including empty allowlists, so no-tool Ollama agents do not receive tool schemas. Fixes #79506. Thanks @donkeykong91.</li>
<li>Agents/doctor: warn when channel-routed agents cannot call the <code>message</code> tool, so operators can fix tool policy mismatches before explicit channel actions such as attachments or thread replies fail. Refs #80128. Thanks @jeffjhunterai.</li>
<li>Gateway: reread config from disk after the first in-process restart loop startup, preventing SIGUSR1 restarts from reusing a stale startup snapshot and dropping config written after boot. Fixes #79947. Thanks @TheLevti.</li>
<li>Codex app-server: deliver native image-generation outputs from Codex <code>savedPath</code> events as reply media, so blank-text image generation turns still attach the generated file. Thanks @keshavbotagent.</li>
<li>Network/SSRF: keep pinned automatic DNS lookups on IPv4 when dual-stack hosts also publish AAAA records, and treat <code>EADDRNOTAVAIL</code> as a transient gateway network failure instead of a fatal crash. Fixes #80078. Thanks @takamasa-aiso.</li>
<li>Control UI: show compact one-line live/idle/terminal run status badges in the Sessions table and rename the active-minute filter to its updated-within meaning. Fixes #78307. Thanks @BunsDev.</li>
<li>Control UI: scope chat session-list refreshes by agent and skip disk-only agent store discovery for configured-only lists, preventing post-first-message session switching stalls on large Windows stores. Fixes #79675. Thanks @lovelefeng-glitch, @BunsDev.</li>
<li>Control UI: allow Appearance tweakcn theme imports through the served CSP so browser-local custom theme links no longer fail with a <code>connect-src</code> violation. Fixes #78504. Thanks @BunsDev.</li>
<li>Control UI/config: remove plugin allowlist entries that the form auto-added when a plugin enable toggle is reverted before saving, so reverting the visible toggle clears dirty state without persisting unintended allowlist changes. (#78329) Thanks @samzong.</li>
<li>Gateway/mobile: reuse bootstrap-issued device-token scopes on handoff reconnects and surface device-token scope mismatches separately from token mismatches while preserving full shared-token dashboard/native sessions. Fixes #79292. Thanks @BunsDev.</li>
<li>Media/host-read: allow buffer-verified gzip, tar, and 7z archives in the shared host-local media validator alongside ZIP and document attachments.</li>
<li>Plugins/install: retry managed npm plugin installs without npm alias overrides after npm's <code>Invalid comparator: npm:</code> failure, so older npm versions can install official plugins instead of aborting. (#80539) Thanks @rubencu.</li>
<li>Plugins/doctor: invalidate persisted plugin registry snapshots when plugin diagnostics point at deleted source paths, so <code>openclaw doctor</code> stops repeating stale warnings after a local extension is replaced by a managed npm plugin. Fixes #80087. (#80134) Thanks @hclsys.</li>
<li>Doctor/OpenAI Codex: preserve Codex auth intent when auto-repairing legacy <code>openai-codex/*</code> model refs to canonical <code>openai/*</code> by adding provider/model-scoped Codex runtime policy, preventing repaired configs from falling through to direct OpenAI API-key auth. Fixes #78533 and #78570. Thanks @superck110 and @Azmodump.</li>
<li>CLI/agents: surface durable message delivery status from <code>sendDurableMessageBatch</code> in <code>deliverAgentCommandResult</code> and <code>openclaw agent --json --deliver</code>, preserving suppressed hook outcomes as terminal no-retry results while exposing partial and failed sends for automation. Supersedes #53961 and #57755. Thanks @Kaspre.</li>
<li>Agents: apply the LLM idle watchdog while provider stream setup is still pending, preventing silent pre-stream model hangs from waiting for the full agent timeout.</li>
<li>Cron: let isolated self-cleanup runs inspect their own job run history while keeping other cron jobs and mutation actions blocked. Fixes #80019. Thanks @hclsys.</li>
<li>Cron: report isolated agent-turn setup and pre-model stalls with phase-specific timeout errors instead of waiting for the full job budget when no model call starts. Fixes #74803. Thanks @jeffsteinbok-openclaw and @dgkim311.</li>
<li>CLI/plugins: treat arbitrary unknown subcommands outside plugin CLI metadata as normal unknown commands instead of suggesting <code>plugins.allow</code>, while preserving allowlist guidance for real plugin command roots. Fixes #80109. (#80123) Thanks @kagura-agent.</li>
<li>CLI/config: persist explicit <code>config set</code> and <code>config patch</code> values that equal runtime defaults instead of reporting success while dropping them. Fixes #79856. (#80106) Thanks @abodanty and @hclsys.</li>
<li>OpenAI/realtime voice: accept Codex-compatible legacy audio and transcript event aliases so provider protocol drift does not drop assistant audio or captions.</li>
<li>Discord/voice: keep default agent-proxy realtime sessions from auto-speaking filler before the forced OpenClaw consult answer, finish Discord playback on realtime response completion, and queue later exact-speech answers until playback idles to avoid mid-sentence replacement.</li>
<li>Gateway: return deterministic <code>400 invalid_request_error</code> responses for malformed encoded session-kill HTTP paths instead of letting route-shaped requests fall through to later Gateway handlers. (#72439) Thanks @rubencu.</li>
<li>Control UI: serve root PWA and favicon assets from <code>/__openclaw__/</code> SPA routes so tab icons, install metadata, and the service worker do not 404 after internal navigation. Fixes #80072. Thanks @CodeNovice2017.</li>
<li>Exec/safe bins: compare trusted safe-bin dirs with path-specific case folding on case-insensitive filesystems so Windows and default macOS paths match without weakening case-sensitive mounts. (#42131) Thanks @hkochar.</li>
<li>OpenAI/realtime voice: honor disabled input-audio interruption locally so server VAD speech-start events do not clear Discord playback after operators set <code>interruptResponseOnInputAudio: false</code>.</li>
<li>Telegram: keep no-response DM turns quiet instead of rewriting them into visible silent-reply chatter. Fixes #78188. (#78228) Thanks @Beandon13.</li>
<li>Telegram: handle managed select button callbacks before the raw callback fallback while preserving delimiter-containing option values such as <code>env|prod</code>. (#79816) Thanks @moeedahmed.</li>
<li>OpenAI-compatible models: handle JSON chat-completion bodies returned to streaming requests, preserving reasoning fields and visible text instead of completing an empty agent turn. Fixes #77870.</li>
<li>Discord/models: defer model picker component interactions before loading route, model, and preference data, preventing "This interaction failed" timeouts under gateway load. Fixes #77283. Thanks @colin-chang.</li>
<li>xAI: expose <code>/think low|medium|high</code> for reasoning-capable Grok models and keep <code>reasoning.effort</code> on native Responses payloads while preserving off-only behavior for non-reasoning routes. Fixes #79210. Thanks @colinmcintosh.</li>
<li>CLI/media: let explicit image description model refs use bundled static provider catalogs and generic model-backed image hooks, so <code>openclaw infer image describe --model zai/glm-4.6v</code> works like direct model runs and Anthropic auth probes avoid stale Claude 3 Haiku catalog entries.</li>
<li>Models/Anthropic: add <code>anthropic/claude-haiku-4-5</code> to Anthropic API-key agent allowlist defaults when an Anthropic default model is configured, so cron model overrides can select the current Haiku alias. Fixes #78000.</li>
<li>Agents/compaction: initialize built-in context engines before CLI transcript compaction resolves the default engine, preventing clean-process <code>legacy</code> engine registration failures during CLI session persistence. Fixes #79446. Thanks @TurboTheTurtle.</li>
<li>Agents/Anthropic-compatible: strip replayed thinking blocks for custom Anthropic-compatible models that explicitly declare <code>supportsReasoningEffort: false</code>, preventing Kimi-compatible providers from resending unsupported <code>thinking</code> content. Fixes #47452.</li>
<li>Kimi: keep Anthropic-compatible thinking streams valid by supplying required thinking budgets and enough output room for hidden reasoning plus final text. (#80481) Thanks @InTheCloudDan.</li>
<li>Browser: wait longer for existing-session Chrome MCP status and non-deep doctor probes so slow first attaches do not falsely report offline while keeping raw CDP status probes short. (#77473) Thanks @rubencu.</li>
<li>Gateway/logging: install console capture before foreground Gateway fast-path parsing and suppress known libsignal session dumps even in verbose mode, preventing raw terminal logs from printing WhatsApp session key material. (#76306) Thanks @rubencu.</li>
<li>Exec approvals: keep <code>exec.approval.list</code> on the lightweight policy-summary path so listing pending approvals no longer loads the rich tree-sitter command explainer. (#76943) Thanks @rubencu.</li>
<li>Agents: surface concise default-visible warnings when <code>exec</code>/<code>bash</code> tool calls fail after the assistant claims success, while keeping raw stderr hidden unless verbose details are enabled. Fixes #60497. (#80003) Thanks @jbetala7.</li>
<li>Channels/iMessage: keep redacted failed probe details in non-sensitive health snapshots so Full Disk Access failures no longer appear as configured/OK in status output. Fixes #79795.</li>
<li>Agents: stop blank model-emitted tool calls before dispatch while preserving id-based tool-name recovery, preventing Kimi/NVIDIA blank-name retry loops without creating a callable <code>_blank</code> sentinel. Fixes #34129. (#56391) Thanks @smartchainark.</li>
<li>Agents/Telegram: deliver the canonical final assistant answer instead of replaying accumulated pre-tool text blocks, preventing duplicate Telegram replies and raw-looking tool-output fragments from leaking into chat delivery. Fixes #79621 and #79986. Thanks @nonzeroclaw and @dudaefj.</li>
<li>Auto-reply/TUI: keep fallback timeout recovery deliverable after a primary model lifecycle error by emitting fallback progress and deferring terminal TUI errors until recovery has a chance to finish. Fixes #80000. (#80009) Thanks @TurboTheTurtle.</li>
<li>Heartbeat: clear stale auto fallback model overrides when the configured default model changes, so heartbeat runs follow updated <code>agents.defaults.model.primary</code> without requiring a manual reset. Fixes #74284. Thanks @brtkwr and @bitloi.</li>
<li>CLI/agent: let <code>openclaw agent --model</code> use the backend/admin Gateway scope without cached device-token scopes silently downscoping the request. (#78837) Thanks @VACInc.</li>
<li>CLI/help: keep help and version invocations configless while improving shared port, channel, plugin, task, session, message, pairing, and auth recovery text.</li>
<li>CLI/config: explain strict JSON parse failures with a valid example and the plain-string escape hatch.</li>
<li>CLI/secrets: turn offline Gateway reload failures into actionable recovery text.</li>
<li>CLI/channels: explain missing or ambiguous channel selections with next commands.</li>
<li>CLI/channels: defer guided channel status collection until a channel is selected, keeping <code>openclaw channels add</code> first screen quieter.</li>
<li>CLI/channels: exit guided channel setup cleanly on cancellation instead of printing the internal wizard error.</li>
<li>Plugins/CLI: route disabled Matrix and LanceDB memory command roots to plugin-enable guidance instead of generic unknown-command errors.</li>
<li>Browser/Docker: detect Playwright-managed Chromium from <code>PLAYWRIGHT_BROWSERS_PATH</code> and the default Playwright cache on Linux, so Docker installs that persist <code>/home/node/.cache/ms-playwright</code> no longer need <code>browser.executablePath</code>.</li>
<li>Ollama: keep DeepSeek V4 cloud models thinking-capable even when Ollama Cloud <code>/api/show</code> omits the <code>thinking</code> capability, so <code>/think high</code> no longer rejects <code>ollama/deepseek-v4-*:cloud</code>.</li>
<li>ACPX/Claude ACP: keep foreground prompts waiting for their own result when autonomous task-notification results arrive during the same session, and retarget the patch for Claude Agent ACP <code>0.33.1</code>.</li>
<li>WhatsApp: keep Baileys media uploads from passing non-Dispatcher agents to undici in <code>7.0.0-rc10</code>, and patch the bundled Baileys declaration so the latest tsdown build stays warning-clean.</li>
<li>Build: keep tsdown <code>0.22.0</code> warning-clean by externalizing known third-party declaration edges and replacing relative channel config module augmentations with explicit built-in channel fields.</li>
<li>ACP sessions: map canonical runtime options to backend-advertised ACP config keys like Claude's <code>effort</code> while keeping persisted OpenClaw state canonical. (#79926) Thanks @InTheCloudDan.</li>
<li>Models/Discord: support <code>provider/*</code> entries in <code>agents.defaults.models</code> so <code>/model</code>, <code>/models</code>, and model pickers can show dynamically discovered models for selected providers without exact model allowlists. Fixes #79485. Thanks @rendrag-git.</li>
<li>Gateway/watch: rebuild or restage missing bundled-plugin dist and runtime-postbuild outputs before launching the Gateway from a source checkout, preventing incomplete watch-mode runtime trees. (#70805) Thanks @rubencu.</li>
<li>CLI/update: allow restart health probes from the previous gateway protocol during self-update, and make plugin dry-runs report exact npm target versions instead of <code>unknown</code> while preserving unchanged status.</li>
<li>OpenAI/Codex: forward persisted <code>openai-codex</code> OAuth profile metadata into Codex plugin harness attempts after canonical <code>openai/*</code> migration, so OAuth-only installs keep using native Codex auth instead of falling through to direct OpenAI API-key auth. Fixes #79978.</li>
<li>OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical <code>openai/gpt-5.5</code> plus Codex OAuth route, and fix trajectory export errors so they suggest the valid <code>openclaw sessions</code> command.</li>
<li>Google/Gemini: normalize retired <code>google/gemini-3-pro-preview</code> primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids for explicit OpenAI-compatible Google and Gemini CLI provider configs, so emitted config targets <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids preserved from existing merged models.json providers so config emission keeps targeting <code>google/gemini-3.1-pro-preview</code>.</li>
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside provider auth config patches so setup-emitted provider catalogs test <code>google/gemini-3.1-pro-preview</code>.</li>
<li>GitHub Copilot: mint short-lived Copilot API tokens with the same <code>vscode-chat</code> integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the <code>copilot-language-server</code> scope. Fixes #79946, #80074. Thanks @TurboTheTurtle.</li>
<li>Plugins/doctor: drop stale managed npm install records when <code>openclaw doctor --fix</code> removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.</li>
<li>Doctor: warn when a per-agent model config omits the <code>fallbacks</code> key and <code>agents.defaults.model.fallbacks</code> is non-empty. Covers both string-form (<code>"model": "..."</code>) and partial-object form (<code>"model": { "primary": "..." }</code>) — both silently clobber the defaults chain at runtime. Use <code>"fallbacks": []</code> to explicitly opt out of fallbacks, or add <code>"fallbacks": [...]</code> to inherit or override. Fixes #79369. Thanks @Kaspre.</li>
<li>Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.</li>
<li>Discord/voice: skip likely incomplete realtime forced-consult transcript fragments and non-actionable closings so stale partial speech does not queue delayed answers over the next turn.</li>
<li>Discord/voice: keep realtime forced consults from clearing active exact-speech playback, so back-to-back voice answers queue instead of cutting each other off.</li>
<li>Discord/voice: synthesize realtime playback timestamps from emitted Discord PCM so OpenAI realtime barge-in truncation no longer sees <code>audioEndMs=0</code> and skips legitimate interruptions.</li>
<li>Plugin SDK: keep activated linked plugin runtime facades loadable when bundled plugin fallback is disabled. Thanks @shakkernerd.</li>
<li>Feishu: auto-thread <code>message(action="send")</code> replies inside the topic when the active session is group_topic or group_topic_sender, and propagate <code>replyInThread</code> through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.</li>
<li>WhatsApp: pass routing context into voice-note transcript echo preflight so echoed transcripts can deliver to the originating chat. Fixes #79778. (#79788) Thanks @hclsys.</li>
<li>Cron/failover: classify structured OpenAI-compatible <code>server_error</code> payloads as <code>server_error</code>, expose that reason in cron state, and let one-shot cron retry policy honor <code>retryOn: ["server_error"]</code> without requiring raw <code>5xx</code> text. (#45594) Thanks @clovericbot.</li>
<li>Slack: wake the resolved thread session after interactive reply button/select clicks and carry Slack delivery context through the queued interaction event, so clicks continue the visible conversation. Fixes #79676 and #61502. (#79836) Thanks @velvet-shark, @tianxiaochannel-oss88, and @Saicheg.</li>
<li>WhatsApp/streaming: send only the new suffix when text-end block replies repeat prior preambles across tool-call cycles, preventing cumulative WhatsApp preamble messages. Fixes #78946. (#79120) Thanks @brokemac79 and @papawattu.</li>
<li>Tests/security audit: sandbox <code>audit-exec-surface.test.ts</code> under a per-case OpenClaw home tempdir, redirecting <code>OPENCLAW_HOME</code> (which wins over <code>HOME</code>/<code>USERPROFILE</code> in <code>resolveRawHomeDir</code>) alongside <code>HOME</code> and <code>USERPROFILE</code>, so its <code>saveExecApprovals(...)</code> calls never touch the live <code>~/.openclaw/exec-approvals.json</code> on the host running the suite. Sibling exec-approvals tests already used the tempdir pattern; this file did not, so running <code>pnpm test</code> against a contributor's local checkout was silently truncating their real approvals to <code>{ "version": 1, "agents": {} }</code>. (#79885) Thanks @omarshahine.</li>
<li>ACP/gateway: preserve <code>AcpRuntimeError</code> cause chain (code/method/JSON-RPC detail) through the lifecycle boundary so gateway logs, telegram replies, and tool-result text show the actual upstream failure instead of opaque <code>Internal error</code>/<code>[object Object]</code>, with redaction applied before the chain reaches log or reply surfaces.</li>
<li>Channels/iMessage: wire <code>action: "reply"</code> attachments through <code>imsg send-rich --file</code> when the installed imsg build advertises that capability (probed once via <code>imsg send-rich --help</code> and cached on the private-API status). Reply now hydrates <code>media</code>/<code>mediaUrl</code>/<code>fileUrl</code>/<code>mediaUrls[0]</code>/<code>filePath</code>/<code>path</code>/base64 <code>buffer</code>+<code>filename</code> through the shared outbound resolver, stages buffers via the existing <code>withTempFile</code> helper, rejects <code>http(s)://</code> URL attachments with a targeted error pointing callers at <code>send</code>'s full attachment-resolver pipeline, and falls back to the explicit <code>imsg#114 not landed yet</code> error on older imsg builds. Depends on the upstream <code>openclaw/imsg#114</code> capability landing in an installable release; until then the new path stays gated and users see the same explicit fallback <code>#79822</code> introduced. (#79864) Thanks @omarshahine.</li>
<li>Telegram: preserve the first-preview debounce while appending true partial-stream deltas, so edited draft previews no longer duplicate earlier text when providers emit incremental output. (#80045) Thanks @TurboTheTurtle.</li>
<li>Agents/Anthropic: report 1M session context for Claude Opus/Sonnet 4 models even when local model config still advertises 200k, matching model discovery and preventing premature status/UI overflow. Fixes #66766.</li>
<li>Models/OpenRouter: hide missing-auth direct provider rows in <code>/model status</code> when they are only duplicated by a nested OpenRouter model id such as <code>openrouter/google/...</code>, while preserving explicitly configured direct providers. Fixes #62317.</li>
<li>Models: preserve an explicitly selected provider/model such as <code>opencode-go/deepseek-v4-pro</code> when another provider owns the same bare model alias. Fixes #79325.</li>
<li>Models/config: explain missing <code>models.providers.<provider>.models[]</code> registration when a model exists only in <code>agents.defaults.models</code>, instead of returning a bare unknown-model error. Fixes #80089.</li>
<li>MCP/tools: prefix bundle MCP server/tool fragments that would start with digits, keeping generated tool names valid for Moonshot/Kimi and other strict providers. Fixes #79179.</li>
<li>Models/OpenRouter: treat <code>403 API key budget limit exceeded</code> as billing so model fallback advances instead of retrying the exhausted primary. Fixes #60191. Thanks @omgitsgela.</li>
<li>Models/OpenRouter: repair stale session overrides that lost the outer <code>openrouter/</code> provider wrapper, so sessions return to the configured OpenRouter model instead of failing as an unknown direct-provider model. Fixes #78161. Thanks @hjamal7-bit.</li>
<li>Google/Gemini: default API-key onboarding back to <code>google/gemini-3.1-pro-preview</code> so fresh Gemini test configs exercise Gemini 3.1 Pro Preview.</li>
<li>Telegram: show full provider/model labels for nested OpenRouter model ids in the model picker, so <code>openrouter/openai/gpt-5.4-mini</code> no longer displays as <code>openai/gpt-5.4-mini</code>. Fixes #67792. (#72752) Thanks @iot2edge.</li>
<li>Models/OpenRouter: preserve live <code>supported_parameters</code> tool support metadata so non-tool Perplexity Sonar models no longer receive agent tool payloads and fall back unnecessarily. Fixes #64175. Thanks @Catfish-75.</li>
<li>Models/OpenRouter: add MoonshotAI Kimi K2.5 to the bundled OpenRouter catalog so onboarding/model pickers can offer it without waiting for live discovery. Fixes #14601.</li>
<li>Models/OpenRouter: keep keyRef/tokenRef-backed auth profiles visible to read-only PI model discovery, so OpenRouter models stay available in model pickers without storing plaintext keys. Fixes #58106. Thanks @ThalynLabs.</li>
<li>Models/list: include explicit configured provider rows and read-only auth-backed catalog rows in the default configured view without loading PI's full registry, keeping Control UI pickers aligned with usable model auth. Refs #79381. Thanks @ismael-81.</li>
<li>Security/audit: honor <code>tools.byProvider["provider/model"].deny</code> when reporting small-model web/browser exposure, so per-model OpenRouter mitigations clear the <code>models.small_params</code> exposure signal. Fixes #80118.</li>
<li>Models/Moonshot: accept direct <code>moonshotai/...</code> and <code>moonshot-ai/...</code> refs as aliases for canonical <code>moonshot/...</code>, so copied OpenRouter Kimi ids no longer fail as unknown direct models. Fixes #73876. (#74946) Thanks @jeffrey701.</li>
<li>Kimi Code: use Kimi's stable <code>kimi-for-coding</code> API model id in bundled catalog, onboarding, and docs while normalizing legacy <code>kimi-code</code> and <code>k2p5</code> refs. Fixes #79965.</li>
<li>Telegram: render cached reply targets and nearby group chatter as one selected conversation context window, so stale replies no longer split JSON reply chains from local chat context.</li>
<li>Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with <code>minLength</code>. Fixes #38817.</li>
<li>DeepSeek: backfill V4 <code>reasoning_content</code> replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608.</li>
<li>iMessage: emit a WARN log when an action is blocked because the imsg private API bridge is not attached, so operators see the silent-drop in <code>~/.openclaw/logs/openclaw.log</code> instead of having to read per-session trajectory JSONL <code>tool.result</code> payloads. Common after a gateway restart un-injects the dylib from Messages.app. (#80035) Thanks @omarshahine.</li>
<li>Codex: cross-fill missing <code>thread.id</code> and <code>thread.sessionId</code> before schema validation so live Codex app-server responses that omit <code>sessionId</code> no longer fail <code>thread/start</code> or <code>thread/resume</code>. Fixes #80124. (#80137) Thanks @kagura-agent.</li>
<li>Agents/Pi: wait for embedded abort cleanup to settle before releasing the session write lock, preventing follow-up turns from racing previous prompt teardown. (#80239) Thanks @samzong.</li>
<li>WhatsApp: downgrade OpenClaw watchdog-triggered Web reconnects from runtime errors to recovery warnings and clear the recovered reconnect status after the next healthy connection. (#77026) Thanks @rubencu.</li>
<li>ACPX/Windows: hide the MCP proxy target child process window on Windows so ACP-backed agents do not flash or fail because of terminal window handling. Fixes #60672. (#60678) Thanks @KChow-ctrl.</li>
<li>Agents: abort generic repeated no-progress tool loops at the critical threshold when identical calls keep returning identical outcomes. (#80668) Thanks @frankekn.</li>
<li>Exec approvals: omit generated command highlights for non-POSIX Windows and shell-wrapper approval commands until those command languages have native highlighting support. (#80566) Thanks @jesse-merhi.</li>
<li>Telegram: keep verbose tool progress and result drafts separate from the final assistant answer so tool output no longer blends into the final Telegram message. (#80294) Thanks @jalehman.</li>
<li>Plugin SDK/Windows: enable the native require fast path for root <code>openclaw/plugin-sdk</code> dist aliases instead of forcing Jiti transforms. (#80878) Thanks @medns.</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.12/OpenClaw-2026.5.12.zip" length="52670462" type="application/octet-stream" sparkle:edSignature="RhckloxLoZhtGQZ+0jrH0qWN3py61an+kiAdgEJY9IZGekTKmJ+DWHXY3ixQJYvKf2WLgxOzaY6Jy27pF+kECw=="/>
</item>
<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>
</channel>
</rss>

View File

@@ -209,15 +209,16 @@ Why these matter:
- Google Play treats SMS and Call Log access as highly restricted. In most cases, Play only allows them for the default SMS app, default Phone app, default Assistant, or a narrow policy exception.
- Review usually involves a `Permissions Declaration Form`, policy justification, and demo video evidence in Play Console.
- If we want a Play-safe build, these should be the first permissions removed behind a dedicated product flavor / variant.
- The Play build removes these behind the `play` flavor.
- Photo library access is also removed from the Play build. Use third-party builds for `photos.latest`.
Current OpenClaw Android implication:
- APK / sideload build can keep SMS and Call Log features.
- Google Play build should exclude SMS send/search and Call Log search unless the product is intentionally positioned and approved as a default-handler exception case.
- APK / sideload build can keep SMS, Call Log, and recent-photo features.
- Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
- The repo now ships this split as Android product flavors:
- `play`: removes `READ_SMS`, `SEND_SMS`, and `READ_CALL_LOG`, and hides SMS / Call Log surfaces in onboarding, settings, and advertised node capabilities.
- `thirdParty`: keeps the full permission set and the existing SMS / Call Log functionality.
- `play`: removes `READ_SMS`, `SEND_SMS`, `READ_CALL_LOG`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VISUAL_USER_SELECTED`, and `READ_EXTERNAL_STORAGE`; hides SMS, Call Log, and Photos surfaces in onboarding, settings, and advertised node capabilities.
- `thirdParty`: keeps the full permission set and the existing SMS / Call Log / Photos functionality.
Policy links:

View File

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

View File

@@ -5,6 +5,7 @@ import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
import ai.openclaw.app.node.CameraCaptureManager
import ai.openclaw.app.node.CanvasController
import ai.openclaw.app.node.SmsManager
@@ -81,6 +82,40 @@ class MainViewModel(
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
val gatewayVersion: StateFlow<String?> = runtimeState(initial = null) { it.gatewayVersion }
val gatewayUpdateAvailable: StateFlow<GatewayUpdateAvailableSummary?> = runtimeState(initial = null) { it.gatewayUpdateAvailable }
val modelCatalog: StateFlow<List<GatewayModelSummary>> = runtimeState(initial = emptyList()) { it.modelCatalog }
val modelAuthProviders: StateFlow<List<GatewayModelProviderSummary>> = runtimeState(initial = emptyList()) { it.modelAuthProviders }
val modelCatalogRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.modelCatalogRefreshing }
val modelCatalogErrorText: StateFlow<String?> = runtimeState(initial = null) { it.modelCatalogErrorText }
val gatewayDefaultAgentId: StateFlow<String?> = runtimeState(initial = null) { it.gatewayDefaultAgentId }
val gatewayAgents: StateFlow<List<GatewayAgentSummary>> = runtimeState(initial = emptyList()) { it.gatewayAgents }
val cronStatus: StateFlow<GatewayCronStatus> = runtimeState(initial = GatewayCronStatus(enabled = false, jobs = 0, nextWakeAtMs = null)) { it.cronStatus }
val cronJobs: StateFlow<List<GatewayCronJobSummary>> = runtimeState(initial = emptyList()) { it.cronJobs }
val cronRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.cronRefreshing }
val cronErrorText: StateFlow<String?> = runtimeState(initial = null) { it.cronErrorText }
val usageSummary: StateFlow<GatewayUsageSummary> = runtimeState(initial = GatewayUsageSummary(updatedAtMs = null, providers = emptyList())) { it.usageSummary }
val usageRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.usageRefreshing }
val usageErrorText: StateFlow<String?> = runtimeState(initial = null) { it.usageErrorText }
val skillsSummary: StateFlow<GatewaySkillsSummary> = runtimeState(initial = GatewaySkillsSummary(skills = emptyList())) { it.skillsSummary }
val skillsRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.skillsRefreshing }
val skillsErrorText: StateFlow<String?> = runtimeState(initial = null) { it.skillsErrorText }
val nodesDevicesSummary: StateFlow<GatewayNodesDevicesSummary> =
runtimeState(initial = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())) { it.nodesDevicesSummary }
val nodesDevicesRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.nodesDevicesRefreshing }
val nodesDevicesErrorText: StateFlow<String?> = runtimeState(initial = null) { it.nodesDevicesErrorText }
val channelsSummary: StateFlow<GatewayChannelsSummary> =
runtimeState(initial = GatewayChannelsSummary(channels = emptyList())) { it.channelsSummary }
val channelsRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.channelsRefreshing }
val channelsErrorText: StateFlow<String?> = runtimeState(initial = null) { it.channelsErrorText }
val dreamingSummary: StateFlow<GatewayDreamingSummary> =
runtimeState(initial = GatewayDreamingSummary()) { it.dreamingSummary }
val dreamingRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.dreamingRefreshing }
val dreamingErrorText: StateFlow<String?> = runtimeState(initial = null) { it.dreamingErrorText }
val healthLogsSummary: StateFlow<GatewayHealthLogsSummary> =
runtimeState(initial = GatewayHealthLogsSummary()) { it.healthLogsSummary }
val healthLogsRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.healthLogsRefreshing }
val healthLogsErrorText: StateFlow<String?> = runtimeState(initial = null) { it.healthLogsErrorText }
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtimeState(initial = null) { it.pendingGatewayTrust }
val seamColorArgb: StateFlow<Long> = runtimeState(initial = 0xFF0EA5E9) { it.seamColorArgb }
val mainSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.mainSessionKey }
@@ -293,6 +328,10 @@ class MainViewModel(
ensureRuntime().setMicEnabled(enabled)
}
fun cancelMicCapture() {
ensureRuntime().cancelMicCapture()
}
fun setTalkModeEnabled(enabled: Boolean) {
ensureRuntime().setTalkModeEnabled(enabled)
}
@@ -355,6 +394,42 @@ class MainViewModel(
ensureRuntime().refreshHomeCanvasOverviewIfConnected()
}
fun refreshModelCatalog() {
ensureRuntime().refreshModelCatalog()
}
fun refreshAgents() {
ensureRuntime().refreshAgents()
}
fun refreshCronJobs() {
ensureRuntime().refreshCronJobs()
}
fun refreshUsage() {
ensureRuntime().refreshUsage()
}
fun refreshSkills() {
ensureRuntime().refreshSkills()
}
fun refreshNodesDevices() {
ensureRuntime().refreshNodesDevices()
}
fun refreshChannels() {
ensureRuntime().refreshChannels()
}
fun refreshDreaming() {
ensureRuntime().refreshDreaming()
}
fun refreshHealthLogs() {
ensureRuntime().refreshHealthLogs()
}
fun loadChat(sessionKey: String) {
ensureRuntime().loadChat(sessionKey)
}

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,7 @@ class ChatController(
private val pendingRuns = mutableSetOf<String>()
private val pendingRunTimeoutJobs = ConcurrentHashMap<String, Job>()
private val optimisticMessagesByRunId = LinkedHashMap<String, ChatMessage>()
private val pendingRunTimeoutMs = 120_000L
private var lastHealthPollAtMs: Long? = null
@@ -76,6 +77,7 @@ class ChatController(
fun load(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
_sessionKey.value = key
optimisticMessagesByRunId.clear()
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
}
@@ -113,6 +115,7 @@ class ChatController(
if (key.isEmpty()) return
if (key == _sessionKey.value) return
_sessionKey.value = key
optimisticMessagesByRunId.clear()
// Keep the thread switch path lean: history + health are needed immediately,
// but the session list is usually unchanged and can refresh on explicit pull-to-refresh.
scope.launch { bootstrap(forceHealth = true, refreshSessions = false) }
@@ -171,14 +174,15 @@ class ChatController(
)
}
}
_messages.value =
_messages.value +
val optimisticMessage =
ChatMessage(
id = UUID.randomUUID().toString(),
role = "user",
content = userContent,
timestampMs = System.currentTimeMillis(),
)
optimisticMessagesByRunId[runId] = optimisticMessage
_messages.value = _messages.value + optimisticMessage
armPendingRunTimeout(runId)
synchronized(pendingRuns) {
@@ -218,6 +222,7 @@ class ChatController(
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
optimisticMessagesByRunId[actualRunId] = optimisticMessagesByRunId.remove(runId) ?: optimisticMessage
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
synchronized(pendingRuns) {
@@ -228,6 +233,7 @@ class ChatController(
true
} catch (err: Throwable) {
clearPendingRun(runId)
removeOptimisticMessage(runId)
_errorText.value = err.message
false
}
@@ -302,7 +308,7 @@ class ChatController(
val historyJson = session.request("chat.history", """{"sessionKey":"$key"}""")
val history = parseHistory(historyJson, sessionKey = key, previousMessages = _messages.value)
_messages.value = history.messages
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
history.thinkingLevel
?.trim()
@@ -369,7 +375,13 @@ class ChatController(
if (state == "error") {
_errorText.value = payload["errorMessage"].asStringOrNull() ?: "Chat failed"
}
if (runId != null) clearPendingRun(runId) else clearPendingRuns()
if (runId != null) {
clearPendingRun(runId)
optimisticMessagesByRunId.remove(runId)
} else {
clearPendingRuns()
optimisticMessagesByRunId.clear()
}
pendingToolCallsById.clear()
publishPendingToolCalls()
_streamingAssistantText.value = null
@@ -378,7 +390,7 @@ class ChatController(
val historyJson =
session.request("chat.history", """{"sessionKey":"${_sessionKey.value}"}""")
val history = parseHistory(historyJson, sessionKey = _sessionKey.value, previousMessages = _messages.value)
_messages.value = history.messages
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
history.thinkingLevel
?.trim()
@@ -471,6 +483,7 @@ class ChatController(
}
if (!stillPending) return@launch
clearPendingRun(runId)
removeOptimisticMessage(runId)
_errorText.value = "Timed out waiting for a reply; try again or refresh."
}
}
@@ -488,12 +501,18 @@ class ChatController(
job.cancel()
}
pendingRunTimeoutJobs.clear()
optimisticMessagesByRunId.clear()
synchronized(pendingRuns) {
pendingRuns.clear()
_pendingRunCount.value = 0
}
}
private fun removeOptimisticMessage(runId: String) {
val message = optimisticMessagesByRunId.remove(runId) ?: return
_messages.value = _messages.value.filterNot { it.id == message.id }
}
private fun parseHistory(
historyJson: String,
sessionKey: String,
@@ -620,11 +639,54 @@ internal fun reconcileMessageIds(
}
}
internal fun mergeOptimisticMessages(
incoming: List<ChatMessage>,
optimistic: Collection<ChatMessage>,
): List<ChatMessage> {
if (optimistic.isEmpty()) return incoming
val unmatchedIncoming = incoming.toMutableList()
val missingOptimistic =
optimistic.filter { message ->
val matchIndex =
unmatchedIncoming.indexOfFirst { incomingMessage ->
incomingMessageConsumesOptimistic(incomingMessage, message)
}
if (matchIndex >= 0) {
unmatchedIncoming.removeAt(matchIndex)
false
} else {
true
}
}
if (missingOptimistic.isEmpty()) return incoming
return (incoming + missingOptimistic).sortedWith(compareBy<ChatMessage> { it.timestampMs ?: Long.MAX_VALUE }.thenBy { it.id })
}
internal fun messageIdentityKey(message: ChatMessage): String? {
val contentKey = messageContentIdentityKey(message) ?: return null
val timestamp = message.timestampMs?.toString().orEmpty()
if (timestamp.isEmpty() && contentKey.isEmpty()) return null
return listOf(contentKey, timestamp).joinToString(separator = "|")
}
private fun optimisticMessageIdentityKey(message: ChatMessage): String? = messageContentIdentityKey(message)
private fun incomingMessageConsumesOptimistic(
incoming: ChatMessage,
optimistic: ChatMessage,
): Boolean {
if (optimisticMessageIdentityKey(incoming) != optimisticMessageIdentityKey(optimistic)) return false
val incomingTimestamp = incoming.timestampMs ?: return false
val optimisticTimestamp = optimistic.timestampMs ?: return true
return incomingTimestamp >= optimisticTimestamp
}
private fun messageContentIdentityKey(message: ChatMessage): String? {
val role = message.role.trim().lowercase()
if (role.isEmpty()) return null
val timestamp = message.timestampMs?.toString().orEmpty()
val contentFingerprint =
message.content.joinToString(separator = "\u001E") { part ->
listOf(
@@ -642,8 +704,7 @@ internal fun messageIdentityKey(message: ChatMessage): String? {
).joinToString(separator = "\u001F")
}
if (timestamp.isEmpty() && contentFingerprint.isEmpty()) return null
return listOf(role, timestamp, contentFingerprint).joinToString(separator = "|")
return listOf(role, contentFingerprint).joinToString(separator = "|")
}
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject

View File

@@ -68,6 +68,20 @@ data class GatewayConnectErrorDetails(
val reason: String? = null,
)
data class GatewayHelloSummary(
val serverName: String?,
val remoteAddress: String?,
val serverVersion: String?,
val mainSessionKey: String?,
val updateAvailable: GatewayUpdateAvailableSummary?,
)
data class GatewayUpdateAvailableSummary(
val currentVersion: String?,
val latestVersion: String?,
val channel: String?,
)
private data class SelectedConnectAuth(
val authToken: String?,
val authBootstrapToken: String?,
@@ -86,7 +100,7 @@ class GatewaySession(
private val scope: CoroutineScope,
private val identityStore: DeviceIdentityStore,
private val deviceAuthStore: DeviceAuthTokenStore,
private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit,
private val onConnected: (GatewayHelloSummary) -> Unit,
private val onDisconnected: (message: String) -> Unit,
private val onEvent: (event: String, payloadJson: String?) -> Unit,
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null,
@@ -604,8 +618,8 @@ class GatewaySession(
val allowedOperatorScopes =
setOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write",
)
scopes.filter { allowedOperatorScopes.contains(it) }.distinct().sorted()
@@ -648,7 +662,9 @@ class GatewaySession(
pendingDeviceTokenRetry = false
deviceTokenRetryBudgetUsed = false
reconnectPausedForAuthFailure = false
val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull()
val server = obj["server"].asObjectOrNull()
val serverName = server?.get("host").asStringOrNull()
val serverVersion = server?.get("version").asStringOrNull()
val authObj = obj["auth"].asObjectOrNull()
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
@@ -686,13 +702,33 @@ class GatewaySession(
?.let { normalized -> surface to normalized }
} ?: emptyList()
pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap()
val snapshot = obj["snapshot"].asObjectOrNull()
val sessionDefaults =
obj["snapshot"]
.asObjectOrNull()
snapshot
?.get("sessionDefaults")
.asObjectOrNull()
mainSessionKey = sessionDefaults?.get("mainSessionKey").asStringOrNull()
onConnected(serverName, remoteAddress, mainSessionKey)
onConnected(
GatewayHelloSummary(
serverName = serverName,
remoteAddress = remoteAddress,
serverVersion = serverVersion,
mainSessionKey = mainSessionKey,
updateAvailable = parseUpdateAvailable(snapshot?.get("updateAvailable").asObjectOrNull()),
),
)
}
private fun parseUpdateAvailable(value: JsonObject?): GatewayUpdateAvailableSummary? {
if (value == null) return null
val latestVersion = value["latestVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
val currentVersion = value["currentVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
val channel = value["channel"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
return GatewayUpdateAvailableSummary(
currentVersion = currentVersion,
latestVersion = latestVersion,
channel = channel,
)
}
private fun buildConnectParams(

View File

@@ -22,6 +22,7 @@ class ConnectionManager(
private val readSmsAvailable: () -> Boolean,
private val smsSearchPossible: () -> Boolean,
private val callLogAvailable: () -> Boolean,
private val photosAvailable: () -> Boolean,
private val hasRecordAudioPermission: () -> Boolean,
private val manualTls: () -> Boolean,
) {
@@ -96,6 +97,7 @@ class ConnectionManager(
readSmsAvailable = readSmsAvailable(),
smsSearchPossible = smsSearchPossible(),
callLogAvailable = callLogAvailable(),
photosAvailable = photosAvailable(),
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
motionActivityAvailable = motionActivityAvailable(),
motionPedometerAvailable = motionPedometerAvailable(),
@@ -160,7 +162,15 @@ class ConnectionManager(
fun buildOperatorConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "operator",
scopes = listOf("operator.read", "operator.write", "operator.talk.secrets"),
// QR bootstrap hands Android a bounded operator token that includes approvals; keep the
// default operator reconnect request aligned so the post-bootstrap loop can approve work.
scopes =
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),
caps = emptyList(),
commands = emptyList(),
permissions = emptyMap(),

View File

@@ -28,6 +28,7 @@ class DeviceHandler(
private val appContext: Context,
private val smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
private val callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
private val photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
) {
companion object {
internal fun hasAnySmsCapability(
@@ -150,7 +151,9 @@ class DeviceHandler(
val smsReadGranted = hasPermission(Manifest.permission.READ_SMS)
val notificationAccess = DeviceNotificationListenerService.isAccessEnabled(appContext)
val photosGranted =
if (Build.VERSION.SDK_INT >= 33) {
if (!photosEnabled) {
false
} else if (Build.VERSION.SDK_INT >= 33) {
hasPermission(Manifest.permission.READ_MEDIA_IMAGES)
} else {
hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
@@ -248,7 +251,7 @@ class DeviceHandler(
"photos",
permissionStateJson(
granted = photosGranted,
promptableWhenDenied = true,
promptableWhenDenied = photosEnabled,
),
)
put(

View File

@@ -23,6 +23,7 @@ data class NodeRuntimeFlags(
val readSmsAvailable: Boolean,
val smsSearchPossible: Boolean,
val callLogAvailable: Boolean,
val photosAvailable: Boolean,
val voiceWakeEnabled: Boolean,
val motionActivityAvailable: Boolean,
val motionPedometerAvailable: Boolean,
@@ -37,6 +38,7 @@ enum class InvokeCommandAvailability {
ReadSmsAvailable,
RequestableSmsSearchAvailable,
CallLogAvailable,
PhotosAvailable,
MotionActivityAvailable,
MotionPedometerAvailable,
DebugBuild,
@@ -48,6 +50,7 @@ enum class NodeCapabilityAvailability {
LocationEnabled,
SmsAvailable,
CallLogAvailable,
PhotosAvailable,
VoiceWakeEnabled,
MotionAvailable,
}
@@ -87,7 +90,10 @@ object InvokeCommandRegistry {
name = OpenClawCapability.Location.rawValue,
availability = NodeCapabilityAvailability.LocationEnabled,
),
NodeCapabilitySpec(name = OpenClawCapability.Photos.rawValue),
NodeCapabilitySpec(
name = OpenClawCapability.Photos.rawValue,
availability = NodeCapabilityAvailability.PhotosAvailable,
),
NodeCapabilitySpec(name = OpenClawCapability.Contacts.rawValue),
NodeCapabilitySpec(name = OpenClawCapability.Calendar.rawValue),
NodeCapabilitySpec(
@@ -188,6 +194,7 @@ object InvokeCommandRegistry {
),
InvokeCommandSpec(
name = OpenClawPhotosCommand.Latest.rawValue,
availability = InvokeCommandAvailability.PhotosAvailable,
),
InvokeCommandSpec(
name = OpenClawContactsCommand.Search.rawValue,
@@ -244,6 +251,7 @@ object InvokeCommandRegistry {
NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled
NodeCapabilityAvailability.SmsAvailable -> flags.sendSmsAvailable || flags.readSmsAvailable
NodeCapabilityAvailability.CallLogAvailable -> flags.callLogAvailable
NodeCapabilityAvailability.PhotosAvailable -> flags.photosAvailable
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
NodeCapabilityAvailability.MotionAvailable -> flags.motionActivityAvailable || flags.motionPedometerAvailable
}
@@ -260,6 +268,7 @@ object InvokeCommandRegistry {
InvokeCommandAvailability.ReadSmsAvailable -> flags.readSmsAvailable
InvokeCommandAvailability.RequestableSmsSearchAvailable -> flags.smsSearchPossible
InvokeCommandAvailability.CallLogAvailable -> flags.callLogAvailable
InvokeCommandAvailability.PhotosAvailable -> flags.photosAvailable
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
InvokeCommandAvailability.DebugBuild -> flags.debugBuild

View File

@@ -77,6 +77,7 @@ class InvokeDispatcher(
private val smsFeatureEnabled: () -> Boolean,
private val smsTelephonyAvailable: () -> Boolean,
private val callLogAvailable: () -> Boolean,
private val photosAvailable: () -> Boolean,
private val debugBuild: () -> Boolean,
private val onCanvasA2uiPush: () -> Unit,
private val onCanvasA2uiReset: () -> Unit,
@@ -325,6 +326,15 @@ class InvokeDispatcher(
message = "CALL_LOG_UNAVAILABLE: call log not available on this build",
)
}
InvokeCommandAvailability.PhotosAvailable ->
if (photosAvailable()) {
null
} else {
GatewaySession.InvokeResult.error(
code = "PHOTOS_UNAVAILABLE",
message = "PHOTOS_UNAVAILABLE: photos not available on this build",
)
}
InvokeCommandAvailability.DebugBuild ->
if (debugBuild()) {
null

View File

@@ -0,0 +1,139 @@
package ai.openclaw.app.ui
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPrimaryButton
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ScreenShare
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
internal fun CanvasSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val isConnected by viewModel.isConnected.collectAsState()
val currentUrl by viewModel.canvasCurrentUrl.collectAsState()
val hydrated by viewModel.canvasA2uiHydrated.collectAsState()
val rehydratePending by viewModel.canvasRehydratePending.collectAsState()
val rehydrateErrorText by viewModel.canvasRehydrateErrorText.collectAsState()
val hasLivePage = currentUrl?.isNotBlank() == true
val showCanvasSurface = isConnected
val canvasLabel = if (hasLivePage) "Live page" else "Home canvas"
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshHomeCanvasOverviewIfConnected()
}
}
SettingsDetailFrame(
title = "Canvas",
subtitle = "Current screen output and interactive app surface.",
icon = Icons.AutoMirrored.Filled.ScreenShare,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Connection", if (isConnected) "Online" else "Offline"),
SettingsMetric("Surface", canvasLabel),
SettingsMetric("Bridge", if (hasLivePage && hydrated) "Ready" else "Standby"),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawPrimaryButton(
text = if (rehydratePending) "Refreshing" else "Refresh Screen",
onClick = { viewModel.requestCanvasRehydrate(source = "settings_canvas") },
enabled = isConnected && !rehydratePending,
modifier = Modifier.weight(1f),
)
ClawSecondaryButton(
text = "Reconnect",
onClick = viewModel::refreshGatewayConnection,
modifier = Modifier.weight(1f),
)
}
rehydrateErrorText?.let {
ClawPanel {
Text(text = it, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
ClawPanel(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = canvasLabel, style = ClawTheme.type.section, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Surface(
modifier = Modifier.fillMaxWidth().height(520.dp).clip(RoundedCornerShape(ClawTheme.radii.panel)),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box {
if (showCanvasSurface) {
CanvasScreen(viewModel = viewModel, visible = true, modifier = Modifier.fillMaxWidth().height(520.dp))
} else {
CanvasStandbyPanel(isConnected = isConnected)
}
}
}
}
}
}
}
@Composable
private fun CanvasStandbyPanel(isConnected: Boolean) {
Column(
modifier = Modifier.fillMaxWidth().height(520.dp).padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Surface(
modifier = Modifier.size(54.dp),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.AutoMirrored.Filled.ScreenShare, contentDescription = null, modifier = Modifier.size(26.dp))
}
}
Text(
text = if (isConnected) "Screen surface ready" else "Connect the gateway",
style = ClawTheme.type.title,
color = ClawTheme.colors.text,
modifier = Modifier.padding(top = 18.dp),
)
Text(
text = if (isConnected) "Canvas output appears here when OpenClaw opens an app surface." else "Canvas output needs an active gateway connection.",
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
modifier = Modifier.padding(top = 6.dp),
)
}
}

View File

@@ -0,0 +1,159 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayChannelSummary
import ai.openclaw.app.GatewayChannelsSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawDetailRow
import ai.openclaw.app.ui.design.ClawListPanel
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTextBadge
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun ChannelsSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val summary by viewModel.channelsSummary.collectAsState()
val refreshing by viewModel.channelsRefreshing.collectAsState()
val errorText by viewModel.channelsErrorText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
val channels = summary.channels
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshChannels()
}
}
SettingsDetailFrame(
title = "Channels",
subtitle = "Messaging surfaces connected to this gateway.",
icon = Icons.Default.Notifications,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Channels", channels.size.toString()),
SettingsMetric("Connected", channels.count { it.connected }.toString()),
SettingsMetric("Configured", channels.count { it.configured }.toString()),
SettingsMetric("Issues", channels.count { it.error != null }.toString()),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(
text = if (refreshing) "Refreshing" else "Refresh",
onClick = viewModel::refreshChannels,
enabled = isConnected && !refreshing,
modifier = Modifier.weight(1f),
)
}
errorText?.let { error ->
ClawPanel {
Text(text = error, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
if (summary.partial || summary.warnings.isNotEmpty()) {
ClawPanel {
Text(text = channelsWarningText(summary), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
when {
!isConnected ->
ClawPanel {
Text(text = "Connect the gateway to load channels.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
channels.isEmpty() ->
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "No channels found.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Telegram, WhatsApp, email, and other channels appear here after setup.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
else -> ChannelsPanel(channels = channels)
}
}
}
@Composable
private fun ChannelsPanel(channels: List<GatewayChannelSummary>) {
ClawListPanel(items = channels) { channel ->
ChannelRow(channel = channel)
}
}
@Composable
private fun ChannelRow(channel: GatewayChannelSummary) {
ClawDetailRow(
title = channel.label,
subtitle = channelSubtitle(channel),
leading = { ClawTextBadge(text = channelBadge(channel.label)) },
trailing = { ClawStatusPill(text = channelStatusText(channel), status = channelStatus(channel)) },
)
}
private fun channelSubtitle(channel: GatewayChannelSummary): String {
val accounts =
when (channel.accountCount) {
0 -> null
1 -> "1 account"
else -> "${channel.accountCount} accounts"
}
val lifecycle =
when {
channel.connected -> "Connected"
channel.running -> "Running"
channel.linked -> "Linked"
channel.configured -> "Configured"
channel.enabled -> "Enabled"
else -> "Off"
}
return listOfNotNull(accounts, lifecycle, channel.error).joinToString(" · ")
}
private fun channelStatusText(channel: GatewayChannelSummary): String =
when {
channel.error != null -> "Issue"
channel.connected -> "Connected"
channel.running -> "Running"
channel.linked || channel.configured -> "Ready"
channel.enabled -> "Setup"
else -> "Off"
}
private fun channelStatus(channel: GatewayChannelSummary): ClawStatus =
when {
channel.error != null -> ClawStatus.Danger
channel.connected || channel.running -> ClawStatus.Success
channel.linked || channel.configured -> ClawStatus.Neutral
channel.enabled -> ClawStatus.Warning
else -> ClawStatus.Neutral
}
private fun channelBadge(label: String): String =
label
.split(' ', '-', '_')
.filter { it.isNotBlank() }
.take(2)
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
.joinToString("")
.ifBlank { "C" }
private fun channelsWarningText(summary: GatewayChannelsSummary): String = summary.warnings.firstOrNull()?.takeIf { it.isNotBlank() } ?: "Some channel status checks did not complete."

View File

@@ -0,0 +1,320 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayModelProviderSummary
import ai.openclaw.app.GatewayModelSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawEmptyState
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawScaffold
import ai.openclaw.app.ui.design.ClawSeparatedColumn
import ai.openclaw.app.ui.design.ClawTextField
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.outlined.AccessTime
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material.icons.outlined.MicNone
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
internal fun CommandPalette(
viewModel: MainViewModel,
onDismiss: () -> Unit,
onOpenChat: () -> Unit,
onOpenVoice: () -> Unit,
onOpenSessions: () -> Unit,
onOpenProviders: () -> Unit,
onOpenSettings: () -> Unit,
onOpenSession: (String) -> Unit,
) {
val isConnected by viewModel.isConnected.collectAsState()
val sessions by viewModel.chatSessions.collectAsState()
val models by viewModel.modelCatalog.collectAsState()
val providers by viewModel.modelAuthProviders.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
var query by rememberSaveable { mutableStateOf("") }
val normalizedQuery = query.trim().lowercase()
val quickActions =
listOf(
CommandItem("Open Chat", "Start or continue a conversation", Icons.Outlined.ChatBubbleOutline, onOpenChat),
CommandItem("Start Voice", "Talk or dictate with OpenClaw", Icons.Outlined.MicNone, onOpenVoice),
CommandItem("Browse Sessions", "Find previous conversations", Icons.Outlined.AccessTime, onOpenSessions),
CommandItem("Providers & Models", providerCommandSubtitle(isConnected, providers, models), Icons.Outlined.Inventory2, onOpenProviders),
CommandItem("Settings", "Gateway, voice, notifications, privacy", Icons.Outlined.Settings, onOpenSettings),
)
val actionRows = quickActions.filter { it.matches(normalizedQuery) }
val sessionRows =
sessions
.filter { session ->
val title = commandSessionTitle(session.displayName)
normalizedQuery.isEmpty() || title.lowercase().contains(normalizedQuery)
}.take(5)
Surface(modifier = Modifier.fillMaxSize(), color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
CommandIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search", onClick = onDismiss)
Text(text = "Search", style = ClawTheme.type.title, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
CommandAvatar(text = "OC")
}
}
item {
ClawTextField(value = query, onValueChange = { query = it }, placeholder = "Search OpenClaw")
}
item {
CommandSectionLabel(title = "Quick actions")
}
if (actionRows.isEmpty()) {
item {
ClawEmptyState(title = "No actions found", body = "Try Chat, Voice, Sessions, Providers, or Settings.")
}
} else {
item {
CommandActionList(rows = actionRows)
}
}
item {
CommandSectionLabel(title = "Sessions")
}
if (sessionRows.isEmpty()) {
item {
ClawPanel {
Text(
text = if (isConnected) "No matching sessions yet." else "Connect the Gateway to search sessions.",
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
}
}
} else {
item {
CommandSessionList(
rows =
sessionRows.map { session ->
CommandSessionRow(
key = session.key,
title = commandSessionTitle(session.displayName),
subtitle = if (pendingRunCount > 0) "Assistant working" else "OpenClaw session",
metadata = session.updatedAtMs?.let(::commandRelativeTime) ?: "now",
)
},
onOpen = onOpenSession,
)
}
}
}
}
}
}
private data class CommandItem(
val title: String,
val subtitle: String,
val icon: ImageVector,
val onClick: () -> Unit,
) {
fun matches(query: String): Boolean = query.isEmpty() || title.lowercase().contains(query) || subtitle.lowercase().contains(query)
}
private data class CommandSessionRow(
val key: String,
val title: String,
val subtitle: String,
val metadata: String,
)
@Composable
private fun CommandActionList(rows: List<CommandItem>) {
ClawPanel(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)) {
ClawSeparatedColumn(items = rows) { row ->
CommandActionRow(row = row)
}
}
}
@Composable
private fun CommandActionRow(row: CommandItem) {
Surface(color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier =
Modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable(onClick = row.onClick)
.padding(horizontal = 2.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(19.dp), tint = ClawTheme.colors.text)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = row.subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open ${row.title}",
modifier = Modifier.size(17.dp),
tint = ClawTheme.colors.textMuted,
)
}
}
}
@Composable
private fun CommandSessionList(
rows: List<CommandSessionRow>,
onOpen: (String) -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)) {
ClawSeparatedColumn(items = rows) { row ->
CommandSessionListRow(row = row, onClick = { onOpen(row.key) })
}
}
}
@Composable
private fun CommandSessionListRow(
row: CommandSessionRow,
onClick: () -> Unit,
) {
Surface(color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
Row(
modifier =
Modifier
.fillMaxWidth()
.heightIn(min = 58.dp)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable(onClick = onClick)
.padding(horizontal = 2.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Surface(
modifier = Modifier.size(30.dp),
shape = CircleShape,
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(15.dp), tint = ClawTheme.colors.text)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = row.subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 1)
}
Text(text = row.metadata, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open session",
modifier = Modifier.size(17.dp),
tint = ClawTheme.colors.textMuted,
)
}
}
}
@Composable
private fun CommandIconButton(
icon: ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
}
}
}
@Composable
private fun CommandAvatar(text: String) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Text(text = text.take(2).uppercase(), style = ClawTheme.type.label)
}
}
}
@Composable
private fun CommandSectionLabel(title: String) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = title.uppercase(), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
}
}
private fun providerCommandSubtitle(
isConnected: Boolean,
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): String {
if (!isConnected) return "Connect Gateway to load models"
val readyProviderCount = providers.count { modelProviderReady(it.status) }
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
if (models.isNotEmpty()) return "${models.size} models available"
return "Configure model access"
}
private fun commandSessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: "Main session"
private fun commandRelativeTime(updatedAtMs: Long): String {
val deltaMs = (System.currentTimeMillis() - updatedAtMs).coerceAtLeast(0L)
val minutes = deltaMs / 60_000L
if (minutes < 1) return "now"
if (minutes < 60) return "${minutes}m"
val hours = minutes / 60
if (hours < 24) return "${hours}h"
return "${hours / 24}d"
}

View File

@@ -0,0 +1,200 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayDreamDiaryEntry
import ai.openclaw.app.GatewayDreamingSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
internal fun DreamingSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val summary by viewModel.dreamingSummary.collectAsState()
val refreshing by viewModel.dreamingRefreshing.collectAsState()
val errorText by viewModel.dreamingErrorText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshDreaming()
}
}
SettingsDetailFrame(
title = "Dreaming",
subtitle = "Memory consolidation and dream diary.",
icon = Icons.Default.Storage,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Status", if (summary.enabled) "On" else "Off"),
SettingsMetric("Waiting", summary.shortTermCount.toString()),
SettingsMetric("Signals", summary.totalSignalCount.toString()),
SettingsMetric("Next Cycle", formatDreamingNextRun(summary.nextRunAtMs)),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(
text = if (refreshing) "Refreshing" else "Refresh",
onClick = viewModel::refreshDreaming,
enabled = isConnected && !refreshing,
modifier = Modifier.weight(1f),
)
}
errorText?.let { error ->
ClawPanel {
Text(text = error, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
when {
!isConnected ->
ClawPanel {
Text(text = "Connect the gateway to load dreaming.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
else -> DreamingPanel(summary = summary)
}
}
}
@Composable
private fun DreamingPanel(summary: GatewayDreamingSummary) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
DreamingHealthRow(
title = "Memory Store",
value = if (summary.storeHealthy) "Healthy" else "Needs attention",
healthy = summary.storeHealthy,
)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
DreamingHealthRow(
title = "Signal Index",
value = if (summary.phaseSignalHealthy) "Healthy" else "Needs attention",
healthy = summary.phaseSignalHealthy,
)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
DreamingHealthRow(
title = "Promoted",
value = "${summary.promotedToday} today · ${summary.promotedTotal} total",
healthy = true,
)
}
}
DreamDiaryPanel(summary = summary)
}
}
@Composable
private fun DreamingHealthRow(
title: String,
value: String,
healthy: Boolean,
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Box(modifier = Modifier.size(7.dp))
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
ClawStatusPill(text = value, status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
}
}
@Composable
private fun DreamDiaryPanel(summary: GatewayDreamingSummary) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = "DIARY", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
if (!summary.diaryFound) {
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "No dream diary yet.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Entries appear after a dreaming cycle writes a narrative summary.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
return
}
if (summary.diaryEntries.isEmpty()) {
ClawPanel {
Text(text = "The diary is waiting for its first entry.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
return
}
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
summary.diaryEntries.forEachIndexed { index, entry ->
DreamDiaryRow(entry = entry)
if (index != summary.diaryEntries.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
}
@Composable
private fun DreamDiaryRow(entry: GatewayDreamDiaryEntry) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Surface(
modifier = Modifier.size(30.dp),
shape = CircleShape,
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Text(text = "D", style = ClawTheme.type.label, color = ClawTheme.colors.text)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = entry.date, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = entry.text, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 2, overflow = TextOverflow.Ellipsis)
}
}
}
private fun formatDreamingNextRun(nextRunAtMs: Long?): String {
val next = nextRunAtMs ?: return "Not scheduled"
val deltaMinutes = ((next - System.currentTimeMillis()) / 60_000L).coerceAtLeast(0L)
val hours = deltaMinutes / 60L
return when {
hours >= 24L -> "In ${hours / 24L}d"
hours >= 1L -> "In ${hours}h"
deltaMinutes >= 1L -> "In ${deltaMinutes}m"
else -> "Soon"
}
}

View File

@@ -0,0 +1,220 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayHealthLogsSummary
import ai.openclaw.app.GatewayLogEntry
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
internal fun HealthLogsSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val isConnected by viewModel.isConnected.collectAsState()
val isNodeConnected by viewModel.isNodeConnected.collectAsState()
val chatHealthOk by viewModel.chatHealthOk.collectAsState()
val statusText by viewModel.statusText.collectAsState()
val modelCount by viewModel.modelCatalog.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
val talkStatus by viewModel.talkModeStatusText.collectAsState()
val logsSummary by viewModel.healthLogsSummary.collectAsState()
val logsRefreshing by viewModel.healthLogsRefreshing.collectAsState()
val logsErrorText by viewModel.healthLogsErrorText.collectAsState()
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshHealthLogs()
}
}
SettingsDetailFrame(
title = "Health",
subtitle = "Gateway status, phone node readiness, and recent log stream.",
icon = Icons.Default.Settings,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Gateway", if (isConnected) "Online" else "Offline"),
SettingsMetric("Node", if (isNodeConnected) "Online" else "Waiting"),
SettingsMetric("Models", modelCount.size.toString()),
SettingsMetric("Logs", logsSummary.entries.size.toString()),
),
)
HealthStatusPanel(
gateway = statusText,
node = if (isNodeConnected) "Online" else "Waiting",
chat = if (chatHealthOk) "Ready" else "Needs connection",
models = "${modelCount.size} available",
voice = talkStatus,
runs = if (pendingRunCount > 0) "$pendingRunCount active" else "Idle",
isConnected = isConnected,
isNodeConnected = isNodeConnected,
chatHealthOk = chatHealthOk,
modelsReady = modelCount.isNotEmpty(),
voiceReady = talkStatus.lowercase() != "off",
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(
text = if (logsRefreshing) "Refreshing" else "Refresh Logs",
onClick = viewModel::refreshHealthLogs,
enabled = isConnected && !logsRefreshing,
modifier = Modifier.weight(1f),
)
}
logsErrorText?.let { error ->
ClawPanel {
Text(text = error, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
GatewayLogsPanel(isConnected = isConnected, summary = logsSummary)
}
}
@Composable
private fun HealthStatusPanel(
gateway: String,
node: String,
chat: String,
models: String,
voice: String,
runs: String,
isConnected: Boolean,
isNodeConnected: Boolean,
chatHealthOk: Boolean,
modelsReady: Boolean,
voiceReady: Boolean,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
HealthStatusRow(title = "Gateway", value = gateway, healthy = isConnected)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HealthStatusRow(title = "Phone Node", value = node, healthy = isNodeConnected)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HealthStatusRow(title = "Chat", value = chat, healthy = chatHealthOk)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HealthStatusRow(title = "Models", value = models, healthy = modelsReady)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HealthStatusRow(title = "Voice", value = voice, healthy = voiceReady)
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HealthStatusRow(title = "Runs", value = runs, healthy = true)
}
}
}
@Composable
private fun HealthStatusRow(
title: String,
value: String,
healthy: Boolean,
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
ClawStatusPill(text = value, status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
}
}
@Composable
private fun GatewayLogsPanel(
isConnected: Boolean,
summary: GatewayHealthLogsSummary,
) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text(text = "RECENT LOGS", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
summary.fileName?.let { fileName ->
Text(text = fileName, style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
when {
!isConnected ->
ClawPanel {
Text(text = "Connect the gateway to load recent logs.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
summary.entries.isEmpty() ->
ClawPanel {
Text(text = "No recent log entries.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
else ->
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
val entries = summary.entries.takeLast(12)
Column {
entries.forEachIndexed { index, entry ->
GatewayLogRow(entry = entry)
if (index != entries.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
if (summary.truncated) {
Text(text = "Showing the latest log chunk.", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
}
}
}
@Composable
private fun GatewayLogRow(entry: GatewayLogEntry) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Text(text = compactLogTime(entry.time), style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, modifier = Modifier.weight(0.72f), maxLines = 1)
Column(modifier = Modifier.weight(2.7f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = entry.message, style = ClawTheme.type.caption, color = ClawTheme.colors.text, maxLines = 2, overflow = TextOverflow.Ellipsis)
entry.subsystem?.let { subsystem ->
Text(text = subsystem, style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
ClawStatusPill(text = entry.level?.uppercase() ?: "LOG", status = logLevelStatus(entry.level))
}
}
private fun compactLogTime(value: String?): String {
val raw = value?.trim().orEmpty()
if (raw.isEmpty()) return "--:--"
val time =
raw
.substringAfter('T', raw)
.substringBefore('.')
.substringBefore('+')
.substringBefore('Z')
return time.takeIf { it.length >= 5 }?.take(5) ?: raw.take(5)
}
private fun logLevelStatus(level: String?): ClawStatus =
when (level?.lowercase()) {
"error", "fatal" -> ClawStatus.Danger
"warn" -> ClawStatus.Warning
"info" -> ClawStatus.Success
else -> ClawStatus.Neutral
}

View File

@@ -0,0 +1,266 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayDeviceTokenSummary
import ai.openclaw.app.GatewayNodeSummary
import ai.openclaw.app.GatewayNodesDevicesSummary
import ai.openclaw.app.GatewayPairedDeviceSummary
import ai.openclaw.app.GatewayPendingDeviceSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawDetailRow
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTextBadge
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun NodesDevicesSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val summary by viewModel.nodesDevicesSummary.collectAsState()
val refreshing by viewModel.nodesDevicesRefreshing.collectAsState()
val errorText by viewModel.nodesDevicesErrorText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshNodesDevices()
}
}
SettingsDetailFrame(
title = "Nodes & Devices",
subtitle = "Live nodes, paired phones, and pending device requests.",
icon = Icons.Default.Cloud,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Nodes", summary.nodes.size.toString()),
SettingsMetric("Online", summary.nodes.count { it.connected }.toString()),
SettingsMetric("Devices", if (summary.devicePairingAvailable) summary.pairedDevices.size.toString() else "Locked"),
SettingsMetric("Pending", summary.pendingDevices.size.toString()),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(
text = if (refreshing) "Refreshing" else "Refresh",
onClick = viewModel::refreshNodesDevices,
enabled = isConnected && !refreshing,
modifier = Modifier.weight(1f),
)
}
errorText?.let {
ClawPanel {
Text(text = it, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
when {
!isConnected ->
ClawPanel {
Text(text = "Connect the gateway to load nodes and paired devices.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
summary.isEmpty() ->
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "No nodes or paired devices.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Linked phones and node hosts will appear here after pairing.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
else -> NodesDevicesPanel(summary = summary)
}
}
}
@Composable
private fun NodesDevicesPanel(summary: GatewayNodesDevicesSummary) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
if (!summary.devicePairingAvailable) {
ClawPanel {
Text(text = "Pairing controls are not available from this connection.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
if (summary.pendingDevices.isNotEmpty()) {
NodesSection(title = "Pending Requests") {
summary.pendingDevices.forEachIndexed { index, device ->
PendingDeviceRow(device = device)
if (index != summary.pendingDevices.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
if (summary.nodes.isNotEmpty()) {
NodesSection(title = "Nodes") {
summary.nodes.forEachIndexed { index, node ->
NodeRow(node = node)
if (index != summary.nodes.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
if (summary.pairedDevices.isNotEmpty()) {
NodesSection(title = "Paired Devices") {
summary.pairedDevices.forEachIndexed { index, device ->
PairedDeviceRow(device = device)
if (index != summary.pairedDevices.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
}
@Composable
private fun NodesSection(
title: String,
content: @Composable () -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = title.uppercase(), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
content()
}
}
}
}
@Composable
private fun NodeRow(node: GatewayNodeSummary) {
DeviceListRow(
badge = nodeBadge(node.displayName ?: node.id),
title = node.displayName ?: node.id,
subtitle = nodeSubtitle(node),
statusText = if (node.connected) "Online" else "Offline",
status = if (node.connected) ClawStatus.Success else ClawStatus.Warning,
)
}
@Composable
private fun PendingDeviceRow(device: GatewayPendingDeviceSummary) {
DeviceListRow(
badge = nodeBadge(device.displayName ?: device.deviceId),
title = device.displayName ?: "New device",
subtitle = pendingDeviceSubtitle(device),
statusText = if (device.repair) "Repair" else "Review",
status = ClawStatus.Warning,
)
}
@Composable
private fun PairedDeviceRow(device: GatewayPairedDeviceSummary) {
DeviceListRow(
badge = nodeBadge(device.displayName ?: device.deviceId),
title = device.displayName ?: "Paired device",
subtitle = pairedDeviceSubtitle(device),
statusText = pairedDeviceStatusText(device.tokens),
status = pairedDeviceStatus(device.tokens),
)
}
@Composable
private fun DeviceListRow(
badge: String,
title: String,
subtitle: String,
statusText: String,
status: ClawStatus,
) {
ClawDetailRow(
title = title,
subtitle = subtitle,
leading = { ClawTextBadge(text = badge) },
trailing = { ClawStatusPill(text = statusText, status = status) },
)
}
private fun GatewayNodesDevicesSummary.isEmpty(): Boolean = nodes.isEmpty() && pendingDevices.isEmpty() && pairedDevices.isEmpty()
private fun nodeSubtitle(node: GatewayNodeSummary): String {
val kind = node.deviceFamily ?: "Node host"
val version = node.version?.let { "OpenClaw $it" }
val status = if (node.paired) "Paired" else "Unpaired"
val commands =
node.commands
.take(2)
.joinToString(", ")
.takeIf { it.isNotBlank() }
return listOfNotNull(kind, version, status, commands).joinToString(" · ")
}
private fun pendingDeviceSubtitle(device: GatewayPendingDeviceSummary): String {
val roles = formatDeviceList(device.roles, "role")
val scopes = formatDeviceList(device.scopes, "scope")
val requested = device.requestedAtMs?.let { "requested ${relativeDeviceTime(it)}" }
return listOfNotNull(roles, scopes, requested, device.remoteIp).joinToString(" · ")
}
private fun pairedDeviceSubtitle(device: GatewayPairedDeviceSummary): String {
val roles = formatDeviceList(device.roles, "role")
val scopes = formatDeviceList(device.scopes, "scope")
val tokens = "${device.tokens.count { !it.revoked }}/${device.tokens.size} active tokens"
return listOfNotNull(roles, scopes, tokens, device.remoteIp).joinToString(" · ")
}
private fun pairedDeviceStatusText(tokens: List<GatewayDeviceTokenSummary>): String =
when {
tokens.isEmpty() -> "Paired"
tokens.any { !it.revoked } -> "Active"
else -> "Needs Token"
}
private fun pairedDeviceStatus(tokens: List<GatewayDeviceTokenSummary>): ClawStatus =
when {
tokens.isEmpty() -> ClawStatus.Neutral
tokens.any { !it.revoked } -> ClawStatus.Success
else -> ClawStatus.Warning
}
private fun formatDeviceList(
values: List<String>,
fallback: String,
): String? =
when (values.size) {
0 -> null
1 -> values.first()
else -> "${values.size} ${fallback}s"
}
private fun nodeBadge(value: String): String =
value
.split(' ', '-', '_')
.filter { it.isNotBlank() }
.take(2)
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
.joinToString("")
.ifBlank { "N" }
private fun relativeDeviceTime(timeMs: Long): String {
val minutes = ((System.currentTimeMillis() - timeMs).coerceAtLeast(0L)) / 60_000L
if (minutes < 1) return "now"
if (minutes < 60) return "${minutes}m ago"
val hours = minutes / 60L
if (hours < 24) return "${hours}h ago"
return "${hours / 24L}d ago"
}

View File

@@ -0,0 +1,558 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayModelProviderSummary
import ai.openclaw.app.GatewayModelSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.providerDisplayName
import ai.openclaw.app.ui.design.ClawEmptyState
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPrimaryButton
import ai.openclaw.app.ui.design.ClawScaffold
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun ProvidersModelsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
onAddProvider: () -> Unit,
) {
val isConnected by viewModel.isConnected.collectAsState()
val models by viewModel.modelCatalog.collectAsState()
val providers by viewModel.modelAuthProviders.collectAsState()
val refreshing by viewModel.modelCatalogRefreshing.collectAsState()
val errorText by viewModel.modelCatalogErrorText.collectAsState()
val providerRows = providerRows(providers = providers, models = models)
val modelGroups = sortedModelGroups(models)
val setupRows = providerSetupRows(providerRows)
var expandedModelProviders by rememberSaveable { mutableStateOf(emptyList<String>()) }
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshModelCatalog()
}
}
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 13.dp, end = 20.dp, bottom = 13.dp)) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp), contentPadding = PaddingValues(bottom = 112.dp)) {
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
ProviderHeaderIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", onClick = onBack)
ProviderHeaderIconButton(icon = Icons.Default.Add, contentDescription = "Add provider", outlined = true, onClick = onAddProvider)
}
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "Providers & Models", style = ClawTheme.type.display.copy(fontSize = 14.8.sp, lineHeight = 18.sp), color = ClawTheme.colors.text, maxLines = 1)
Text(
text = "Connect and manage AI providers\nBrowse models and their capabilities.",
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textMuted,
)
}
}
}
item {
ProviderOverviewPanel(
isConnected = isConnected,
providerRows = providerRows,
modelCount = models.size,
onRefresh = viewModel::refreshModelCatalog,
onSetup = onAddProvider,
refreshing = refreshing,
)
}
item {
ProviderSectionLabel(title = "Provider setup")
}
item {
ProviderSetupList(rows = setupRows, onSetup = onAddProvider)
}
item {
ProviderSectionLabel(title = "Connected providers")
}
item {
if (!isConnected && providerRows.isEmpty()) {
ClawEmptyState(title = "Gateway offline", body = "Connect your Gateway to load provider readiness and model catalog.")
} else {
ProviderList(rows = providerRows, refreshing = refreshing)
}
}
errorText?.let { message ->
item {
ClawPanel {
Text(text = message, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
item {
ProviderSectionLabel(title = "Model catalog")
}
if (modelGroups.isEmpty()) {
item {
ModelCatalogEmpty(
title = if (refreshing) "Loading models" else "No models loaded",
body = if (isConnected) "Refresh after configuring a provider on the Gateway." else "Connect the Gateway to browse models.",
)
}
} else {
items(modelGroups, key = { it.first }) { entry ->
val expanded = expandedModelProviders.contains(entry.first)
ModelGroup(
provider = entry.first,
models = entry.second,
expanded = expanded,
onToggle = {
expandedModelProviders =
if (expanded) {
expandedModelProviders - entry.first
} else {
expandedModelProviders + entry.first
}
},
)
}
}
}
ProviderAddButton(onClick = onAddProvider, modifier = Modifier.align(Alignment.BottomCenter))
}
}
}
private data class ProviderSetupRow(
val id: String,
val name: String,
val subtitle: String,
val ready: Boolean,
)
private data class ProviderRow(
val id: String,
val name: String,
val status: String,
val ready: Boolean,
val modelCount: Int,
)
private fun providerRows(
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): List<ProviderRow> {
val modelCounts = models.groupingBy { it.provider }.eachCount()
val authRows =
providers.map { provider ->
val ready = modelProviderReady(provider.status)
ProviderRow(
id = provider.id,
name = provider.displayName,
status = if (ready) "Ready" else "Needs setup",
ready = ready,
modelCount = modelCounts[provider.id] ?: 0,
)
}
val missingAuthRows =
modelCounts.keys
.filter { provider -> authRows.none { it.id == provider } }
.map { provider ->
ProviderRow(
id = provider,
name = providerDisplayName(provider),
status = "Ready",
ready = true,
modelCount = modelCounts[provider] ?: 0,
)
}
return (authRows + missingAuthRows).sortedWith(compareBy(::providerPriority, { it.name.lowercase() }))
}
private fun providerSetupRows(providerRows: List<ProviderRow>): List<ProviderSetupRow> {
val byId = providerRows.associateBy { it.id.trim().lowercase() }
return listOf("openai", "anthropic", "google", "openrouter", "ollama").map { id ->
val row = byId[id] ?: byId["ollama-local"].takeIf { id == "ollama" }
ProviderSetupRow(
id = id,
name = providerDisplayName(id),
subtitle = providerSetupSubtitle(id, row),
ready = row?.ready == true,
)
}
}
private fun providerSetupSubtitle(
id: String,
row: ProviderRow?,
): String =
when {
row?.ready == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Ready"
row != null -> "Finish setup to use ${row.name}"
id == "ollama" -> "Use models running on your network"
else -> "Add provider credentials on your Gateway"
}
internal fun modelProviderReady(status: String): Boolean {
val normalized = status.trim().lowercase()
return normalized == "ok" ||
normalized == "ready" ||
normalized == "healthy" ||
normalized == "configured" ||
normalized == "static"
}
private fun sortedModelGroups(models: List<GatewayModelSummary>): List<Pair<String, List<GatewayModelSummary>>> =
models
.groupBy { it.provider }
.entries
.sortedWith(compareBy({ providerPriority(it.key) }, { providerDisplayName(it.key).lowercase() }))
.map { it.key to it.value }
private fun providerPriority(row: ProviderRow): Int = providerPriority(row.id)
private fun providerPriority(provider: String): Int =
when (provider.trim().lowercase()) {
"openai" -> 0
"anthropic" -> 1
"google" -> 2
"openrouter" -> 3
"ollama", "ollama-local" -> 4
"codex", "openai-codex" -> 5
else -> 100
}
@Composable
private fun ProviderList(
rows: List<ProviderRow>,
refreshing: Boolean,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
if (rows.isEmpty()) {
ProviderListRow(ProviderRow(id = "loading", name = "Provider catalog", status = if (refreshing) "Loading" else "No providers", ready = false, modelCount = 0))
} else {
val visibleRows = rows.take(5)
visibleRows.forEachIndexed { index, row ->
ProviderListRow(row)
if (index != visibleRows.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
}
@Composable
private fun ProviderOverviewPanel(
isConnected: Boolean,
providerRows: List<ProviderRow>,
modelCount: Int,
refreshing: Boolean,
onRefresh: () -> Unit,
onSetup: () -> Unit,
) {
val readyCount = providerRows.count { it.ready }
val needsSetupCount = providerRows.count { !it.ready }
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ProviderMetricTile(label = "Ready", value = readyCount.toString(), modifier = Modifier.weight(1f))
ProviderMetricTile(label = "Models", value = modelCount.toString(), modifier = Modifier.weight(1f))
ProviderMetricTile(label = "Setup", value = needsSetupCount.toString(), modifier = Modifier.weight(1f))
}
Text(
text = if (isConnected) "Choose a provider below, then finish credentials on your Gateway." else "Connect your Gateway before adding model providers.",
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(text = if (refreshing) "Refreshing" else "Refresh", onClick = onRefresh, enabled = isConnected && !refreshing, modifier = Modifier.weight(1f))
ClawPrimaryButton(text = "Setup Provider", onClick = onSetup, enabled = isConnected, modifier = Modifier.weight(1f))
}
}
}
}
@Composable
private fun ProviderMetricTile(
label: String,
value: String,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surface,
border = BorderStroke(1.dp, ClawTheme.colors.border),
contentColor = ClawTheme.colors.text,
) {
Column(modifier = Modifier.padding(horizontal = 9.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = value, style = ClawTheme.type.title, color = ClawTheme.colors.text, maxLines = 1)
Text(text = label, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@Composable
private fun ProviderSetupList(
rows: List<ProviderSetupRow>,
onSetup: () -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
rows.forEachIndexed { index, row ->
ProviderSetupListRow(row = row, onClick = onSetup)
if (index != rows.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
@Composable
private fun ProviderSetupListRow(
row: ProviderSetupRow,
onClick: () -> Unit,
) {
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
ProviderBadge(text = row.name)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = row.name, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = row.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
Text(text = if (row.ready) "Ready" else "Setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
}
}
}
}
@Composable
private fun ProviderListRow(row: ProviderRow) {
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ProviderBadge(text = row.name)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = row.name, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = if (row.modelCount > 0) "${row.modelCount} models" else "Provider setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
Text(text = row.status, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@Composable
private fun ProviderBadge(text: String) {
Surface(modifier = Modifier.size(30.dp), shape = RoundedCornerShape(ClawTheme.radii.row), color = ClawTheme.colors.surfacePressed, border = BorderStroke(1.dp, ClawTheme.colors.border)) {
Box(contentAlignment = Alignment.Center) {
Text(text = providerInitials(text), style = ClawTheme.type.label, color = ClawTheme.colors.text, textAlign = TextAlign.Center)
}
}
}
private fun providerInitials(value: String): String =
value
.split(' ', '-', '_')
.filter { it.isNotBlank() }
.take(2)
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
.joinToString("")
.ifBlank { "AI" }
@Composable
private fun ModelCatalogEmpty(
title: String,
body: String,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 11.dp, vertical = 10.dp)) {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = body, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
}
}
}
@Composable
private fun ModelGroup(
provider: String,
models: List<GatewayModelSummary>,
expanded: Boolean,
onToggle: () -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
Surface(onClick = onToggle, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ProviderBadge(text = providerDisplayName(provider))
Text(text = providerDisplayName(provider), style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
ProviderMiniTag(text = "${models.size} models")
Icon(imageVector = if (expanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = if (expanded) "Collapse ${providerDisplayName(provider)} models" else "Expand ${providerDisplayName(provider)} models", modifier = Modifier.size(14.dp), tint = ClawTheme.colors.textMuted)
}
}
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
val visibleModels = if (expanded) models else models.take(3)
visibleModels.forEachIndexed { index, model ->
ModelRow(model)
if (index != visibleModels.lastIndex || models.size > visibleModels.size) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
if (models.size > visibleModels.size) {
Surface(onClick = onToggle, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = "View all models", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, modifier = Modifier.weight(1f))
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "View all models", modifier = Modifier.size(14.dp), tint = ClawTheme.colors.text)
}
}
}
}
}
}
@Composable
private fun ModelRow(model: GatewayModelSummary) {
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = model.name, style = ClawTheme.type.mono, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
modelCapabilityLabels(model).take(3).forEach { label ->
ProviderMiniTag(text = label)
}
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(ClawTheme.colors.success))
}
}
private fun modelCapabilityLabels(model: GatewayModelSummary): List<String> =
buildList {
if (model.supportsReasoning) add("Reasoning")
if (model.supportsVision) add("Vision")
if (model.supportsAudio) add("Voice")
if (model.supportsDocuments) add("Docs")
if ((model.contextTokens ?: 0L) >= 100_000L) add("Long context")
if (isEmpty()) add("Fast")
}
@Composable
private fun ProviderSectionLabel(title: String) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = title.uppercase(), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
}
}
@Composable
private fun ProviderHeaderIconButton(
icon: ImageVector,
contentDescription: String,
outlined: Boolean = false,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = Color.Transparent,
contentColor = ClawTheme.colors.text,
border = if (outlined) BorderStroke(1.dp, ClawTheme.colors.borderStrong) else null,
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(if (outlined) 17.dp else 20.dp))
}
}
}
@Composable
private fun ProviderAddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
modifier = modifier.fillMaxWidth().height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(17.dp))
Spacer(modifier = Modifier.width(7.dp))
Text(text = "Open Gateway Setup", style = ClawTheme.type.label, maxLines = 1)
}
}
}
@Composable
private fun ProviderMiniTag(text: String) {
Surface(
shape = RoundedCornerShape(5.dp),
color = Color.Transparent,
border = BorderStroke(1.dp, ClawTheme.colors.border),
contentColor = ClawTheme.colors.textMuted,
) {
Text(text = text, modifier = Modifier.padding(horizontal = 4.dp, vertical = 0.5.dp), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), maxLines = 1)
}
}

View File

@@ -16,5 +16,5 @@ fun RootScreen(viewModel: MainViewModel) {
return
}
PostOnboardingTabs(viewModel = viewModel, modifier = Modifier.fillMaxSize())
ShellScreen(viewModel = viewModel, modifier = Modifier.fillMaxSize())
}

View File

@@ -0,0 +1,334 @@
package ai.openclaw.app.ui
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawEmptyState
import ai.openclaw.app.ui.design.ClawPrimaryButton
import ai.openclaw.app.ui.design.ClawScaffold
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.StarBorder
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.SwapVert
import androidx.compose.material.icons.outlined.AccessTime
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material.icons.outlined.MicNone
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
internal fun SessionsScreen(
viewModel: MainViewModel,
onOpenCommand: () -> Unit,
onOpenChat: () -> Unit,
) {
val sessions by viewModel.chatSessions.collectAsState()
val chatSessionKey by viewModel.chatSessionKey.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
var filter by rememberSaveable { mutableStateOf(SessionFilter.Recent) }
var compactLayout by rememberSaveable { mutableStateOf(false) }
var recentFirst by rememberSaveable { mutableStateOf(true) }
val visibleSessions =
sessions
.let { rows ->
when (filter) {
SessionFilter.Recent -> rows
SessionFilter.Live -> rows.filter { it.key == chatSessionKey }
}
}.let { rows ->
if (recentFirst) {
rows.sortedByDescending { it.updatedAtMs ?: 0L }
} else {
rows.sortedBy { it.updatedAtMs ?: 0L }
}
}
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshChatSessions(limit = 200)
}
}
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = "Sessions", style = ClawTheme.type.display.copy(fontSize = 17.4.sp, lineHeight = 21.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
SessionPlainIconButton(icon = Icons.Default.Search, contentDescription = "Search sessions", onClick = onOpenCommand)
SessionPlainIconButton(icon = Icons.Default.SwapVert, contentDescription = "Reverse session sort", onClick = { recentFirst = !recentFirst })
}
}
item {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
FilterPill(text = "Recent", icon = Icons.Outlined.AccessTime, active = filter == SessionFilter.Recent, onClick = { filter = SessionFilter.Recent })
FilterPill(text = "Live", icon = Icons.Outlined.MicNone, active = filter == SessionFilter.Live, live = sessions.any { it.key == chatSessionKey }, onClick = { filter = SessionFilter.Live })
}
}
item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Row(
modifier =
Modifier
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable { recentFirst = !recentFirst }
.padding(horizontal = 2.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(text = "Sort: ${if (recentFirst) "Newest" else "Oldest"}", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
Icon(imageVector = Icons.Default.KeyboardArrowDown, contentDescription = null, modifier = Modifier.size(11.dp), tint = ClawTheme.colors.textMuted)
}
SessionOutlineIconButton(icon = Icons.Default.Storage, contentDescription = "Toggle session layout", onClick = { compactLayout = !compactLayout })
}
}
item {
Text(text = if (compactLayout) "Layout: Compact" else "Layout: Detailed", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
}
if (visibleSessions.isEmpty()) {
item {
ClawEmptyState(
title = emptySessionTitle(filter),
body = emptySessionBody(filter),
action = { ClawPrimaryButton(text = "Start Chat", onClick = onOpenChat) },
)
}
} else {
items(visibleSessions, key = { it.key }) { session ->
val active = session.key == chatSessionKey
SessionRow(
title = displaySessionTitle(session.displayName),
subtitle = if (active) "Current session" else "OpenClaw session",
metadata = session.updatedAtMs?.let(::relativeSessionTime) ?: "now",
active = active,
compact = compactLayout,
onClick = {
viewModel.switchChatSession(session.key)
onOpenChat()
},
)
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
@Composable
private fun FilterPill(
text: String,
icon: ImageVector? = null,
active: Boolean = false,
live: Boolean = false,
dropdown: Boolean = false,
onClick: (() -> Unit)? = null,
) {
Surface(
onClick = onClick ?: {},
enabled = onClick != null,
shape = RoundedCornerShape(7.dp),
color = if (active) ClawTheme.colors.surfaceRaised else Color.Transparent,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
icon?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(12.dp), tint = ClawTheme.colors.text) }
Text(text = text, style = ClawTheme.type.label, color = ClawTheme.colors.text, maxLines = 1)
if (live) {
Box(modifier = Modifier.size(4.dp).clip(CircleShape).background(ClawTheme.colors.success))
}
if (dropdown) {
Icon(imageVector = Icons.Default.KeyboardArrowDown, contentDescription = null, modifier = Modifier.size(11.dp), tint = ClawTheme.colors.textMuted)
}
}
}
}
@Composable
private fun SessionRow(
title: String,
subtitle: String,
metadata: String,
active: Boolean,
compact: Boolean,
onClick: () -> Unit,
) {
Surface(onClick = onClick, color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
Column {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(7.dp),
) {
Surface(
modifier = Modifier.size(30.dp),
shape = CircleShape,
color = Color.Transparent,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = if (active) Icons.Default.StarBorder else Icons.Outlined.ChatBubbleOutline,
contentDescription = null,
modifier = Modifier.size(15.dp),
tint = ClawTheme.colors.text,
)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.5.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = title,
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (active) {
Box(modifier = Modifier.size(3.5.dp).clip(CircleShape).background(ClawTheme.colors.success))
}
}
if (!compact) {
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
SessionMiniTag(text = "Workspace")
SessionMiniTag(text = if (active) "Active" else "OpenClaw")
}
}
}
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(13.dp), tint = ClawTheme.colors.textMuted)
Text(text = metadata, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
@Composable
private fun SessionPlainIconButton(
icon: ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
}
}
}
@Composable
private fun SessionOutlineIconButton(
icon: ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(7.dp),
color = Color.Transparent,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(14.dp))
}
}
}
@Composable
private fun SessionMiniTag(text: String) {
Surface(
shape = RoundedCornerShape(5.dp),
color = Color.Transparent,
border = BorderStroke(1.dp, ClawTheme.colors.border),
contentColor = ClawTheme.colors.textMuted,
) {
Text(text = text, modifier = Modifier.padding(horizontal = 4.dp, vertical = 0.5.dp), style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), maxLines = 1)
}
}
private enum class SessionFilter {
Recent,
Live,
}
private fun emptySessionTitle(filter: SessionFilter): String =
when (filter) {
SessionFilter.Recent -> "No sessions yet"
SessionFilter.Live -> "No live session"
}
private fun emptySessionBody(filter: SessionFilter): String =
when (filter) {
SessionFilter.Recent -> "Start a new conversation and it will show up here."
SessionFilter.Live -> "Open Chat to start or resume the current session."
}
private fun relativeSessionTime(updatedAtMs: Long): String {
val deltaMs = (System.currentTimeMillis() - updatedAtMs).coerceAtLeast(0L)
val minutes = deltaMs / 60_000L
if (minutes < 1) return "now"
if (minutes < 60) return "${minutes}m"
val hours = minutes / 60
if (hours < 24) return "${hours}h"
return "${hours / 24}d"
}
private fun displaySessionTitle(displayName: String?): String = displayName?.takeIf { it.isNotBlank() } ?: "Main session"

File diff suppressed because it is too large Load Diff

View File

@@ -209,6 +209,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
context.packageManager?.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) == true
}
val callLogPermissionAvailable = remember { SensitiveFeatureConfig.callLogEnabled }
val photosPermissionAvailable = remember { SensitiveFeatureConfig.photosEnabled }
val photosPermission =
if (Build.VERSION.SDK_INT >= 33) {
Manifest.permission.READ_MEDIA_IMAGES
@@ -245,8 +246,11 @@ fun SettingsSheet(viewModel: MainViewModel) {
var photosPermissionGranted by
remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, photosPermission) ==
PackageManager.PERMISSION_GRANTED,
if (photosPermissionAvailable) {
ContextCompat.checkSelfPermission(context, photosPermission) == PackageManager.PERMISSION_GRANTED
} else {
false
},
)
}
val photosPermissionLauncher =
@@ -347,8 +351,11 @@ fun SettingsSheet(viewModel: MainViewModel) {
notificationListenerEnabled = isNotificationListenerEnabled(context)
installedNotificationApps = queryInstalledApps(context, notificationForwardingPackages)
photosPermissionGranted =
ContextCompat.checkSelfPermission(context, photosPermission) ==
PackageManager.PERMISSION_GRANTED
if (photosPermissionAvailable) {
ContextCompat.checkSelfPermission(context, photosPermission) == PackageManager.PERMISSION_GRANTED
} else {
false
}
contactsPermissionGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) ==
PackageManager.PERMISSION_GRANTED &&
@@ -980,31 +987,33 @@ fun SettingsSheet(viewModel: MainViewModel) {
}
item {
Column(modifier = Modifier.settingsRowModifier()) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Photos", style = mobileHeadline) },
supportingContent = { Text("Access recent photos.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (photosPermissionGranted) {
openAppSettings(context)
} else {
photosPermissionLauncher.launch(photosPermission)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (photosPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
HorizontalDivider(color = mobileBorder)
if (photosPermissionAvailable) {
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Photos", style = mobileHeadline) },
supportingContent = { Text("Access recent photos.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (photosPermissionGranted) {
openAppSettings(context)
} else {
photosPermissionLauncher.launch(photosPermission)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (photosPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
HorizontalDivider(color = mobileBorder)
}
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewaySkillSummary
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.design.ClawDetailRow
import ai.openclaw.app.ui.design.ClawListPanel
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTextBadge
import ai.openclaw.app.ui.design.ClawTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun SkillsSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val skillsSummary by viewModel.skillsSummary.collectAsState()
val skillsRefreshing by viewModel.skillsRefreshing.collectAsState()
val skillsErrorText by viewModel.skillsErrorText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
val skills = skillsSummary.skills
val readyCount = skills.count { skillReady(it) }
val needsSetupCount = skills.count { skillNeedsSetup(it) }
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshSkills()
}
}
SettingsDetailFrame(
title = "Skills",
subtitle = "Installed capabilities available to OpenClaw.",
icon = Icons.Default.Settings,
onBack = onBack,
) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Installed", skills.size.toString()),
SettingsMetric("Ready", readyCount.toString()),
SettingsMetric("Needs Setup", needsSetupCount.toString()),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawSecondaryButton(
text = if (skillsRefreshing) "Refreshing" else "Refresh",
onClick = viewModel::refreshSkills,
enabled = isConnected && !skillsRefreshing,
modifier = Modifier.weight(1f),
)
}
skillsErrorText?.let { errorText ->
ClawPanel {
Text(text = errorText, style = ClawTheme.type.body, color = ClawTheme.colors.warning)
}
}
when {
!isConnected ->
ClawPanel {
Text(text = "Connect the gateway to load skills.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
skills.isEmpty() ->
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "No skills installed.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Skills installed on the gateway will appear here.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
else -> SkillsPanel(skills = skills)
}
}
}
@Composable
private fun SkillsPanel(skills: List<GatewaySkillSummary>) {
ClawListPanel(items = skills) { skill ->
SkillListRow(skill = skill)
}
}
@Composable
private fun SkillListRow(skill: GatewaySkillSummary) {
ClawDetailRow(
title = skill.name,
subtitle = skillSubtitle(skill),
leading = { ClawTextBadge(text = skillBadge(skill)) },
trailing = { ClawStatusPill(text = skillStatusText(skill), status = skillStatus(skill)) },
)
}
private fun skillReady(skill: GatewaySkillSummary): Boolean = !skill.disabled && skill.eligible && skill.missingCount == 0
private fun skillNeedsSetup(skill: GatewaySkillSummary): Boolean = !skill.disabled && (skill.blockedByAllowlist || !skill.eligible || skill.missingCount > 0)
private fun skillStatusText(skill: GatewaySkillSummary): String =
when {
skill.disabled -> "Off"
skillNeedsSetup(skill) -> "Setup"
else -> "Ready"
}
private fun skillStatus(skill: GatewaySkillSummary): ClawStatus =
when {
skill.disabled -> ClawStatus.Neutral
skillNeedsSetup(skill) -> ClawStatus.Warning
else -> ClawStatus.Success
}
private fun skillSubtitle(skill: GatewaySkillSummary): String {
val issue =
when {
skill.disabled -> "Disabled"
skill.blockedByAllowlist -> "Blocked"
skill.missingCount > 0 -> "${skill.missingCount} missing"
!skill.eligible -> "Needs setup"
else -> null
}
return listOfNotNull(skill.description, skillSourceLabel(skill), issue).joinToString(" · ")
}
private fun skillSourceLabel(skill: GatewaySkillSummary): String =
when (skill.source) {
"openclaw-bundled" -> if (skill.bundled) "Built-in" else "Bundled"
"openclaw-managed" -> "Installed"
"openclaw-workspace" -> "Workspace"
"openclaw-extra" -> "Extra"
else -> "Skill"
}
private fun skillBadge(skill: GatewaySkillSummary): String {
skill.emoji?.let { return it }
return skill.name
.split(' ', '-', '_')
.filter { it.isNotBlank() }
.take(2)
.mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
.joinToString("")
.ifBlank { "S" }
}

View File

@@ -0,0 +1,954 @@
package ai.openclaw.app.ui
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.VoiceCaptureMode
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPrimaryButton
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTheme
import ai.openclaw.app.voice.VoiceConversationEntry
import ai.openclaw.app.voice.VoiceConversationRole
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MicOff
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.PhoneDisabled
import androidx.compose.material.icons.filled.RecordVoiceOver
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.TextFields
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
@Composable
fun VoiceScreen(
viewModel: MainViewModel,
onOpenCommand: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
val context = LocalContext.current
val gatewayStatus by viewModel.statusText.collectAsState()
val voiceCaptureMode by viewModel.voiceCaptureMode.collectAsState()
val micEnabled by viewModel.micEnabled.collectAsState()
val micCooldown by viewModel.micCooldown.collectAsState()
val speakerEnabled by viewModel.speakerEnabled.collectAsState()
val micStatusText by viewModel.micStatusText.collectAsState()
val micLiveTranscript by viewModel.micLiveTranscript.collectAsState()
val micQueuedMessages by viewModel.micQueuedMessages.collectAsState()
val micConversation by viewModel.micConversation.collectAsState()
val micIsSending by viewModel.micIsSending.collectAsState()
val talkModeEnabled by viewModel.talkModeEnabled.collectAsState()
val talkModeListening by viewModel.talkModeListening.collectAsState()
val talkModeSpeaking by viewModel.talkModeSpeaking.collectAsState()
val talkModeConversation by viewModel.talkModeConversation.collectAsState()
var pendingAction by remember { mutableStateOf<VoiceAction?>(null) }
var hasMicPermission by remember { mutableStateOf(context.hasRecordAudioPermission()) }
val requestMicPermission =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
hasMicPermission = granted
if (granted) {
when (pendingAction) {
VoiceAction.Talk -> viewModel.setTalkModeEnabled(true)
VoiceAction.Dictation -> viewModel.setMicEnabled(true)
null -> Unit
}
}
pendingAction = null
}
val activeConversation = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) talkModeConversation else micConversation
val voiceActive = micEnabled || micIsSending || talkModeEnabled
val activeStatus =
voiceStatusLabel(
gatewayStatus = gatewayStatus,
voiceCaptureMode = voiceCaptureMode,
micStatusText = micStatusText,
micQueuedMessages = micQueuedMessages.size,
micIsSending = micIsSending,
talkModeListening = talkModeListening,
talkModeSpeaking = talkModeSpeaking,
)
if (talkModeEnabled) {
TalkSessionScreen(
entries = talkModeConversation,
listening = talkModeListening,
speaking = talkModeSpeaking,
speakerEnabled = speakerEnabled,
onToggleSpeaker = { viewModel.setSpeakerEnabled(!speakerEnabled) },
onEndTalk = { viewModel.setTalkModeEnabled(false) },
onOpenVoiceSettings = onOpenVoiceSettings,
)
return
}
if (voiceCaptureMode == VoiceCaptureMode.ManualMic || micEnabled || micIsSending) {
DictationScreen(
liveTranscript = micLiveTranscript,
conversation = micConversation,
listening = micEnabled,
sending = micIsSending,
statusText = activeStatus,
gatewayStatus = gatewayStatus,
onCancel = { viewModel.cancelMicCapture() },
onSend = { viewModel.setMicEnabled(false) },
onOpenVoiceSettings = onOpenVoiceSettings,
)
return
}
Column(
modifier =
Modifier
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
VoiceHeader(
statusText = if (voiceActive) activeStatus else "Your voice command center.",
speakerEnabled = speakerEnabled,
onToggleSpeaker = { viewModel.setSpeakerEnabled(!speakerEnabled) },
onOpenCommand = onOpenCommand,
)
VoiceHero(
gatewayStatus = gatewayStatus,
voiceCaptureMode = voiceCaptureMode,
micEnabled = micEnabled,
talkModeEnabled = talkModeEnabled,
talkModeListening = talkModeListening,
talkModeSpeaking = talkModeSpeaking,
micLiveTranscript = micLiveTranscript,
onStartTalk = {
runVoiceAction(
action = VoiceAction.Talk,
hasMicPermission = hasMicPermission,
requestPermission = {
pendingAction = VoiceAction.Talk
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
},
run = { viewModel.setTalkModeEnabled(!talkModeEnabled) },
)
},
onStartDictation = {
if (micCooldown) return@VoiceHero
runVoiceAction(
action = VoiceAction.Dictation,
hasMicPermission = hasMicPermission,
requestPermission = {
pendingAction = VoiceAction.Dictation
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
},
run = { viewModel.setMicEnabled(!micEnabled) },
)
},
)
if (!hasMicPermission) {
VoicePermissionPanel(
onRequestPermission = {
pendingAction = VoiceAction.Talk
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
},
)
}
VoiceTranscript(
entries = activeConversation,
showThinking = micIsSending && activeConversation.none { it.role == VoiceConversationRole.Assistant && it.isStreaming },
modifier = Modifier.weight(1f),
)
}
}
@Composable
private fun DictationScreen(
liveTranscript: String?,
conversation: List<VoiceConversationEntry>,
listening: Boolean,
sending: Boolean,
statusText: String,
gatewayStatus: String,
onCancel: () -> Unit,
onSend: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
val lastUserText = conversation.lastOrNull { it.role == VoiceConversationRole.User }?.text
val draftText = liveTranscript?.takeIf { it.isNotBlank() } ?: lastUserText.orEmpty()
val speechProviderReady = gatewayStatus.isVoiceGatewayReady()
Column(
modifier =
Modifier
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onCancel)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = "Dictation", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
Text(text = "Transcribe then send", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
VoicePlainIconButton(icon = Icons.Default.Settings, contentDescription = "Dictation settings", onClick = onOpenVoiceSettings)
}
Surface(
modifier = Modifier.fillMaxWidth().aspectRatio(0.82f),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp, vertical = 12.dp), verticalArrangement = Arrangement.SpaceBetween) {
Text(
text = draftText.ifBlank { if (sending) "Sending to chat..." else "Start speaking..." },
style = ClawTheme.type.title.copy(fontSize = 15.sp, lineHeight = 19.sp),
color = if (draftText.isBlank()) ClawTheme.colors.textSubtle else ClawTheme.colors.text,
maxLines = 7,
overflow = TextOverflow.Ellipsis,
)
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
DictationWaveform(active = listening || sending)
Row(horizontalArrangement = Arrangement.spacedBy(7.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = Icons.Default.Mic, contentDescription = null, modifier = Modifier.size(15.dp), tint = if (listening) ClawTheme.colors.success else ClawTheme.colors.textMuted)
Text(text = statusText, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
}
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Surface(
modifier = Modifier.size(30.dp),
shape = CircleShape,
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(16.dp), tint = ClawTheme.colors.text)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = "Speech provider", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = gatewayStatus.voiceGatewayLabel(), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(
text =
when {
sending -> "Sending"
speechProviderReady -> "Ready"
else -> "Offline"
},
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color =
when {
sending -> ClawTheme.colors.warning
speechProviderReady -> ClawTheme.colors.success
else -> ClawTheme.colors.textMuted
},
)
Box(
modifier =
Modifier
.size(6.dp)
.clip(CircleShape)
.background(
when {
sending -> ClawTheme.colors.warning
speechProviderReady -> ClawTheme.colors.success
else -> ClawTheme.colors.textSubtle
},
),
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Icon(imageVector = Icons.Default.Info, contentDescription = null, modifier = Modifier.size(16.dp), tint = ClawTheme.colors.textMuted)
Text(text = "Tip: stop listening to send the captured turn.", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
ClawSecondaryButton(text = "Cancel", icon = Icons.Default.Close, onClick = onCancel, modifier = Modifier.weight(0.95f))
ClawPrimaryButton(text = if (sending) "Sending" else "Send to Chat", icon = Icons.AutoMirrored.Filled.Send, onClick = onSend, enabled = !sending, modifier = Modifier.weight(1.25f))
}
}
}
@Composable
private fun DictationWaveform(active: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) {
List(48) { index ->
val height = if (active) 3 + ((index * 7) % 16) else 3 + (index % 3) * 2
Box(
modifier =
Modifier
.size(width = 2.dp, height = height.dp)
.clip(RoundedCornerShape(999.dp))
.background(if (active) ClawTheme.colors.text else ClawTheme.colors.textSubtle),
)
}
}
}
@Composable
private fun TalkSessionScreen(
entries: List<VoiceConversationEntry>,
listening: Boolean,
speaking: Boolean,
speakerEnabled: Boolean,
onToggleSpeaker: () -> Unit,
onEndTalk: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
Column(
modifier =
Modifier
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(11.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onEndTalk)
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 14.sp, lineHeight = 17.sp), color = ClawTheme.colors.text)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (speaking || listening) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
Text(
text =
if (speaking) {
"OpenClaw speaking"
} else if (listening) {
"Realtime voice"
} else {
"Connected"
},
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
}
}
VoicePlainIconButton(icon = Icons.Default.Info, contentDescription = "Talk settings", onClick = onOpenVoiceSettings)
}
Surface(
modifier = Modifier.fillMaxWidth().height(58.dp),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
TalkWaveform(active = listening || speaking)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Live transcript", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
TalkTranscript(entries = entries, modifier = Modifier.weight(1f))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
TalkControl(icon = if (speakerEnabled) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff, label = if (speakerEnabled) "Mute" else "Unmute", onClick = onToggleSpeaker)
TalkControl(icon = Icons.Default.PhoneDisabled, label = "End", primary = true, onClick = onEndTalk)
TalkControl(icon = Icons.Default.GraphicEq, label = "Voice", onClick = onOpenVoiceSettings)
}
}
}
@Composable
private fun TalkTranscript(
entries: List<VoiceConversationEntry>,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(7.dp)) {
if (entries.isEmpty()) {
item {
TalkTranscriptCard(label = "OpenClaw", text = "Listening for your next turn.", muted = true)
}
} else {
items(entries.takeLast(6), key = { it.id }) { entry ->
TalkTranscriptCard(
label = if (entry.role == VoiceConversationRole.User) "You" else "OpenClaw",
text = if (entry.isStreaming && entry.text.isBlank()) "Listening response..." else entry.text,
muted = entry.isStreaming,
)
}
}
}
}
@Composable
private fun TalkTranscriptCard(
label: String,
text: String,
muted: Boolean = false,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surface,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(text = label, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = text, style = ClawTheme.type.body, color = if (muted) ClawTheme.colors.textMuted else ClawTheme.colors.text)
}
}
}
@Composable
private fun TalkControl(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
primary: Boolean = false,
onClick: () -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp)) {
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = if (primary) ClawTheme.colors.primary else ClawTheme.colors.canvas,
contentColor = if (primary) ClawTheme.colors.primaryText else ClawTheme.colors.text,
border = BorderStroke(1.dp, if (primary) ClawTheme.colors.primary else ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = label, modifier = Modifier.size(if (primary) 20.dp else 18.dp))
}
}
Text(text = label, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
}
}
@Composable
private fun TalkWaveform(active: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
listOf(4, 12, 24, 34, 46, 28, 12, 38, 44, 24, 12, 30, 42, 18, 6).forEachIndexed { index, height ->
Box(
modifier =
Modifier
.size(width = 3.dp, height = (if (active) height else 6 + index % 4 * 5).dp)
.clip(RoundedCornerShape(999.dp))
.background(if (active) ClawTheme.colors.text else ClawTheme.colors.textSubtle),
)
}
}
}
@Composable
private fun VoiceHeader(
statusText: String,
speakerEnabled: Boolean,
onToggleSpeaker: () -> Unit,
onOpenCommand: () -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = "O P E N C L A W",
style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp),
color = ClawTheme.colors.text,
modifier = Modifier.weight(1f),
)
VoicePlainIconButton(icon = Icons.Default.Search, contentDescription = "Search voice", onClick = onOpenCommand)
VoiceAvatar(text = "OC")
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "Voice", style = ClawTheme.type.display.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
Text(
text = statusText,
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
VoicePlainIconButton(
icon = if (speakerEnabled) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff,
contentDescription = if (speakerEnabled) "Mute speaker" else "Unmute speaker",
onClick = onToggleSpeaker,
)
}
}
}
@Composable
private fun VoiceAvatar(text: String) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Text(text = text.take(2).uppercase(), style = ClawTheme.type.label)
}
}
}
@Composable
private fun VoicePlainIconButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
Surface(onClick = onClick, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
}
}
}
@Composable
private fun VoiceHero(
gatewayStatus: String,
voiceCaptureMode: VoiceCaptureMode,
micEnabled: Boolean,
talkModeEnabled: Boolean,
talkModeListening: Boolean,
talkModeSpeaking: Boolean,
micLiveTranscript: String?,
onStartTalk: () -> Unit,
onStartDictation: () -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
VoiceOrb(
active = micEnabled || talkModeEnabled,
listening = talkModeListening || voiceCaptureMode == VoiceCaptureMode.ManualMic,
speaking = talkModeSpeaking,
)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Box(
modifier =
Modifier
.size(7.dp)
.clip(CircleShape)
.background(if (micEnabled || talkModeEnabled) ClawTheme.colors.success else ClawTheme.colors.textSubtle),
)
Text(
text =
when {
talkModeSpeaking -> "OpenClaw is replying"
talkModeListening -> "Listening"
talkModeEnabled -> "Talk is live"
micEnabled -> "Dictation is listening"
else -> "Ready to talk"
},
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
)
}
if (!micLiveTranscript.isNullOrBlank()) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surface,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Text(
text = micLiveTranscript.trim(),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp),
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
)
}
}
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
VoiceModeRow(
title = if (talkModeEnabled) "End Talk" else "Realtime Talk",
subtitle = if (talkModeEnabled) "Conversation is live" else "Natural conversation in real time",
icon = if (talkModeEnabled) Icons.Default.PhoneDisabled else Icons.Default.RecordVoiceOver,
onClick = onStartTalk,
)
VoiceModeRow(
title = if (micEnabled) "Stop Dictation" else "Dictation",
subtitle = if (micEnabled) "Listening for one turn" else "Convert speech to text",
icon = if (micEnabled) Icons.Default.MicOff else Icons.Default.TextFields,
onClick = onStartDictation,
)
}
VoiceProviderCard(gatewayStatus = gatewayStatus)
VoicePrimaryAction(
text = if (talkModeEnabled) "End Talk" else "Start Talk",
icon = if (talkModeEnabled) Icons.Default.PhoneDisabled else Icons.Default.Phone,
onClick = onStartTalk,
)
}
}
@Composable
private fun VoiceModeRow(
title: String,
subtitle: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
) {
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 60.dp).padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
color = ClawTheme.colors.surface,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, modifier = Modifier.size(21.dp), tint = ClawTheme.colors.textMuted)
}
}
}
@Composable
private fun VoiceProviderCard(gatewayStatus: String) {
val ready = gatewayStatus.isVoiceGatewayReady()
Surface(
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surface,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(17.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = "Provider", style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = gatewayStatus.voiceGatewayLabel(), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(7.dp)) {
Box(
modifier =
Modifier
.size(7.dp)
.clip(CircleShape)
.background(if (ready) ClawTheme.colors.success else ClawTheme.colors.textSubtle),
)
Text(text = if (ready) "Ready" else "Offline", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
}
@Composable
private fun VoicePrimaryAction(
text: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = Modifier.fillMaxWidth().height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(17.dp))
Text(text = text, modifier = Modifier.padding(start = 8.dp), style = ClawTheme.type.label)
}
}
}
@Composable
private fun VoiceOrb(
active: Boolean,
listening: Boolean,
speaking: Boolean,
) {
Surface(
modifier = Modifier.size(132.dp),
shape = CircleShape,
color = if (active) ClawTheme.colors.surfacePressed else ClawTheme.colors.surface,
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
imageVector =
when {
speaking -> Icons.Default.RecordVoiceOver
listening -> Icons.Default.GraphicEq
else -> Icons.Default.Mic
},
contentDescription = null,
modifier = Modifier.size(38.dp),
tint = ClawTheme.colors.text,
)
Waveform(active = active)
}
}
}
}
@Composable
private fun Waveform(active: Boolean) {
Row(horizontalArrangement = Arrangement.spacedBy(3.dp), verticalAlignment = Alignment.CenterVertically) {
listOf(6, 11, 17, 23, 14, 9, 20, 14, 7).forEachIndexed { index, height ->
Box(
modifier =
Modifier
.size(width = 2.dp, height = (if (active) height else 6 + index % 3 * 3).dp)
.clip(RoundedCornerShape(999.dp))
.background(if (active) ClawTheme.colors.text else ClawTheme.colors.textSubtle),
)
}
}
}
@Composable
private fun VoiceTranscript(
entries: List<VoiceConversationEntry>,
showThinking: Boolean,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
LaunchedEffect(entries.size, showThinking) {
if (entries.isNotEmpty() || showThinking) {
listState.animateScrollToItem(0)
}
}
LazyColumn(
modifier = modifier.fillMaxWidth(),
state = listState,
reverseLayout = true,
verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(bottom = 8.dp),
) {
if (showThinking) {
item(key = "thinking") {
VoiceThinkingCard()
}
}
items(entries.asReversed(), key = { it.id }) { entry ->
VoiceTurnCard(entry = entry)
}
if (entries.isEmpty() && !showThinking) {
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Live transcript", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 9.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "No transcript yet", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
text = "Your words and OpenClaw replies will appear here.",
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
}
}
}
}
}
}
}
@Composable
private fun VoiceTurnCard(entry: VoiceConversationEntry) {
val isUser = entry.role == VoiceConversationRole.User
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start) {
Surface(
modifier = Modifier.fillMaxWidth(if (isUser) 0.82f else 0.92f),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = if (isUser) ClawTheme.colors.surfacePressed else ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, if (entry.isStreaming) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(
text = if (isUser) "You" else "OpenClaw",
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
color = ClawTheme.colors.textSubtle,
)
Text(
text = if (entry.isStreaming && entry.text.isBlank()) "Listening..." else entry.text,
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
)
}
}
}
}
@Composable
private fun VoiceThinkingCard() {
ClawPanel {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ClawStatusPill(text = "Sending", status = ClawStatus.Warning)
Text(text = "OpenClaw is preparing a response.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
@Composable
private fun VoicePermissionPanel(onRequestPermission: () -> Unit) {
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
ClawStatusPill(text = "Permission needed", status = ClawStatus.Warning)
Text(text = "Microphone access is needed.", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
text = "OpenClaw only listens when you start Talk or Dictation.",
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
ClawSecondaryButton(text = "Enable Microphone", icon = Icons.Default.Mic, onClick = onRequestPermission)
}
}
}
private enum class VoiceAction {
Talk,
Dictation,
}
private fun runVoiceAction(
action: VoiceAction,
hasMicPermission: Boolean,
requestPermission: () -> Unit,
run: () -> Unit,
) {
if (hasMicPermission) {
run()
} else {
requestPermission()
}
}
private fun voiceStatusLabel(
gatewayStatus: String,
voiceCaptureMode: VoiceCaptureMode,
micStatusText: String,
micQueuedMessages: Int,
micIsSending: Boolean,
talkModeListening: Boolean,
talkModeSpeaking: Boolean,
): String =
when {
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeSpeaking -> "OpenClaw is speaking"
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeListening -> "Listening"
voiceCaptureMode == VoiceCaptureMode.TalkMode -> "Talk is live"
micIsSending -> "Sending dictation"
voiceCaptureMode == VoiceCaptureMode.ManualMic -> micStatusText.ifBlank { "Listening" }
micQueuedMessages > 0 -> "$micQueuedMessages queued"
!gatewayStatus.isVoiceGatewayReady() -> "Gateway offline"
else -> "Ready to talk"
}
private fun String.isVoiceGatewayReady(): Boolean {
val status = lowercase()
return !status.contains("offline") && !status.contains("not connected") && !status.contains("failed") && !status.contains("error")
}
private fun String.voiceGatewayLabel(): String = if (isVoiceGatewayReady()) "Connected and ready" else "Gateway not connected"
private fun Context.hasRecordAudioPermission(): Boolean = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED

View File

@@ -141,7 +141,7 @@ fun ChatComposer(
if (!healthOk) {
Text(
text = "Gateway is offline. Connect first in the Connect tab.",
text = "Gateway is offline. Open Settings to reconnect.",
style = mobileCallout,
color = ai.openclaw.app.ui.mobileWarning,
)

View File

@@ -0,0 +1,853 @@
package ai.openclaw.app.ui.chat
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatMessageContent
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.ui.design.ClawListItem
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
import ai.openclaw.app.ui.design.ClawTheme
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@Composable
fun ChatScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
onVoice: () -> Unit,
) {
val messages by viewModel.chatMessages.collectAsState()
val errorText by viewModel.chatError.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
val healthOk by viewModel.chatHealthOk.collectAsState()
val sessionKey by viewModel.chatSessionKey.collectAsState()
val mainSessionKey by viewModel.mainSessionKey.collectAsState()
val thinkingLevel by viewModel.chatThinkingLevel.collectAsState()
val streamingAssistantText by viewModel.chatStreamingAssistantText.collectAsState()
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
val sessions by viewModel.chatSessions.collectAsState()
val chatDraft by viewModel.chatDraft.collectAsState()
val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
val context = LocalContext.current
val resolver = context.contentResolver
val scope = rememberCoroutineScope()
val attachments = remember { mutableStateListOf<PendingImageAttachment>() }
val pickImages =
rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
if (uris.isNullOrEmpty()) return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
val next =
uris.take(8).mapNotNull { uri ->
try {
loadSizedImageAttachment(resolver, uri)
} catch (_: Throwable) {
null
}
}
withContext(Dispatchers.Main) {
attachments.addAll(next)
}
}
}
LaunchedEffect(Unit) {
val loadSessionKey = resolveInitialChatLoadSessionKey(sessionKey, mainSessionKey)
if (loadSessionKey != null) {
viewModel.loadChat(loadSessionKey)
}
viewModel.refreshChatSessions(limit = 100)
}
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {
val accepted =
dispatchPendingAssistantAutoSend(
pendingPrompt = pendingAssistantAutoSend,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
) { prompt ->
viewModel.sendChatAwaitAcceptance(message = prompt, thinking = thinkingLevel, attachments = emptyList())
}
if (accepted) {
viewModel.clearPendingAssistantAutoSend()
}
}
var input by rememberSaveable { mutableStateOf("") }
LaunchedEffect(chatDraft) {
val draft = chatDraft?.trim()?.ifEmpty { null } ?: return@LaunchedEffect
input = draft
viewModel.clearChatDraft()
}
Column(
modifier =
Modifier
.fillMaxSize()
.padding(horizontal = 18.dp, vertical = 6.dp),
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
ChatHeader(
sessionTitle = currentSessionTitle(sessionKey = sessionKey, sessions = sessions),
thinkingLevel = thinkingLevel,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
onBack = onBack,
onMore = {
viewModel.refreshChat()
viewModel.refreshChatSessions(limit = 100)
},
)
errorText?.takeIf { it.isNotBlank() }?.let { error ->
ChatNotice(title = "Chat needs attention", body = userFacingChatError(error))
}
ChatMessageList(
messages = messages,
pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText,
healthOk = healthOk,
onStarterPrompt = { prompt -> input = prompt },
modifier = Modifier.weight(1f),
)
ChatComposer(
value = input,
onValueChange = { input = it },
attachments = attachments,
thinkingLevel = thinkingLevel,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
onThinkingLevelChange = viewModel::setChatThinkingLevel,
onPickImages = { pickImages.launch("image/*") },
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onVoice = onVoice,
onAbort = viewModel::abortChat,
onSend = {
val message = input.trim()
if (message.isEmpty() && attachments.isEmpty()) return@ChatComposer
val outgoing =
attachments.map { attachment ->
OutgoingAttachment(
type = "image",
mimeType = attachment.mimeType,
fileName = attachment.fileName,
base64 = attachment.base64,
)
}
input = ""
attachments.clear()
scope.launch {
viewModel.sendChat(message = message, thinking = thinkingLevel, attachments = outgoing)
}
},
)
}
}
@Composable
private fun ChatHeader(
sessionTitle: String,
thinkingLevel: String,
healthOk: Boolean,
pendingRunCount: Int,
onBack: () -> Unit,
onMore: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
HeaderIcon(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", onClick = onBack)
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Text(
text = sessionTitle,
style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp),
color = ClawTheme.colors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
)
ModelPill(
text =
when {
pendingRunCount > 0 -> "Working"
healthOk -> "auto"
else -> "offline"
},
status =
when {
pendingRunCount > 0 -> ClawStatus.Warning
healthOk -> ClawStatus.Neutral
else -> ClawStatus.Danger
},
)
}
HeaderIcon(icon = Icons.Default.Refresh, contentDescription = "Refresh chat", onClick = onMore)
}
}
@Composable
private fun ModelPill(
text: String,
status: ClawStatus,
) {
val borderColor =
if (status == ClawStatus.Warning) {
ClawTheme.colors.warning
} else {
ClawTheme.colors.border
}
Surface(
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.textMuted,
border = BorderStroke(1.dp, borderColor),
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 7.dp, vertical = 1.5.dp),
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
maxLines = 1,
)
}
}
@Composable
private fun HeaderIcon(
icon: androidx.compose.ui.graphics.vector.ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = Color.Transparent,
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(20.dp))
}
}
}
@Composable
private fun ChatMessageList(
messages: List<ChatMessage>,
pendingRunCount: Int,
pendingToolCalls: List<ChatPendingToolCall>,
streamingAssistantText: String?,
healthOk: Boolean,
onStarterPrompt: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val displayMessages = remember(messages) { messages.asReversed() }
val stream = streamingAssistantText?.trim()
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
listState.animateScrollToItem(index = 0)
}
LaunchedEffect(stream) {
if (!stream.isNullOrEmpty()) {
listState.scrollToItem(index = 0)
}
}
Box(modifier = modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
reverseLayout = true,
verticalArrangement = Arrangement.spacedBy(5.dp),
contentPadding = PaddingValues(top = 6.dp, bottom = 3.dp),
) {
if (!stream.isNullOrEmpty()) {
item(key = "stream") {
ChatBubble(role = "assistant", live = true, content = listOf(ChatMessageContent(text = stream)), timestampMs = null)
}
}
if (pendingToolCalls.isNotEmpty()) {
item(key = "tools") {
ToolBubble(toolCalls = pendingToolCalls)
}
}
if (pendingRunCount > 0) {
item(key = "thinking") {
ChatThinkingBubble()
}
}
items(items = displayMessages, key = { it.id }) { message ->
ChatBubble(role = message.role, live = false, content = message.content, timestampMs = message.timestampMs)
}
}
if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && stream.isNullOrBlank()) {
EmptyChatHint(healthOk = healthOk, onStarterPrompt = onStarterPrompt, modifier = Modifier.align(Alignment.Center))
}
}
}
@Composable
private fun EmptyChatHint(
healthOk: Boolean,
onStarterPrompt: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth().padding(horizontal = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(text = if (healthOk) "Ready when you are" else "Gateway offline", style = ClawTheme.type.title.copy(fontSize = 18.sp, lineHeight = 23.sp), color = ClawTheme.colors.text)
Text(
text =
if (healthOk) {
"Start with a prompt, or use voice."
} else {
"Reconnect from Settings to send messages."
},
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
)
}
if (healthOk) {
StarterPromptList(onStarterPrompt = onStarterPrompt)
}
}
}
@Composable
private fun StarterPromptList(onStarterPrompt: (String) -> Unit) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
starterPrompts.forEachIndexed { index, prompt ->
StarterPromptRow(prompt = prompt, onClick = { onStarterPrompt(prompt.message) })
if (index != starterPrompts.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
}
@Composable
private fun StarterPromptRow(
prompt: StarterPrompt,
onClick: () -> Unit,
) {
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp).padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Box(
modifier =
Modifier
.size(30.dp)
.background(ClawTheme.colors.surfacePressed, RoundedCornerShape(ClawTheme.radii.row)),
contentAlignment = Alignment.Center,
) {
Text(text = prompt.mark, style = ClawTheme.type.label, color = ClawTheme.colors.text)
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = prompt.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = prompt.subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
}
private data class StarterPrompt(
val mark: String,
val title: String,
val subtitle: String,
val message: String,
)
private val starterPrompts =
listOf(
StarterPrompt(mark = "1", title = "Catch me up", subtitle = "Summarize recent sessions and next steps.", message = "Catch me up on my recent OpenClaw sessions and suggest next steps."),
StarterPrompt(mark = "2", title = "Plan the work", subtitle = "Turn a goal into an actionable checklist.", message = "Help me turn this goal into a practical checklist: "),
StarterPrompt(mark = "3", title = "Use this phone", subtitle = "Ask OpenClaw to use Android capabilities.", message = "What can you help me do from this phone right now?"),
)
@Composable
private fun ChatBubble(
role: String,
live: Boolean,
content: List<ChatMessageContent>,
timestampMs: Long?,
) {
val normalizedRole = role.trim().lowercase(Locale.US)
val isUser = normalizedRole == "user"
val displayableContent =
content.filter { part ->
when (part.type) {
"text" -> !part.text.isNullOrBlank()
else -> part.base64 != null
}
}
if (displayableContent.isEmpty()) return
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start,
) {
Surface(
modifier = Modifier.fillMaxWidth(if (isUser) 0.64f else 0.56f),
shape = RoundedCornerShape(7.dp),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, if (live) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.5.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text =
when {
live -> "OpenClaw · Live"
isUser -> "You"
normalizedRole == "system" -> "System"
else -> "OpenClaw"
},
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
color = ClawTheme.colors.text,
)
displayableContent.forEach { part ->
if (part.type == "text") {
ChatText(text = part.text.orEmpty(), textColor = ClawTheme.colors.text)
} else {
Text(text = part.fileName ?: "Attachment", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
timestampMs?.let {
Text(
text = formatChatTimestamp(it),
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textMuted,
modifier = Modifier.align(Alignment.End),
)
}
}
}
}
}
@Composable
private fun ChatText(
text: String,
textColor: Color,
) {
if (text.hasMarkdownSyntax()) {
ChatMarkdown(text = text, textColor = textColor)
} else {
Text(
text = text,
style = ClawTheme.type.body,
color = textColor,
)
}
}
@Composable
private fun ToolBubble(toolCalls: List<ChatPendingToolCall>) {
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
ClawStatusPill(text = "Tools running", status = ClawStatus.Warning)
toolCalls.take(4).forEach { tool ->
ClawListItem(title = tool.name, subtitle = "OpenClaw is working")
}
if (toolCalls.size > 4) {
Text(text = "+${toolCalls.size - 4} more", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
}
}
}
}
@Composable
private fun ChatThinkingBubble() {
ClawPanel {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ClawStatusPill(text = "Thinking", status = ClawStatus.Warning)
Text(text = "OpenClaw is preparing a response.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
@Composable
private fun ChatNotice(
title: String,
body: String,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surface,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 11.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Box(modifier = Modifier.size(6.dp).background(ClawTheme.colors.warning, CircleShape))
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = body, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
}
@Composable
private fun ChatComposer(
value: String,
onValueChange: (String) -> Unit,
attachments: List<PendingImageAttachment>,
thinkingLevel: String,
healthOk: Boolean,
pendingRunCount: Int,
onThinkingLevelChange: (String) -> Unit,
onPickImages: () -> Unit,
onRemoveAttachment: (String) -> Unit,
onVoice: () -> Unit,
onAbort: () -> Unit,
onSend: () -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth().imePadding(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (attachments.isNotEmpty()) {
AttachmentStrip(attachments = attachments, onRemoveAttachment = onRemoveAttachment)
}
ChatContextMeter(thinkingLevel = thinkingLevel, onClick = { onThinkingLevelChange(nextThinkingValue(thinkingLevel)) })
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ChatInputPill(value = value, onValueChange = onValueChange, onPickImages = onPickImages, onVoice = onVoice, modifier = Modifier.weight(1f))
SendButton(
enabled = healthOk && pendingRunCount == 0 && (value.trim().isNotEmpty() || attachments.isNotEmpty()),
onClick = onSend,
)
}
if (pendingRunCount > 0) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Surface(
onClick = onAbort,
modifier = Modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.text,
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(modifier = Modifier.size(8.dp).background(ClawTheme.colors.danger, RoundedCornerShape(2.dp)))
Text(text = "Stop", style = ClawTheme.type.label)
}
}
}
}
}
}
@Composable
private fun ChatContextMeter(
thinkingLevel: String,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.width(178.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(7.dp),
) {
Surface(
onClick = onClick,
modifier = Modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.text,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(12.dp), tint = ClawTheme.colors.textSubtle)
Text(text = "Context ${contextPercent(thinkingLevel)}%", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
}
}
Box(
modifier =
Modifier
.weight(1f)
.height(3.dp)
.background(ClawTheme.colors.surfacePressed, RoundedCornerShape(999.dp)),
) {
Box(
modifier =
Modifier
.fillMaxWidth(thinkingMeterWidth(thinkingLevel))
.height(3.dp)
.background(ClawTheme.colors.primary, RoundedCornerShape(999.dp)),
)
}
}
}
@Composable
private fun ChatInputPill(
value: String,
onValueChange: (String) -> Unit,
onPickImages: () -> Unit,
onVoice: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 9.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(7.dp),
) {
Surface(onClick = onPickImages, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = ClawTheme.colors.surfaceRaised, contentColor = ClawTheme.colors.text) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.AttachFile, contentDescription = "Attach image", modifier = Modifier.size(16.dp))
}
}
Box(modifier = Modifier.weight(1f)) {
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = ClawTheme.type.body.copy(color = ClawTheme.colors.text),
cursorBrush = SolidColor(ClawTheme.colors.primary),
minLines = 1,
maxLines = 4,
modifier = Modifier.fillMaxWidth(),
decorationBox = { innerTextField ->
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
if (value.isEmpty()) {
Text(text = "Message OpenClaw", style = ClawTheme.type.body, color = ClawTheme.colors.textSubtle)
}
innerTextField()
}
},
)
}
Surface(
onClick = onVoice,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.Mic, contentDescription = "Voice", modifier = Modifier.size(18.dp))
}
}
}
}
}
@Composable
private fun AttachmentStrip(
attachments: List<PendingImageAttachment>,
onRemoveAttachment: (String) -> Unit,
) {
Row(modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
attachments.forEach { attachment ->
AttachmentChip(fileName = attachment.fileName, onRemove = { onRemoveAttachment(attachment.id) })
}
}
}
@Composable
private fun AttachmentChip(
fileName: String,
onRemove: () -> Unit,
) {
Surface(
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(start = 9.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(text = fileName, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
Surface(onClick = onRemove, modifier = Modifier.size(22.dp), shape = CircleShape, color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.Close, contentDescription = "Remove attachment", modifier = Modifier.size(13.dp))
}
}
}
}
}
private fun currentSessionTitle(
sessionKey: String,
sessions: List<ai.openclaw.app.chat.ChatSessionEntry>,
): String {
val entry = sessions.firstOrNull { it.key == sessionKey }
val name = entry?.displayName?.takeIf { it.isNotBlank() } ?: return "New chat"
return friendlySessionName(name)
}
@Composable
private fun SendButton(
enabled: Boolean,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
enabled = enabled,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = if (enabled) ClawTheme.colors.primary else ClawTheme.colors.surfacePressed,
contentColor = if (enabled) ClawTheme.colors.primaryText else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, if (enabled) ClawTheme.colors.primary else ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "Send", modifier = Modifier.size(18.dp))
}
}
}
private fun userFacingChatError(error: String): String {
val lower = error.lowercase(Locale.US)
return when {
lower.contains("not connected") -> "Gateway is offline. Open Settings to reconnect."
lower.contains("unauthorized") || lower.contains("auth") -> "Gateway authentication needs attention."
else -> error
}
}
private fun thinkingDisplay(value: String): String =
when (value.lowercase(Locale.US)) {
"low" -> "Low"
"medium" -> "Medium"
"high" -> "High"
else -> "Off"
}
private fun thinkingValue(display: String): String =
when (display.lowercase(Locale.US)) {
"low" -> "low"
"medium" -> "medium"
"high" -> "high"
else -> "off"
}
private fun nextThinkingValue(value: String): String =
when (value.lowercase(Locale.US)) {
"off" -> "low"
"low" -> "medium"
"medium" -> "high"
else -> "off"
}
private fun thinkingMeterWidth(value: String): Float =
when (value.lowercase(Locale.US)) {
"low" -> 0.34f
"medium" -> 0.58f
"high" -> 0.82f
else -> 0.18f
}
private fun contextPercent(value: String): Int = (thinkingMeterWidth(value) * 100).toInt()
private fun formatChatTimestamp(timestampMs: Long): String = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(Date(timestampMs))
private fun String.hasMarkdownSyntax(): Boolean =
any { it == '#' || it == '*' || it == '`' || it == '[' || it == '|' } ||
contains("\n- ") ||
contains("\n1. ")

View File

@@ -71,6 +71,16 @@ internal suspend fun dispatchPendingAssistantAutoSend(
return dispatch(prompt)
}
internal fun resolveInitialChatLoadSessionKey(
sessionKey: String,
mainSessionKey: String,
): String? {
val current = sessionKey.trim()
val main = mainSessionKey.trim().ifEmpty { "main" }
if (current.isNotEmpty() && current != "main" && current != main) return null
return main
}
@Composable
fun ChatSheetContent(viewModel: MainViewModel) {
val messages by viewModel.chatMessages.collectAsState()
@@ -87,7 +97,10 @@ fun ChatSheetContent(viewModel: MainViewModel) {
val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadChat(mainSessionKey)
val loadSessionKey = resolveInitialChatLoadSessionKey(sessionKey, mainSessionKey)
if (loadSessionKey != null) {
viewModel.loadChat(loadSessionKey)
}
}
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {

View File

@@ -0,0 +1,543 @@
package ai.openclaw.app.ui.design
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
internal enum class ClawStatus {
Neutral,
Success,
Warning,
Danger,
}
@Composable
internal fun ClawScaffold(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(horizontal = ClawTheme.spacing.lg, vertical = ClawTheme.spacing.lg),
content: @Composable () -> Unit,
) {
Box(
modifier =
modifier
.fillMaxSize()
.background(ClawTheme.colors.canvas)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(contentPadding),
) {
content()
}
}
@Composable
internal fun ClawSectionHeader(
title: String,
modifier: Modifier = Modifier,
action: (@Composable () -> Unit)? = null,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = title,
style = ClawTheme.type.section,
color = ClawTheme.colors.text,
)
action?.invoke()
}
}
@Composable
internal fun ClawPrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
colors =
ButtonDefaults.buttonColors(
containerColor = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
disabledContainerColor = ClawTheme.colors.surfacePressed,
disabledContentColor = ClawTheme.colors.textSubtle,
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp, pressedElevation = 0.dp),
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(8.dp))
}
Text(text = text, style = ClawTheme.type.label, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
@Composable
internal fun ClawSecondaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
) {
Surface(
onClick = onClick,
enabled = enabled,
modifier = modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = if (enabled) ClawTheme.colors.surfaceRaised else ClawTheme.colors.surface,
contentColor = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, if (enabled) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(7.dp))
}
Text(text = text, style = ClawTheme.type.label, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
@Composable
internal fun ClawIconButton(
icon: ImageVector,
contentDescription: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Surface(
onClick = onClick,
enabled = enabled,
modifier = modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
color = if (enabled) ClawTheme.colors.surfaceRaised else ClawTheme.colors.surface,
contentColor = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = contentDescription, modifier = Modifier.size(18.dp))
}
}
}
@Composable
internal fun ClawStatusPill(
text: String,
status: ClawStatus,
modifier: Modifier = Modifier,
) {
val colors = ClawTheme.colors
val (dotColor, backgroundColor) =
when (status) {
ClawStatus.Neutral -> colors.textSubtle to colors.surfaceRaised
ClawStatus.Success -> colors.success to colors.successSoft
ClawStatus.Warning -> colors.warning to colors.warningSoft
ClawStatus.Danger -> colors.danger to colors.dangerSoft
}
Surface(
modifier = modifier,
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = backgroundColor,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(7.dp),
) {
Box(
modifier =
Modifier
.size(6.dp)
.clip(CircleShape)
.background(dotColor),
)
Text(text = text, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@Composable
internal fun ClawPill(
text: String,
modifier: Modifier = Modifier,
selected: Boolean = false,
onClick: (() -> Unit)? = null,
) {
val surfaceModifier =
if (onClick == null) {
modifier
} else {
modifier.clickable(onClick = onClick)
}
Surface(
modifier = surfaceModifier,
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = if (selected) ClawTheme.colors.primary else ClawTheme.colors.surfaceRaised,
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted,
border = BorderStroke(1.dp, if (selected) ClawTheme.colors.primary else ClawTheme.colors.border),
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = ClawTheme.type.caption,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
internal fun <T> ClawListPanel(
items: List<T>,
modifier: Modifier = Modifier,
row: @Composable (T) -> Unit,
) {
ClawPanel(modifier = modifier, contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawSeparatedColumn(items = items, row = row)
}
}
@Composable
internal fun <T> ClawSeparatedColumn(
items: List<T>,
modifier: Modifier = Modifier,
row: @Composable (T) -> Unit,
) {
Column(modifier = modifier) {
items.forEachIndexed { index, item ->
row(item)
if (index != items.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
}
}
}
}
@Composable
internal fun ClawDetailRow(
title: String,
subtitle: String,
modifier: Modifier = Modifier,
leading: @Composable () -> Unit,
trailing: @Composable () -> Unit,
) {
Row(
modifier =
modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.padding(horizontal = 12.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
leading()
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = subtitle, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
trailing()
}
}
@Composable
internal fun ClawTextBadge(
text: String,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.size(30.dp),
shape = CircleShape,
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.border),
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.Center) {
Text(text = text, style = ClawTheme.type.label, color = ClawTheme.colors.text, maxLines = 1)
}
}
}
@Composable
internal fun ClawIconBadge(
icon: ImageVector,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.size(30.dp),
shape = CircleShape,
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.border),
contentColor = ClawTheme.colors.text,
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(14.dp), tint = ClawTheme.colors.text)
}
}
}
@Composable
internal fun ClawListItem(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
metadata: String? = null,
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null,
) {
val rowModifier =
if (onClick == null) {
modifier
} else {
modifier.clickable(onClick = onClick)
}
Row(
modifier =
rowModifier
.fillMaxWidth()
.heightIn(min = ClawTheme.spacing.touchTarget)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.padding(horizontal = 2.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
leading?.invoke()
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title,
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (subtitle != null) {
Text(
text = subtitle,
style = ClawTheme.type.caption,
color = ClawTheme.colors.textSubtle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
if (metadata != null) {
Text(text = metadata, style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle, maxLines = 1)
}
trailing?.invoke()
}
}
@Composable
internal fun ClawSegmentedControl(
options: List<String>,
selected: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier =
modifier
.clip(RoundedCornerShape(ClawTheme.radii.pill))
.border(1.dp, ClawTheme.colors.border, RoundedCornerShape(ClawTheme.radii.pill))
.padding(2.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
options.forEach { option ->
val active = option == selected
Box(
modifier =
Modifier
.weight(1f)
.clip(RoundedCornerShape(ClawTheme.radii.pill))
.background(if (active) ClawTheme.colors.primary else Color.Transparent)
.clickable { onSelect(option) }
.padding(horizontal = 9.dp, vertical = 7.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = option,
style = ClawTheme.type.caption,
color = if (active) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@Composable
internal fun ClawTextField(
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier,
minLines: Int = 1,
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier =
modifier
.fillMaxWidth()
.clip(RoundedCornerShape(ClawTheme.radii.control))
.background(ClawTheme.colors.surfaceRaised)
.border(1.dp, ClawTheme.colors.border, RoundedCornerShape(ClawTheme.radii.control))
.padding(horizontal = 11.dp, vertical = 8.dp),
textStyle = ClawTheme.type.body.copy(color = ClawTheme.colors.text),
cursorBrush = SolidColor(ClawTheme.colors.primary),
minLines = minLines,
decorationBox = { innerTextField ->
Box(modifier = Modifier.fillMaxWidth()) {
if (value.isEmpty()) {
Text(text = placeholder, style = ClawTheme.type.body, color = ClawTheme.colors.textSubtle)
}
innerTextField()
}
},
)
}
@Composable
internal fun ClawComponentShowcase(modifier: Modifier = Modifier) {
var selected by rememberSaveable { mutableStateOf("Chat") }
var prompt by rememberSaveable { mutableStateOf("") }
ClawScaffold(modifier = modifier) {
Column(verticalArrangement = Arrangement.spacedBy(18.dp)) {
ClawTopBar(
title = "OpenClaw",
subtitle = "Local command center",
navigation = { ClawAvatarMark(text = "OC") },
actions = {
ClawIconButton(icon = Icons.Default.Search, contentDescription = "Search", onClick = {})
},
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "OpenClaw", style = ClawTheme.type.display, color = ClawTheme.colors.text)
Text(text = "Design system prototype", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
ClawStatusPill(text = "Connected", status = ClawStatus.Success)
}
ClawSegmentedControl(
options = listOf("Chat", "Voice", "Sessions"),
selected = selected,
onSelect = { selected = it },
modifier = Modifier.fillMaxWidth(),
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
ClawSectionHeader(title = "Sessions")
ClawListItem(
title = "Testing testing 1 2 3",
subtitle = "14 messages · Android",
metadata = "now",
)
ClawListItem(
title = "Provider setup",
subtitle = "OpenClaw gateway",
metadata = "8m",
)
}
ClawTextField(value = prompt, onValueChange = { prompt = it }, placeholder = "Ask OpenClaw anything", minLines = 3)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ClawPrimaryButton(text = "Start Chat", onClick = {}, modifier = Modifier.weight(1f))
ClawSecondaryButton(text = "Voice", onClick = {}, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawPill(text = "Realtime", selected = true)
ClawPill(text = "Dictation")
ClawPill(text = "Screen")
}
ClawEmptyState(
title = "Nothing needs your attention",
body = "OpenClaw will surface approvals, failed jobs, and channel issues here.",
)
ClawBottomNav(
items =
listOf(
ClawNavItem(key = "overview", label = "Home", icon = Icons.Default.Home),
ClawNavItem(key = "chat", label = "Chat", icon = Icons.Default.ChatBubble),
ClawNavItem(key = "voice", label = "Voice", icon = Icons.Default.Mic),
ClawNavItem(key = "settings", label = "Settings", icon = Icons.Default.Settings),
),
selectedKey = "chat",
onSelect = {},
)
}
}
}

View File

@@ -0,0 +1,152 @@
package ai.openclaw.app.ui.design
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Immutable
internal data class ClawNavItem(
val key: String,
val label: String,
val icon: ImageVector,
)
@Composable
internal fun ClawTopBar(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
navigation: (@Composable () -> Unit)? = null,
actions: (@Composable () -> Unit)? = null,
) {
Row(
modifier =
modifier
.fillMaxWidth()
.padding(horizontal = ClawTheme.spacing.lg, vertical = ClawTheme.spacing.sm),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
navigation?.invoke()
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title,
style = ClawTheme.type.section,
color = ClawTheme.colors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (subtitle != null) {
Text(
text = subtitle,
style = ClawTheme.type.caption,
color = ClawTheme.colors.textSubtle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
actions?.invoke()
}
}
@Composable
internal fun ClawBottomNav(
items: List<ClawNavItem>,
selectedKey: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val safeInsets = WindowInsets.navigationBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Bottom)
Surface(
modifier = modifier.fillMaxWidth(),
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
border = BorderStroke(1.dp, ClawTheme.colors.border),
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
) {
Row(
modifier =
Modifier
.windowInsetsPadding(safeInsets)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
ClawBottomNavItem(
item = item,
selected = item.key == selectedKey,
onClick = { onSelect(item.key) },
modifier = Modifier.weight(1f),
)
}
}
}
}
@Composable
private fun ClawBottomNavItem(
item: ClawNavItem,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
modifier = modifier.heightIn(min = 48.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = if (selected) ClawTheme.colors.primary else Color.Transparent,
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textSubtle,
) {
Column(
modifier = Modifier.padding(horizontal = 5.dp, vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Icon(imageVector = item.icon, contentDescription = item.label, modifier = Modifier.size(18.dp))
Text(text = item.label, style = ClawTheme.type.caption, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
@Composable
internal fun ClawAvatarMark(
text: String,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.size(38.dp),
shape = CircleShape,
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Text(text = text.take(2).uppercase(), style = ClawTheme.type.label)
}
}
}

View File

@@ -0,0 +1,16 @@
package ai.openclaw.app.ui.design
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Preview(
name = "OpenClaw Design System",
showBackground = true,
backgroundColor = 0xFF030303,
)
@Composable
private fun ClawComponentShowcasePreview() {
ClawDesignTheme {
ClawComponentShowcase()
}
}

View File

@@ -0,0 +1,108 @@
package ai.openclaw.app.ui.design
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun ClawPanel(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(9.dp),
content: @Composable () -> Unit,
) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(contentPadding)) {
content()
}
}
}
@Composable
internal fun ClawSheetSurface(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(18.dp),
content: @Composable () -> Unit,
) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
color = ClawTheme.colors.surface,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(contentPadding)) {
content()
}
}
}
@Composable
internal fun ClawEmptyState(
title: String,
body: String,
modifier: Modifier = Modifier,
action: (@Composable () -> Unit)? = null,
) {
ClawPanel(modifier = modifier) {
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = body, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
action?.invoke()
}
}
}
@Composable
internal fun ClawLoadingState(
title: String,
modifier: Modifier = Modifier,
) {
ClawPanel(modifier = modifier) {
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 14.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
CircularProgressIndicator(color = ClawTheme.colors.primary, strokeWidth = 2.dp)
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
@Composable
internal fun ClawErrorState(
title: String,
body: String,
modifier: Modifier = Modifier,
action: (@Composable () -> Unit)? = null,
) {
ClawPanel(modifier = modifier) {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
ClawStatusPill(text = "Needs attention", status = ClawStatus.Danger)
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = body, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
action?.invoke()
}
}
}

View File

@@ -0,0 +1,273 @@
package ai.openclaw.app.ui.design
import ai.openclaw.app.ui.mobileFontFamily
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Immutable
internal data class ClawColors(
val canvas: Color,
val surface: Color,
val surfaceRaised: Color,
val surfacePressed: Color,
val border: Color,
val borderStrong: Color,
val text: Color,
val textMuted: Color,
val textSubtle: Color,
val primary: Color,
val primaryText: Color,
val success: Color,
val successSoft: Color,
val warning: Color,
val warningSoft: Color,
val danger: Color,
val dangerSoft: Color,
)
@Immutable
internal data class ClawSpacing(
val xxxs: Dp = 4.dp,
val xxs: Dp = 8.dp,
val xs: Dp = 12.dp,
val sm: Dp = 16.dp,
val md: Dp = 20.dp,
val lg: Dp = 24.dp,
val xl: Dp = 32.dp,
val xxl: Dp = 40.dp,
val touchTarget: Dp = 48.dp,
)
@Immutable
internal data class ClawRadii(
val row: Dp = 4.dp,
val panel: Dp = 7.dp,
val control: Dp = 8.dp,
val sheet: Dp = 12.dp,
val pill: Dp = 999.dp,
)
@Immutable
internal data class ClawTypography(
val display: TextStyle,
val title: TextStyle,
val section: TextStyle,
val body: TextStyle,
val label: TextStyle,
val caption: TextStyle,
val mono: TextStyle,
)
private val ClawDarkColors =
ClawColors(
canvas = Color(0xFF030303),
surface = Color(0xFF0A0A0A),
surfaceRaised = Color(0xFF111111),
surfacePressed = Color(0xFF1A1A1A),
border = Color(0xFF242424),
borderStrong = Color(0xFF3A3A3A),
text = Color(0xFFF8F8F8),
textMuted = Color(0xFFA8A8A8),
textSubtle = Color(0xFF707070),
primary = Color(0xFFFFFFFF),
primaryText = Color(0xFF050505),
success = Color(0xFF3EDB82),
successSoft = Color(0xFF102719),
warning = Color(0xFFE6B956),
warningSoft = Color(0xFF2B2412),
danger = Color(0xFFFF6B6B),
dangerSoft = Color(0xFF2C1414),
)
private val ClawLightColors =
ClawColors(
canvas = Color(0xFFF7F7F7),
surface = Color(0xFFFFFFFF),
surfaceRaised = Color(0xFFFFFFFF),
surfacePressed = Color(0xFFEDEDED),
border = Color(0xFFE0E0E0),
borderStrong = Color(0xFFBDBDBD),
text = Color(0xFF070707),
textMuted = Color(0xFF595959),
textSubtle = Color(0xFF8A8A8A),
primary = Color(0xFF050505),
primaryText = Color(0xFFFFFFFF),
success = Color(0xFF157A3E),
successSoft = Color(0xFFEAF8EF),
warning = Color(0xFF9A6A12),
warningSoft = Color(0xFFFFF5DD),
danger = Color(0xFFB42323),
dangerSoft = Color(0xFFFFE9E9),
)
private val LocalClawColors = staticCompositionLocalOf { ClawDarkColors }
private val LocalClawSpacing = staticCompositionLocalOf { ClawSpacing() }
private val LocalClawRadii = staticCompositionLocalOf { ClawRadii() }
private val LocalClawTypography = staticCompositionLocalOf { clawTypography(mobileFontFamily) }
internal object ClawTheme {
val colors: ClawColors
@Composable
@ReadOnlyComposable
get() = LocalClawColors.current
val spacing: ClawSpacing
@Composable
@ReadOnlyComposable
get() = LocalClawSpacing.current
val radii: ClawRadii
@Composable
@ReadOnlyComposable
get() = LocalClawRadii.current
val type: ClawTypography
@Composable
@ReadOnlyComposable
get() = LocalClawTypography.current
}
@Composable
internal fun ClawDesignTheme(
dark: Boolean = true,
content: @Composable () -> Unit,
) {
val colors = if (dark) ClawDarkColors else ClawLightColors
val typography = clawTypography(mobileFontFamily)
CompositionLocalProvider(
LocalClawColors provides colors,
LocalClawSpacing provides ClawSpacing(),
LocalClawRadii provides ClawRadii(),
LocalClawTypography provides typography,
) {
MaterialTheme(
colorScheme = clawMaterialColorScheme(colors, dark),
typography = materialTypography(typography),
shapes = Shapes(),
content = content,
)
}
}
@Composable
internal fun rememberClawDarkPreference(): Boolean = isSystemInDarkTheme()
private fun clawTypography(fontFamily: FontFamily) =
ClawTypography(
display =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Bold,
fontSize = 26.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp,
),
title =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
lineHeight = 25.sp,
letterSpacing = 0.sp,
),
section =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 15.sp,
lineHeight = 20.sp,
letterSpacing = 0.sp,
),
body =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 19.sp,
letterSpacing = 0.sp,
),
label =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 18.sp,
letterSpacing = 0.sp,
),
caption =
TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Medium,
fontSize = 12.5.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp,
),
mono =
TextStyle(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
fontSize = 13.sp,
lineHeight = 18.sp,
letterSpacing = 0.sp,
),
)
private fun materialTypography(type: ClawTypography) =
Typography(
displayMedium = type.display,
titleLarge = type.title,
titleMedium = type.section,
bodyLarge = type.body,
labelLarge = type.label,
labelSmall = type.caption,
)
private fun clawMaterialColorScheme(
colors: ClawColors,
dark: Boolean,
) = if (dark) {
darkColorScheme(
primary = colors.primary,
onPrimary = colors.primaryText,
background = colors.canvas,
onBackground = colors.text,
surface = colors.surface,
onSurface = colors.text,
surfaceVariant = colors.surfaceRaised,
onSurfaceVariant = colors.textMuted,
outline = colors.border,
error = colors.danger,
onError = colors.primaryText,
)
} else {
lightColorScheme(
primary = colors.primary,
onPrimary = colors.primaryText,
background = colors.canvas,
onBackground = colors.text,
surface = colors.surface,
onSurface = colors.text,
surfaceVariant = colors.surfaceRaised,
onSurfaceVariant = colors.textMuted,
outline = colors.border,
error = colors.danger,
onError = colors.primaryText,
)
}

View File

@@ -186,6 +186,15 @@ class MicCaptureManager(
}
}
fun cancelMicCapture() {
transcriptionDrainJob?.cancel()
transcriptionDrainJob = null
_micEnabled.value = false
_micCooldown.value = false
_liveTranscript.value = null
stop()
}
suspend fun pauseForTts() {
val shouldPause =
synchronized(ttsPauseLock) {

View File

@@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove" />
</manifest>

View File

@@ -3,4 +3,5 @@ package ai.openclaw.app
object SensitiveFeatureConfig {
const val smsEnabled: Boolean = false
const val callLogEnabled: Boolean = false
const val photosEnabled: Boolean = false
}

View File

@@ -0,0 +1,40 @@
package ai.openclaw.app
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class CronJobStatusParsingTest {
@Test
fun cronJobLastRunStatusReadsGatewayLastStatus() {
val state =
buildJsonObject {
put("lastStatus", JsonPrimitive(" error "))
put("lastRunStatus", JsonPrimitive("success"))
}
assertEquals("error", cronJobLastRunStatus(state))
}
@Test
fun cronJobLastRunStatusReadsLastRunStatus() {
val state =
buildJsonObject {
put("lastRunStatus", JsonPrimitive("error"))
}
assertEquals("error", cronJobLastRunStatus(state))
}
@Test
fun cronJobLastRunStatusIgnoresEmptyStatus() {
val state =
buildJsonObject {
put("lastStatus", JsonPrimitive(" "))
}
assertNull(cronJobLastRunStatus(state))
}
}

View File

@@ -78,4 +78,91 @@ class ChatControllerMessageIdentityTest {
assertEquals("new-2", reconciled[1].id)
assertNotEquals(reconciled[0].id, reconciled[1].id)
}
@Test
fun mergeOptimisticMessagesKeepsOutgoingUserTurnWhenHistoryOmitsIt() {
val optimistic =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "Testing testing 1 2 3")),
timestampMs = 1000L,
)
val assistant =
ChatMessage(
id = "remote-assistant",
role = "assistant",
content = listOf(ChatMessageContent(type = "text", text = "Received.")),
timestampMs = 2000L,
)
val merged = mergeOptimisticMessages(incoming = listOf(assistant), optimistic = listOf(optimistic))
assertEquals(listOf("local-user", "remote-assistant"), merged.map { it.id })
}
@Test
fun mergeOptimisticMessagesDoesNotDuplicateHistoryTurns() {
val user =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "hello")),
timestampMs = 1000L,
)
val remoteUser = user.copy(id = "remote-user")
val merged = mergeOptimisticMessages(incoming = listOf(remoteUser), optimistic = listOf(user))
assertEquals(listOf("remote-user"), merged.map { it.id })
}
@Test
fun mergeOptimisticMessagesDoesNotDuplicateGatewayPersistedUserTurnWithDifferentTimestamp() {
val optimistic =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "hello")),
timestampMs = 1000L,
)
val remoteUser = optimistic.copy(id = "remote-user", timestampMs = 2000L)
val merged = mergeOptimisticMessages(incoming = listOf(remoteUser), optimistic = listOf(optimistic))
assertEquals(listOf("remote-user"), merged.map { it.id })
}
@Test
fun mergeOptimisticMessagesKeepsRepeatedOptimisticTurnWhenHistoryOnlyHasOneMatch() {
val first =
ChatMessage(
id = "local-user-1",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "hello")),
timestampMs = 1000L,
)
val second = first.copy(id = "local-user-2", timestampMs = 1100L)
val remoteUser = first.copy(id = "remote-user", timestampMs = 2000L)
val merged = mergeOptimisticMessages(incoming = listOf(remoteUser), optimistic = listOf(first, second))
assertEquals(listOf("local-user-2", "remote-user"), merged.map { it.id })
}
@Test
fun mergeOptimisticMessagesDoesNotConsumeOlderIdenticalHistoryTurn() {
val optimistic =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "ok")),
timestampMs = 2000L,
)
val oldHistoryUser = optimistic.copy(id = "remote-old-user", timestampMs = 1000L)
val merged = mergeOptimisticMessages(incoming = listOf(oldHistoryUser), optimistic = listOf(optimistic))
assertEquals(listOf("remote-old-user", "local-user"), merged.map { it.id })
}
}

View File

@@ -335,7 +335,7 @@ class GatewaySessionInvokeTest {
connectResponseFrame(
id,
authJson =
"""{"deviceToken":"bootstrap-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"bootstrap-operator-token","role":"operator","scopes":["operator.admin","operator.approvals","operator.read","operator.talk.secrets","operator.write"]}]}""",
"""{"deviceToken":"bootstrap-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"bootstrap-operator-token","role":"operator","scopes":["operator.admin","operator.approvals","operator.pairing","operator.read","operator.talk.secrets","operator.write"]}]}""",
),
)
webSocket.close(1000, "done")
@@ -365,7 +365,7 @@ class GatewaySessionInvokeTest {
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(
listOf("operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"),
listOf("operator.approvals", "operator.pairing", "operator.read", "operator.write"),
operatorEntry?.scopes,
)
} finally {
@@ -642,7 +642,7 @@ class GatewaySessionInvokeTest {
scope = CoroutineScope(sessionJob + Dispatchers.Default),
identityStore = DeviceIdentityStore(app),
deviceAuthStore = deviceAuthStore,
onConnected = { _, _, _ ->
onConnected = {
if (!connected.isCompleted) connected.complete(Unit)
},
onDisconnected = { message ->

View File

@@ -241,7 +241,7 @@ class GatewaySessionReconnectTest {
scope = CoroutineScope(sessionJob + Dispatchers.Default),
identityStore = DeviceIdentityStore(app),
deviceAuthStore = ReconnectDeviceAuthStore(),
onConnected = { _, _, _ -> },
onConnected = {},
onDisconnected = { _ -> },
onEvent = { _, _ -> },
onInvoke = { GatewaySession.InvokeResult.ok("""{"handled":true}""") },

View File

@@ -11,6 +11,7 @@ import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCapability
import ai.openclaw.app.protocol.OpenClawLocationCommand
import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawPhotosCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -367,6 +368,21 @@ class ConnectionManagerTest {
assertEquals(false, params?.allowTOFU)
}
@Test
fun buildOperatorConnectOptions_requestsQrBootstrapHandoffScopes() {
val options = newManager().buildOperatorConnectOptions()
assertEquals(
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),
options.scopes,
)
}
@Test
fun buildNodeConnectOptions_advertisesRequestableSmsSearchWithoutSmsCapability() {
val options =
@@ -431,6 +447,7 @@ class ConnectionManagerTest {
voiceWakeMode = VoiceWakeMode.Always,
motionActivityAvailable = true,
callLogAvailable = true,
photosAvailable = true,
hasRecordAudioPermission = true,
).buildNodeConnectOptions()
@@ -438,10 +455,12 @@ class ConnectionManagerTest {
assertTrue(options.commands.contains(OpenClawLocationCommand.Get.rawValue))
assertTrue(options.commands.contains(OpenClawMotionCommand.Activity.rawValue))
assertTrue(options.commands.contains(OpenClawCallLogCommand.Search.rawValue))
assertTrue(options.commands.contains(OpenClawPhotosCommand.Latest.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.Camera.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.Location.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.Motion.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.CallLog.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.Photos.rawValue))
assertTrue(options.caps.contains(OpenClawCapability.VoiceWake.rawValue))
}
@@ -457,12 +476,13 @@ class ConnectionManagerTest {
}
@Test
fun buildNodeConnectOptions_omitsUnavailableCameraLocationAndCallLogSurfaces() {
fun buildNodeConnectOptions_omitsUnavailableCameraLocationCallLogAndPhotosSurfaces() {
val options =
newManager(
cameraEnabled = false,
locationMode = LocationMode.Off,
callLogAvailable = false,
photosAvailable = false,
).buildNodeConnectOptions()
assertFalse(options.commands.contains(OpenClawCameraCommand.List.rawValue))
@@ -470,9 +490,11 @@ class ConnectionManagerTest {
assertFalse(options.commands.contains(OpenClawCameraCommand.Clip.rawValue))
assertFalse(options.commands.contains(OpenClawLocationCommand.Get.rawValue))
assertFalse(options.commands.contains(OpenClawCallLogCommand.Search.rawValue))
assertFalse(options.commands.contains(OpenClawPhotosCommand.Latest.rawValue))
assertFalse(options.caps.contains(OpenClawCapability.Camera.rawValue))
assertFalse(options.caps.contains(OpenClawCapability.Location.rawValue))
assertFalse(options.caps.contains(OpenClawCapability.CallLog.rawValue))
assertFalse(options.caps.contains(OpenClawCapability.Photos.rawValue))
}
@Test
@@ -511,6 +533,7 @@ class ConnectionManagerTest {
readSmsAvailable: Boolean = false,
smsSearchPossible: Boolean = false,
callLogAvailable: Boolean = false,
photosAvailable: Boolean = false,
hasRecordAudioPermission: Boolean = false,
): ConnectionManager {
val context = RuntimeEnvironment.getApplication()
@@ -531,6 +554,7 @@ class ConnectionManagerTest {
readSmsAvailable = { readSmsAvailable },
smsSearchPossible = { smsSearchPossible },
callLogAvailable = { callLogAvailable },
photosAvailable = { photosAvailable },
hasRecordAudioPermission = { hasRecordAudioPermission },
manualTls = { false },
)

View File

@@ -28,7 +28,6 @@ class InvokeCommandRegistryTest {
OpenClawCapability.Notifications.rawValue,
OpenClawCapability.System.rawValue,
OpenClawCapability.Talk.rawValue,
OpenClawCapability.Photos.rawValue,
OpenClawCapability.Contacts.rawValue,
OpenClawCapability.Calendar.rawValue,
)
@@ -41,6 +40,7 @@ class InvokeCommandRegistryTest {
OpenClawCapability.CallLog.rawValue,
OpenClawCapability.VoiceWake.rawValue,
OpenClawCapability.Motion.rawValue,
OpenClawCapability.Photos.rawValue,
)
private val coreCommands =
@@ -56,7 +56,6 @@ class InvokeCommandRegistryTest {
OpenClawTalkCommand.PttStop.rawValue,
OpenClawTalkCommand.PttCancel.rawValue,
OpenClawTalkCommand.PttOnce.rawValue,
OpenClawPhotosCommand.Latest.rawValue,
OpenClawContactsCommand.Search.rawValue,
OpenClawContactsCommand.Add.rawValue,
OpenClawCalendarCommand.Events.rawValue,
@@ -74,6 +73,7 @@ class InvokeCommandRegistryTest {
OpenClawSmsCommand.Send.rawValue,
OpenClawSmsCommand.Search.rawValue,
OpenClawCallLogCommand.Search.rawValue,
OpenClawPhotosCommand.Latest.rawValue,
)
private val debugCommands = setOf("debug.logs", "debug.ed25519")
@@ -97,6 +97,7 @@ class InvokeCommandRegistryTest {
readSmsAvailable = true,
smsSearchPossible = true,
callLogAvailable = true,
photosAvailable = true,
voiceWakeEnabled = true,
motionActivityAvailable = true,
motionPedometerAvailable = true,
@@ -125,6 +126,7 @@ class InvokeCommandRegistryTest {
readSmsAvailable = true,
smsSearchPossible = true,
callLogAvailable = true,
photosAvailable = true,
motionActivityAvailable = true,
motionPedometerAvailable = true,
debugBuild = true,
@@ -145,6 +147,7 @@ class InvokeCommandRegistryTest {
readSmsAvailable = false,
smsSearchPossible = false,
callLogAvailable = false,
photosAvailable = false,
voiceWakeEnabled = false,
motionActivityAvailable = true,
motionPedometerAvailable = false,
@@ -212,6 +215,17 @@ class InvokeCommandRegistryTest {
assertFalse(capabilities.contains(OpenClawCapability.CallLog.rawValue))
}
@Test
fun advertisedPhotosSurface_respectsFeatureAvailability() {
val disabledFlags = defaultFlags(photosAvailable = false)
val enabledFlags = defaultFlags(photosAvailable = true)
assertFalse(InvokeCommandRegistry.advertisedCapabilities(disabledFlags).contains(OpenClawCapability.Photos.rawValue))
assertFalse(InvokeCommandRegistry.advertisedCommands(disabledFlags).contains(OpenClawPhotosCommand.Latest.rawValue))
assertTrue(InvokeCommandRegistry.advertisedCapabilities(enabledFlags).contains(OpenClawCapability.Photos.rawValue))
assertTrue(InvokeCommandRegistry.advertisedCommands(enabledFlags).contains(OpenClawPhotosCommand.Latest.rawValue))
}
@Test
fun advertisedCapabilities_includesVoiceWakeWithoutAdvertisingCommands() {
val capabilities = InvokeCommandRegistry.advertisedCapabilities(defaultFlags(voiceWakeEnabled = true))
@@ -244,6 +258,7 @@ class InvokeCommandRegistryTest {
readSmsAvailable: Boolean = false,
smsSearchPossible: Boolean = false,
callLogAvailable: Boolean = false,
photosAvailable: Boolean = false,
voiceWakeEnabled: Boolean = false,
motionActivityAvailable: Boolean = false,
motionPedometerAvailable: Boolean = false,
@@ -256,6 +271,7 @@ class InvokeCommandRegistryTest {
readSmsAvailable = readSmsAvailable,
smsSearchPossible = smsSearchPossible,
callLogAvailable = callLogAvailable,
photosAvailable = photosAvailable,
voiceWakeEnabled = voiceWakeEnabled,
motionActivityAvailable = motionActivityAvailable,
motionPedometerAvailable = motionPedometerAvailable,

View File

@@ -6,6 +6,7 @@ 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.OpenClawPhotosCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.protocol.OpenClawTalkCommand
import android.content.Context
@@ -201,6 +202,15 @@ class InvokeDispatcherTest {
assertEquals("CALL_LOG_UNAVAILABLE: call log not available on this build", result.error?.message)
}
@Test
fun handleInvoke_blocksPhotosWhenUnavailable() =
runTest {
val result = newDispatcher(photosAvailable = false).handleInvoke(OpenClawPhotosCommand.Latest.rawValue, null)
assertEquals("PHOTOS_UNAVAILABLE", result.error?.code)
assertEquals("PHOTOS_UNAVAILABLE: photos not available on this build", result.error?.message)
}
@Test
fun handleInvoke_treatsDebugCommandsAsUnknownOutsideDebugBuilds() =
runTest {
@@ -239,6 +249,7 @@ class InvokeDispatcherTest {
smsFeatureEnabled: Boolean = true,
smsTelephonyAvailable: Boolean = true,
callLogAvailable: Boolean = false,
photosAvailable: Boolean = true,
debugBuild: Boolean = false,
motionActivityAvailable: Boolean = false,
motionPedometerAvailable: Boolean = false,
@@ -285,6 +296,7 @@ class InvokeDispatcherTest {
smsFeatureEnabled = { smsFeatureEnabled },
smsTelephonyAvailable = { smsTelephonyAvailable },
callLogAvailable = { callLogAvailable },
photosAvailable = { photosAvailable },
debugBuild = { debugBuild },
onCanvasA2uiPush = {},
onCanvasA2uiReset = {},

View File

@@ -0,0 +1,17 @@
package ai.openclaw.app.ui
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ProviderModelStatusTest {
@Test
fun staticProviderStatusIsReady() {
assertTrue(modelProviderReady("static"))
}
@Test
fun missingProviderStatusIsNotReady() {
assertFalse(modelProviderReady("missing"))
}
}

View File

@@ -71,4 +71,25 @@ class ChatSheetContentTest {
assertTrue(consumed)
assertEquals("summarize mail", dispatchedPrompt)
}
@Test
fun initialChatLoadUsesMainWhenNoSessionIsSelected() {
assertEquals(
"agent:ops:device",
resolveInitialChatLoadSessionKey(
sessionKey = "main",
mainSessionKey = "agent:ops:device",
),
)
}
@Test
fun initialChatLoadPreservesSelectedSession() {
assertNull(
resolveInitialChatLoadSessionKey(
sessionKey = "session:history",
mainSessionKey = "agent:ops:device",
),
)
}
}

View File

@@ -256,7 +256,7 @@ class TalkModeManagerTest {
scope = CoroutineScope(sessionJob + Dispatchers.Default),
identityStore = DeviceIdentityStore(app),
deviceAuthStore = InMemoryDeviceAuthStore(),
onConnected = { _, _, _ -> },
onConnected = {},
onDisconnected = {},
onEvent = { _, _ -> },
)

View File

@@ -3,4 +3,5 @@ package ai.openclaw.app
object SensitiveFeatureConfig {
const val smsEnabled: Boolean = true
const val callLogEnabled: Boolean = true
const val photosEnabled: Boolean = true
}

View File

@@ -1,5 +1,13 @@
# OpenClaw iOS Changelog
## 2026.5.21 - 2026-05-21
Maintenance update for the current OpenClaw release.
## 2026.5.20 - 2026-05-20
Maintenance update for the current OpenClaw release.
## 2026.5.19 - 2026-05-19
Maintenance update for the current OpenClaw 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.19
OPENCLAW_MARKETING_VERSION = 2026.5.19
OPENCLAW_IOS_VERSION = 2026.5.21
OPENCLAW_MARKETING_VERSION = 2026.5.21
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -0,0 +1,31 @@
import Foundation
import OpenClawKit
enum GatewayOnboardingReset {
@MainActor
static func reset(
appModel: NodeAppModel,
instanceId: String,
defaults: UserDefaults = .standard)
{
appModel.disconnectGateway()
let trimmedInstanceId = instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
}
GatewaySettingsStore.clearLastGatewayConnection()
GatewaySettingsStore.clearPreferredGatewayStableID()
GatewaySettingsStore.clearLastDiscoveredGatewayStableID()
GatewayTLSStore.clearAllFingerprints()
OnboardingStateStore.reset(defaults: defaults)
defaults.set(false, forKey: "gateway.onboardingComplete")
defaults.set(false, forKey: "gateway.hasConnectedOnce")
defaults.set(false, forKey: "gateway.manual.enabled")
defaults.set("", forKey: "gateway.manual.host")
defaults.set("", forKey: "gateway.setupCode")
defaults.set(defaults.integer(forKey: "onboarding.requestID") + 1, forKey: "onboarding.requestID")
}
}

View File

@@ -1016,10 +1016,24 @@ struct OnboardingWizardView: View {
}
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
if problem.suggestsOnboardingReset { return "Scan QR again" }
return problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
}
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
if problem.suggestsOnboardingReset {
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
self.gatewayToken = ""
self.gatewayPassword = ""
self.connectingGatewayID = nil
self.connectMessage = nil
self.issue = .none
self.pairingRequestId = nil
self.statusLine = "Scan a fresh setup QR code from this gateway."
self.step = .connect
self.showQRScanner = true
return
}
if problem.canTrustRotatedCertificate {
self.connectingGatewayID = "trust-certificate"
self.connectMessage = "Updating gateway certificate…"

View File

@@ -15,6 +15,7 @@ struct RootCanvas: View {
@AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0
@AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false
@AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
@AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false
@AppStorage("gateway.manual.host") private var manualGatewayHost: String = ""
@@ -102,6 +103,9 @@ struct RootCanvas: View {
},
retryGatewayConnection: {
Task { await self.gatewayController.connectLastKnown() }
},
resetOnboarding: {
self.resetOnboardingFromGatewayProblem()
})
.preferredColorScheme(.dark)
@@ -429,6 +433,13 @@ struct RootCanvas: View {
guard shouldPresent else { return }
self.presentedSheet = .quickSetup
}
private func resetOnboardingFromGatewayProblem() {
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
self.presentedSheet = nil
self.onboardingAllowSkip = false
self.showOnboarding = true
}
}
private struct HomeCanvasPayload: Codable {
@@ -469,6 +480,7 @@ private struct CanvasContent: View {
var openChat: () -> Void
var openSettings: () -> Void
var retryGatewayConnection: () -> Void
var resetOnboarding: () -> Void
private var brightenButtons: Bool {
self.systemColorScheme == .light
@@ -578,12 +590,15 @@ private struct CanvasContent: View {
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
if problem.canTrustRotatedCertificate { return "Trust certificate" }
if problem.suggestsOnboardingReset { return "Reset onboarding" }
return problem.retryable ? "Retry" : "Open Settings"
}
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) {
if problem.canTrustRotatedCertificate {
Task { await self.gatewayController.trustRotatedGatewayCertificate(from: problem) }
} else if problem.suggestsOnboardingReset {
self.resetOnboarding()
} else if problem.retryable {
self.retryGatewayConnection()
} else {

View File

@@ -1057,10 +1057,15 @@ struct SettingsTab: View {
}
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
if problem.suggestsOnboardingReset { return "Reset onboarding" }
return problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
}
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
if problem.suggestsOnboardingReset {
self.resetOnboarding()
return
}
if problem.canTrustRotatedCertificate {
_ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem)
return
@@ -1070,7 +1075,6 @@ struct SettingsTab: View {
private func resetOnboarding() {
// Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
self.appModel.disconnectGateway()
self.connectingGatewayID = nil
self.setupStatusText = nil
self.setupCode = ""
@@ -1082,19 +1086,7 @@ struct SettingsTab: View {
self.gatewayToken = ""
self.gatewayPassword = ""
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
}
// Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks).
GatewaySettingsStore.clearLastGatewayConnection()
GatewaySettingsStore.clearPreferredGatewayStableID()
GatewaySettingsStore.clearLastDiscoveredGatewayStableID()
// Resetting onboarding should also forget trusted gateway TLS fingerprints.
// Otherwise a restarted dev gateway can stay stuck in a local TLS cancel loop.
GatewayTLSStore.clearAllFingerprints()
OnboardingStateStore.reset()
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
// RootCanvas also short-circuits onboarding when these are true.
self.onboardingComplete = false

View File

@@ -35,6 +35,7 @@ Sources/Model/NodeAppModel+WatchNotifyNormalization.swift
Sources/Model/NodeAppModel.swift
Sources/Model/WatchReplyCoordinator.swift
Sources/Motion/MotionService.swift
Sources/Onboarding/GatewayOnboardingReset.swift
Sources/Onboarding/GatewayOnboardingView.swift
Sources/Onboarding/OnboardingStateStore.swift
Sources/Onboarding/OnboardingWizardView.swift

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.19"
"version": "2026.5.21"
}

View File

@@ -1,5 +1,5 @@
{
"originHash" : "284269c447b94311beae65318f1912f813261bfdc559185028fc1233ce288efa",
"originHash" : "92edc1a12985a9d17dc33bfc8b590ab5f5a9566bb2bdd8debdb79e6a8d445908",
"pins" : [
{
"identity" : "axorcist",
@@ -42,8 +42,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/Peekaboo.git",
"state" : {
"revision" : "41180ca7e391c2a05e7cfa9eb6390812805d4f22",
"version" : "3.0.0"
"revision" : "36108b4ea7d7848d616c3cad1e7a03e40d6be857",
"version" : "3.2.1"
}
},
{

View File

@@ -19,7 +19,7 @@ let package = Package(
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.0.0"),
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.2.1"),
.package(path: "../shared/OpenClawKit"),
.package(path: "../swabble"),
],

View File

@@ -77,7 +77,7 @@ struct AboutSettings: View {
}
}
Text("© 2025 Peter Steinberger — MIT License.")
Text("© 2026 OpenClaw Foundation — MIT License.")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.top, 4)

View File

@@ -192,8 +192,6 @@ enum HostEnvSecurityPolicy {
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"XDG_CONFIG_DIRS",
"XDG_CONFIG_HOME",
"YARN_RC_FILENAME"
]
@@ -437,8 +435,13 @@ enum HostEnvSecurityPolicy {
"VISUAL",
"WGETRC",
"WINDIR",
"XDG_CACHE_HOME",
"XDG_CONFIG_DIRS",
"XDG_CONFIG_HOME",
"XDG_DATA_DIRS",
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"XDG_STATE_HOME",
"YARN_RC_FILENAME",
"ZDOTDIR"
]

View File

@@ -163,6 +163,7 @@ private final class OpenClawPeekabooBridgeServices: PeekabooBridgeServiceProvidi
let dock: any DockServiceProtocol
let dialogs: any DialogServiceProtocol
let snapshots: any SnapshotManagerProtocol
let desktopObservation: any DesktopObservationServiceProtocol
init() {
let logging = LoggingService(subsystem: "ai.openclaw.peekaboo")
@@ -175,19 +176,29 @@ private final class OpenClawPeekabooBridgeServices: PeekabooBridgeServiceProvidi
let applications = ApplicationService(feedbackClient: feedbackClient)
let screenCapture = ScreenCaptureService(loggingService: logging)
let automation = UIAutomationService(
snapshotManager: snapshots,
loggingService: logging,
searchPolicy: .balanced,
feedbackClient: feedbackClient)
let menu = MenuService(applicationService: applications, feedbackClient: feedbackClient)
let screens = ScreenService()
self.permissions = PermissionsService()
self.snapshots = snapshots
self.applications = applications
self.screenCapture = screenCapture
self.automation = UIAutomationService(
snapshotManager: snapshots,
loggingService: logging,
searchPolicy: .balanced,
feedbackClient: feedbackClient)
self.automation = automation
self.windows = WindowManagementService(applicationService: applications, feedbackClient: feedbackClient)
self.menu = MenuService(applicationService: applications, feedbackClient: feedbackClient)
self.menu = menu
self.dock = DockService(feedbackClient: feedbackClient)
self.dialogs = DialogService(feedbackClient: feedbackClient)
self.desktopObservation = DesktopObservationService(
screenCapture: screenCapture,
automation: automation,
applications: applications,
menu: menu,
screens: screens,
snapshotManager: snapshots)
}
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.5.19</string>
<string>2026.5.21</string>
<key>CFBundleVersion</key>
<string>2026051900</string>
<string>2026052100</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -601,7 +601,6 @@ public actor GatewayChannelActor {
let allowedOperatorScopes: Set = [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
]
return Array(Set(scopes.filter { allowedOperatorScopes.contains($0) })).sorted()

View File

@@ -121,6 +121,10 @@ public struct GatewayConnectionProblem: Equatable, Sendable {
}
}
public var suggestsOnboardingReset: Bool {
self.kind == .gatewayAuthTokenMismatch
}
public var statusText: String {
switch self.kind {
case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired,

View File

@@ -70,6 +70,19 @@ import Testing
#expect(problem?.needsCredentialUpdate == false)
}
@Test func tokenMismatchSuggestsOnboardingReset() {
let error = GatewayConnectAuthError(
message: "token mismatch",
detailCode: GatewayConnectAuthDetailCode.authTokenMismatch.rawValue,
canRetryWithDeviceToken: false)
let problem = GatewayConnectionProblemMapper.map(error: error)
#expect(problem?.kind == .gatewayAuthTokenMismatch)
#expect(problem?.suggestsOnboardingReset == true)
#expect(problem?.needsCredentialUpdate == true)
}
@Test func cancelledTransportDoesNotReplaceStructuredPairingProblem() {
let pairing = GatewayConnectAuthError(
message: "pairing required",

View File

@@ -405,7 +405,6 @@ struct GatewayNodeSessionTests {
#expect(operatorEntry.scopes == [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
])

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Peter Steinberger
Copyright (c) 2026 OpenClaw Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,4 +1,4 @@
82d56352536e75291ec81540bd4d93e22aeae282e2ef864aa0f231b6deb11bba config-baseline.json
5d6aa4d0789482b1bdb6681d19fe193a8696ca25c20cbb9e07edb6d1b23ad8f2 config-baseline.core.json
e068db276fdff1727939d4f3a8001376e550c444bdff3e3443ab26812e2f8c5d config-baseline.channel.json
a87fc4c9bc6499c5fb9d9343b8c1c4f0c3381a6afbdb0a676dc8ba9e03ff5755 config-baseline.plugin.json
4f9946cf7d5afea985c8b9be185c7d8b420e038d915ce91be7476dd6541e0f08 config-baseline.json
35d17c60d2858a9cbc875807cdfc7f2fc8ba4745aa4e140a3cdf7ecf38b8a034 config-baseline.core.json
11839c7a1b858c66075156f0e203aa8367cd8321047684679a18e18b7c8fe1f7 config-baseline.channel.json
a0a88df97080adf50c2c2bccd2ca076ad43e81b24dd25f3c3cace41f09a7c8f0 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
86a7102dd571ca4c14f45c890392a95a51244e9bfba108986d87ca37bfb0b3c4 plugin-sdk-api-baseline.json
810d6807ff58ae79178dd0fadc84c10ce04813bbff738cc2fdaf12d1c2acef16 plugin-sdk-api-baseline.jsonl
bb0da3ba4560521d2c9725cd96429f64ce8e6150972ba77be71fdf8ea03e0234 plugin-sdk-api-baseline.json
4d951b989cc00a86f64907bb28d52a950a466116382c9877f24807b0fba3df44 plugin-sdk-api-baseline.jsonl

View File

@@ -51,7 +51,7 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.
- Isolated cron runs that receive the narrow cron self-cleanup grant can still read scheduler status, a self-filtered list of their current job, and that job's run history, so status/heartbeat checks can inspect their own schedule without gaining broader cron mutation access.
- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
- Isolated cron runs prefer structured execution-denial metadata from the embedded run, then fall back to known final summary/output markers such as `SYSTEM_RUN_DENIED` and `INVALID_REQUEST`, so a blocked command is not reported as a green run.
- Isolated cron runs use structured execution-denial metadata from the embedded run, including node-host `UNAVAILABLE` wrappers whose nested error message starts with `SYSTEM_RUN_DENIED` or `INVALID_REQUEST`, so a blocked command is not reported as a green run while ordinary assistant prose is not treated as a denial.
- Isolated cron runs also treat run-level agent failures as job errors even when no reply payload is produced, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful.
- When an isolated agent-turn job reaches `timeoutSeconds`, cron aborts the underlying agent run and gives it a short cleanup window. If the run does not drain, Gateway-owned cleanup force-clears that run's session ownership before cron records the timeout, so queued chat work is not left behind a stale processing session.
- If an isolated agent-turn stalls before the runner starts or before the first model call, cron records a phase-specific timeout such as `setup timed out before runner start` or `stalled before first model call (last phase: context-engine)`. These watchdogs cover embedded providers and CLI-backed providers before their external CLI process is actually started, and are capped independently from long `timeoutSeconds` values so cold-start/auth/context failures surface quickly instead of waiting for the full job budget.
@@ -90,14 +90,14 @@ This fires ~56 times per month instead of 01 times per month. OpenClaw use
| Style | `--session` value | Runs in | Best for |
| --------------- | ------------------- | ------------------------ | ------------------------------- |
| Main session | `main` | Next heartbeat turn | Reminders, system events |
| Main session | `main` | Dedicated cron wake lane | Reminders, system events |
| Isolated | `isolated` | Dedicated `cron:<jobId>` | Reports, background chores |
| Current session | `current` | Bound at creation time | Context-aware recurring work |
| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history |
<AccordionGroup>
<Accordion title="Main session vs isolated vs custom">
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). Those system events do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
**Main session** jobs enqueue a system event into a cron-owned run lane and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). They can use the target main session's last delivery context for replies, but they do not append routine cron turns to the human chat lane and do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
</Accordion>
<Accordion title="What 'fresh session' means for isolated jobs">
For isolated jobs, "fresh session" means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.

View File

@@ -352,6 +352,8 @@ By default, components are single use. Set `components.reusable=true` to allow b
To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial.
Component callbacks expire after 30 minutes by default. Set `channels.discord.agentComponents.ttlMs` to change that callback registry lifetime for the default Discord account, or `channels.discord.accounts.<accountId>.agentComponents.ttlMs` to override one account in a multi-account setup. The value is milliseconds, must be a positive integer, and is capped at `86400000` (24 hours). Longer TTLs are useful for review or approval workflows that need buttons to remain usable, but they also extend the window where an old Discord message can still trigger an action. Prefer the shortest TTL that fits the workflow, and keep the default when stale callbacks would be surprising.
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai-codex` or `vllm`.
File attachments:
@@ -1136,6 +1138,7 @@ OpenClaw uses Discord components v2 for exec approvals and cross-context markers
- `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex).
- Set per account with `channels.discord.accounts.<id>.ui.components.accentColor`.
- `channels.discord.agentComponents.ttlMs` controls how long sent Discord component callbacks remain registered (default `1800000`, maximum `86400000`). Set per account with `channels.discord.accounts.<id>.agentComponents.ttlMs`.
- `embeds` are ignored when components v2 are present.
- Plain URL previews are suppressed by default. Set `suppressEmbeds: false` on a message action when a single outbound link should expand.
@@ -1226,15 +1229,17 @@ Notes:
- `voice.mode` controls the conversation path. The default is `agent-proxy`: a realtime voice front end handles turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent through `openclaw_agent_consult`, and treats the result like a typed Discord prompt from that speaker. `stt-tts` keeps the older batch STT plus TTS flow. `bidi` lets the realtime model converse directly while exposing `openclaw_agent_consult` for the OpenClaw brain.
- `voice.agentSession` controls which OpenClaw conversation receives voice turns. Leave it unset for the voice channel's own session, or set `{ mode: "target", target: "channel:<text-channel-id>" }` to make the voice channel act as the microphone/speaker extension of an existing Discord text channel session such as `#maintainers`.
- `voice.model` overrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is separate from `voice.realtime.model`.
- `voice.followUsers` lets the bot join, move, and leave Discord voice with selected users. See [Follow users in voice](#follow-users-in-voice) for behavior rules and examples.
- `agent-proxy` routes speech through `discord-voice`, which preserves normal owner/tool authorization for the speaker and target session but hides the agent `tts` tool because Discord voice owns playback. By default, `agent-proxy` gives the consult full owner-equivalent tool access for owner speakers (`voice.realtime.toolPolicy: "owner"`) and strongly prefers consulting the OpenClaw agent before substantive answers (`voice.realtime.consultPolicy: "always"`). In that default `always` mode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence.
- In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription.
- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.voice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai-codex/gpt-5.5"`.
- Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. The supported realtime bootstrap files are limited to those profile files; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions.
- The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio.
- `voice.realtime.bargeIn` controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.
- `voice.realtime.minBargeInAudioEndMs` controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: `250`. Set `0` for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.
- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.openai.voice` or `voice.tts.providers.openai.voice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model.
- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel.
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`) for owner-gated commands and channel actions. Agent tool visibility follows the configured tool policy for the routed session.
- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent.
- `channels.discord.intents.voiceStates` can explicitly override voice-state intent subscription. Leave it unset for the intent to follow effective voice enablement.
- If `voice.autoJoin` has multiple entries for the same guild, OpenClaw joins the last configured channel for that guild.
@@ -1254,6 +1259,47 @@ Notes:
- Verbose Discord voice logs include a bounded one-line STT transcript preview for each accepted speaker segment, so debugging shows both the user side and the agent reply side without dumping unbounded transcript text.
- In `agent-proxy` mode, forced consult fallback skips likely incomplete transcript fragments such as text ending in `...` or a trailing connector like `and`, plus obvious non-actionable closings like “be right back” or “bye”. Logs show `forced agent consult skipped reason=...` when this prevents a stale queued answer.
### Follow users in voice
Use `voice.followUsers` when you want the Discord voice bot to stay with one or more known Discord users instead of joining a fixed channel at startup or waiting for `/vc join`.
```json5
{
channels: {
discord: {
voice: {
enabled: true,
followUsersEnabled: true,
followUsers: ["discord:123456789012345678"],
allowedChannels: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
},
],
},
},
},
}
```
Behavior:
- `followUsers` accepts raw Discord user IDs and `discord:<id>` values. OpenClaw normalizes both forms before matching voice-state events.
- `followUsersEnabled` defaults to `true` when `followUsers` is configured. Set it to `false` to keep the saved list but stop automatic voice following.
- When a followed user joins an allowed voice channel, OpenClaw joins that channel. When the user moves, OpenClaw moves with them. When the active followed user disconnects, OpenClaw leaves.
- If multiple followed users are in the same guild and the active followed user leaves, OpenClaw moves to another tracked followed user's channel before leaving the guild. If several followed users move at once, the latest observed voice-state event wins.
- `allowedChannels` still applies. A followed user in a disallowed channel is ignored, and a follow-owned session moves to another followed user or leaves.
- OpenClaw reconciles missed voice-state events on startup and at a bounded interval. Reconciliation samples configured guilds and caps REST lookups per run, so very large `followUsers` lists may take more than one interval to converge.
- If Discord or an admin moves the bot while it is following a user, OpenClaw rebuilds the voice session and preserves follow ownership when the destination is allowed. If the bot is moved outside `allowedChannels`, OpenClaw leaves and rejoins the configured target when one exists.
- DAVE receive recovery may leave and rejoin the same channel after repeated decrypt failures. Follow-owned sessions keep their follow ownership through that recovery path, so a later followed-user disconnect still leaves the channel.
Choose between the join modes:
- Use `followUsers` for personal or operator setups where the bot should automatically be in voice when you are.
- Use `autoJoin` for fixed-room bots that should be present even when no tracked user is in voice.
- Use `/vc join` for one-off joins or rooms where automatic voice presence would be surprising.
Native opus setup for source checkouts:
```bash
@@ -1288,6 +1334,8 @@ Default agent-proxy voice-channel session example:
voice: {
enabled: true,
model: "openai-codex/gpt-5.5",
followUsersEnabled: true,
followUsers: ["123456789012345678"],
realtime: {
provider: "openai",
model: "gpt-realtime-2",
@@ -1680,7 +1728,7 @@ Primary reference: [Configuration reference - Discord](/gateway/config-channels#
- actions: `actions.*`
- presence: `activity`, `status`, `activityType`, `activityUrl`
- UI: `ui.components.accentColor`
- features: `threadBindings`, top-level `bindings[]` (`type: "acp"`), `pluralkit`, `execApprovals`, `intents`, `agentComponents`, `heartbeat`, `responsePrefix`
- features: `threadBindings`, top-level `bindings[]` (`type: "acp"`), `pluralkit`, `execApprovals`, `intents`, `agentComponents.enabled`, `agentComponents.ttlMs`, `heartbeat`, `responsePrefix`
</Accordion>

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