Compare commits

...

880 Commits

Author SHA1 Message Date
Dallin Romney
42140a2d70 fix: preserve non-oneOf schema array order 2026-06-10 01:10:44 -07:00
Vincent Koc
3b180d5d99 fix(qa): wait for restart wake before capability check 2026-06-10 17:09:18 +09:00
Ayaan Zaidi
b9095bf70d refactor(channel): share draft chunking resolver 2026-06-10 13:35:19 +05:30
Ayaan Zaidi
049c3c4877 test(telegram): cover callback API metadata 2026-06-10 13:29:11 +05:30
Ayaan Zaidi
1265da2a5c fix(telegram): use concrete callback API calls 2026-06-10 13:29:11 +05:30
Josh Avant
cfdabfbaab Fix stale visible reply recovery (#91840)
* fix visible reply stale recovery

* fix visible recovery lint loop

* fix visible reply registry recovery

* test: cover failed visible recovery admission
2026-06-10 02:56:15 -05:00
Vincent Koc
e3fe6715af test(sessions): allow canonical recovery path aliases 2026-06-10 16:46:06 +09:00
Vincent Koc
0948bd648a test(e2e): widen kitchen sink RPC coverage 2026-06-10 16:42:47 +09:00
Vincent Koc
d07cd4c968 fix(ci): give QA builds larger runners 2026-06-10 16:42:24 +09:00
openclaw-clownfish[bot]
db5b883a9c fix(ci): include ACPX in shared live-test image
* fix(ci): include ACPX in shared live-test image

* fix(clownfish): address review for clawsweeper-commit-openclaw-openclaw-806a0119f3cd (1)

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-10 16:40:25 +09:00
Vincent Koc
fd7e181500 fix(ci): raise QA build heap limit 2026-06-10 16:35:34 +09:00
Ayaan Zaidi
3407402b2c test(plugins): guard dedicated channel sidecars 2026-06-10 13:05:24 +05:30
Ayaan Zaidi
05a0dfdd08 refactor(extensions): split channel contract sidecars 2026-06-10 13:05:24 +05:30
Ayaan Zaidi
7f9f687d82 refactor(channels): remove bundled contract fallbacks 2026-06-10 13:05:24 +05:30
Vincent Koc
7f1d82ab25 revert(sessions): defer session metadata sqlite
Reverts 538d36eaaa while preserving subsequent main changes. The beta-only SQLite downgrade rescue and reverse migration remain excluded.
2026-06-10 16:34:06 +09:00
Andy Ye
9408380ae7 fix(memory-core): keep qmd json search one-shot (#91837) 2026-06-10 16:30:15 +09:00
Vincent Koc
0a3aa5f278 fix(test): add session ids to Slack fixtures 2026-06-10 16:18:43 +09:00
Vincent Koc
a3d5e5bc72 fix(test): support macOS Bash 3 script suites 2026-06-10 15:37:15 +09:00
Vincent Koc
7cb2571a99 test(sessions): migrate fixtures to sqlite store 2026-06-10 15:35:10 +09:00
Vincent Koc
7b7e8f6e88 test(memory): stop asserting private sync return 2026-06-10 15:23:58 +09:00
Vincent Koc
60459d4061 test(chutes): normalize discovery request headers 2026-06-10 15:22:43 +09:00
Vincent Koc
54288a1e7c test(imessage): align echo cache assertion 2026-06-10 15:20:05 +09:00
brokemac79
de4b8d8ebf feat(plugins): allow installed trusted policy contracts
Allow explicitly enabled installed plugins to register declared trusted tool policies and agent tool result middleware, with trusted policy ids scoped by plugin owner.\n\nVerification covered targeted plugin/agent tests, typecheck, build, lint, local autoreview, and a Blacksmith Testbox runtime proof (tbx_01ktr1nq0rhq47fjkwrepm7fd3).
2026-06-10 16:18:23 +10:00
Vincent Koc
52bc2a12bc fix(ci): disable memory slot in release smoke config 2026-06-10 14:56:21 +09:00
Vincent Koc
ca4e4d93d2 fix(ci): use cursor pagination for closed issues 2026-06-10 14:53:40 +09:00
Vincent Koc
92418fc9da fix(memory-core): filter stale recall entries in REM harness preview 2026-06-10 14:35:44 +09:00
Ayaan Zaidi
c7b4c6bfc5 fix(gateway): handle missing launchd gui domains 2026-06-10 10:51:19 +05:30
FullerStackDev
c39dea917a fix(gateway): route headless doctor hints 2026-06-10 10:51:19 +05:30
FullerStackDev
40aad24e8a fix(gateway): surface headless LaunchAgent state 2026-06-10 10:51:19 +05:30
Vincent Koc
69a73b6278 chore(codex): bump app-server to 0.139.0 2026-06-10 14:14:53 +09:00
openclaw-clownfish[bot]
54c400a975 fix(plugin-sdk): refresh API baseline hash 2026-06-10 14:12:38 +09:00
Patrick Erichsen
e9671ed603 feat: feature openrouter in onboarding provider picker 2026-06-09 21:59:56 -07:00
kenny
b9280d5863 feat: add OpenRouter OAuth login
(cherry picked from commit dccfb60656)
2026-06-09 21:59:56 -07:00
Vincent Koc
b4cdd92119 fix(codex): avoid guardian review for local models (#88630)
* fix(codex): avoid guardian review for local models

* fix(codex): route app-server auto exec review

* fix(codex): make guardian requirements provider-aware

* fix(codex): block unrouted bound approvals

* fix(channels): satisfy ingress queue lint

* fix(codex): use local-model policy for side forks

* fix(extensions): satisfy ingress lint

* fix(codex): require trusted exec reviewer model

* fix(exec): share control command approval guards

* fix(codex): fail closed for unknown guardian model provider

* fix(codex): reject custom exec reviewer endpoints

* fix(codex): preserve bound providers on app-server reuse

* fix(codex): prefer qualified app-server model providers

* fix(codex): preserve guardian on model control switches

* fix(codex): retain local providers across model switches

* fix(codex): distrust aliased reviewer model refs

* fix(codex): preserve providers after thread rotation

* fix(codex): clear stale providers on qualified model switches

* fix(codex): prefer qualified models over legacy providers

* fix(codex): validate reviewer trust before auto approvals

* fix(codex): recompute reviewer policy after binding rotation

* fix(codex): normalize reviewer aliases before trust checks

* fix(codex): retain bound providers for slashed local models

* fix(codex): normalize provider trust checks for exec review

* fix(codex): ignore stale bindings for explicit providers

* fix(codex): share trusted reviewer endpoint policy

* fix(codex): keep network approvals on plugin path

* fix(codex): route provider-qualified model refs

* fix(codex): reject blank masked OpenAI base overrides

* fix(codex): scope exec reviewer alias trust

* fix(codex): distrust exec reviewer transport overrides
2026-06-09 21:38:22 -07:00
Patrick Erichsen
5a0b95269d docs: add plugin validation fixes guide (#91819) 2026-06-09 21:14:17 -07:00
dependabot[bot]
69b95c3447 chore(deps): bump useblacksmith/setup-docker-builder (#91666)
Bumps the actions group with 1 update: [useblacksmith/setup-docker-builder](https://github.com/useblacksmith/setup-docker-builder).


Updates `useblacksmith/setup-docker-builder` from 1.8.0 to 1.9.0
- [Release notes](https://github.com/useblacksmith/setup-docker-builder/releases)
- [Commits](722e97d12b...ab5c1da94f)

---
updated-dependencies:
- dependency-name: useblacksmith/setup-docker-builder
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 20:57:06 -07:00
Colin Johnson
bf89552e67 Improve iPad and iPhone control surfaces (#91557)
* feat(ios): expand iPad layout support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: improve iPad and iPhone control surfaces

* fix: preserve workboard dispatch compatibility

* fix: keep Talk reachable on iPad

* fix: add universal iPad app icons

* fix: address ready-review iOS feedback

* fix: avoid workboard board id shadowing

* fix ios sidebar separators

---------

Co-authored-by: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-06-09 21:46:02 -05:00
Josh Avant
a8d33f23a0 Fix context-engine compaction ownership for Codex sessions (#91590)
* fix(agents): keep context-engine compaction primary

* fix(codex): request native compaction after context engines

* test: cover 90496 compaction and reset edge cases

* fix(codex): guard secondary native compaction binding

* fix(codex): keep native compaction hint internal

* fix(codex): wait for active native turns before resume
2026-06-09 21:33:00 -05:00
Omar Shahine
6c045c5ca3 fix(imessage): surface inbound startup diagnostics (#91785)
Merged via squash.

Prepared head SHA: 597684c365

Proof:
- Focused tests, lint/type/diff checks, and autoreview passed before merge.
- ClawSweeper re-review marked proof and patch quality platinum after lobster live monitor proof.
- Maintainer accepted the diagnostics-only default-log privacy/noise tradeoff.

Lobster proof id: openclaw-lobster-live-monitor-proof-ada22165-6306-46b6-8ed0-6c94fcab6bbc

Reviewed-by: @omarshahine
2026-06-09 19:10:09 -07:00
Omar Shahine
bfccbc3fee fix(imessage): harden outbound send transport (#91783)
Merged via squash.

Prepared head SHA: 39ea25767b

Proof:
- Focused tests, docs/config generation, lint/type/doc checks passed before merge.
- ClawSweeper re-review marked proof and patch quality platinum after lobster live send proof.
- Maintainer accepted the `channels.imessage.sendTransport` config surface and compatibility-risk tradeoff.

Lobster proof id: openclaw-lobster-live-proof-c74895c2-b629-4bb0-abcb-e6521069b3d8

Reviewed-by: @omarshahine
2026-06-09 19:09:15 -07:00
Vincent Koc
9a1f2022b1 fix(security): avoid crypto hash for oauth lock names 2026-06-10 09:54:38 +09:00
Vincent Koc
5967ae61bd fix(security): audit oauth lock hash 2026-06-10 09:32:32 +09:00
Vincent Koc
48ec58a584 fix(security): remediate openclaw alerts 2026-06-10 09:02:00 +09:00
openclaw-clownfish[bot]
c0a4a7890d fix(doctor): keep TTS legacy migration on supported paths (#91787)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-10 08:54:36 +09:00
Vincent Koc
0a6a10193d fix(release): guard Parallels skip-restore lanes 2026-06-10 08:27:59 +09:00
Vincent Koc
c350c35fad fix(release): allow QA capability restore patch
(cherry picked from commit db711701d2)
2026-06-10 08:27:59 +09:00
Vincent Koc
56dc53f6d2 fix(release): harden Parallels smoke validation
(cherry picked from commit 810a821c65)
2026-06-10 08:27:59 +09:00
Dallin Romney
ec0f311f7f fix(config): clarify retired skill workshop plugin warning (#91757) 2026-06-09 16:26:02 -07:00
Agustin Rivera
f0d8048aa3 fix(search): enforce native web search tool policy (#91750)
* fix(search): enforce native web search tool policy

* fix(search): apply session policy to native web search

* fix(search): gate direct OpenAI native search

* fix(search): redact native web search provider context
2026-06-09 16:25:15 -07:00
openclaw-clownfish[bot]
54415d322f fix(ui): drain restored chat queue after session switch (#91780)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com>
2026-06-10 08:20:12 +09:00
colmbrogan
3a9ea1d85b fix(imessage): skip idle approval discovery scans (#88530)
* fix(imessage): bound idle approval discovery scans

* fix(imessage): complete bounded approval discovery

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-06-09 16:03:48 -07:00
Michael Appel
a90eb93452 Harden sandbox bind source validation (#91741) 2026-06-09 15:59:03 -07:00
clawsweeper[bot]
468db12c21 fix(mcp): lowercase SSE event-source header keys to prevent duplicate Authorization (401) (#91773)
Summary:
- The branch lowercases SSE EventSource SDK and operator header keys before merging and adds a regression test for duplicate case-variant Authorization headers.
- PR surface: Source 0, Tests +59. Total +59 across 2 files.
- Reproducibility: yes. Source inspection shows current main can preserve both lowercase `authorization` from  ... K EventSource hook and configured `Authorization`, and the PR adds a focused regression test for that path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(mcp): lowercase SSE event-source header keys to prevent duplicate…

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

Prepared head SHA: c8f7a7940e
Review: https://github.com/openclaw/openclaw/pull/91773#issuecomment-4664644390

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-06-09 22:52:29 +00:00
Patrick Erichsen
4ca6ac326e docs: remove superpowers spec draft 2026-06-09 15:51:37 -07:00
Andy Ye
9833f3ea9b fix(ui): require user intent for chat sessions (#91480)
Summary:
- The PR adds an explicit user-intent argument to `createChatSession`, updates the New Chat and `/new` action callers to pass it, adds helper regression coverage, and carries minor gateway formatting/import ordering churn.
- PR surface: Source +8, Tests +9. Total +17 across 8 files.
- Reproducibility: yes. at source level: current main lets `createChatSession(state)` reach `sessions.create`  ... ct flow, so the exact user-path reproduction remains integration-level rather than locally reproduced here.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(tasks): restore timers before maintenance apply
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into HEAD

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

Prepared head SHA: e7cd79006b
Review: https://github.com/openclaw/openclaw/pull/91480#issuecomment-4651778423

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-06-09 22:40:07 +00:00
Jacob Tomlinson
e9bd90d209 docs(security): clarify env var report scope (#91765) 2026-06-10 06:14:24 +09:00
Agustin Rivera
314de694c4 fix(mcp): harden stdio env filtering (#91751) 2026-06-09 14:08:47 -07:00
Shakker
4648701fc1 chore: fix ACP guard lint issues 2026-06-09 22:07:05 +01:00
Shakker
3b7631e50d perf: bound ACP metadata key repair lookup 2026-06-09 22:07:05 +01:00
Shakker
21104cd52e test: use valid deleted ACP bridge fixture 2026-06-09 22:07:05 +01:00
Shakker
6ab084e89d test: use valid configured ACP bridge fixture 2026-06-09 22:07:05 +01:00
Shakker
e16193bad5 fix: skip ACP metadata probe for configured agents 2026-06-09 22:07:05 +01:00
Shakker
31d49c59d7 test: require ACP metadata in resolve unit fixture 2026-06-09 22:07:05 +01:00
Shakker
ef2ca9e50e fix: repair ACP metadata in deleted-agent guard 2026-06-09 22:07:05 +01:00
Shakker
09854d9de7 fix: make ACP metadata key repair idempotent 2026-06-09 22:07:05 +01:00
Shakker
d31d26ef42 fix: validate migrated ACP metadata at canonical key 2026-06-09 22:07:05 +01:00
Shakker
440284f879 fix: rekey ACP metadata during session key migration 2026-06-09 22:07:05 +01:00
Shakker
784e86433c fix: preserve ACP metadata key during deleted-agent checks 2026-06-09 22:07:05 +01:00
Shakker
a82abc771a fix: align session resolve deleted-agent entry type 2026-06-09 22:07:05 +01:00
Shakker
a93bc61a84 fix: read canonical ACP metadata for deleted-agent guard 2026-06-09 22:07:05 +01:00
Shakker
b502a92bf1 fix: require ACP metadata for deleted-agent bypass 2026-06-09 22:07:05 +01:00
Agustin Rivera
21410d1c32 fix(codex): guard sandbox http requests (#91752)
* fix(codex): guard sandbox http requests

* fix(codex): align sandbox http policy
2026-06-09 13:54:24 -07:00
Agustin Rivera
a4e02cd1dd fix(elevated): reject group ids as senders (#91748)
* fix(elevated): reject group ids as senders

* fix(elevated): keep channel parsing out of core
2026-06-09 13:20:36 -07:00
Agustin Rivera
b6a3f2988c fix(gateway): restrict non-owner loopback tools (#91749)
* fix(gateway): restrict non-owner loopback tools

* fix(gateway): split loopback owner cache key
2026-06-09 13:15:48 -07:00
Alex Knight
bf95883812 feat(diagnostics-otel): capture tool input/output content via trusted channel (#91256)
diagnostics.otel.captureContent.{toolInputs,toolOutputs} were documented
and config-wired but never produced any span content. Emit tool args and
results over the trusted private-data diagnostic channel (mirroring the
model-content path), and have the OTel exporter bound/redact/truncate them
before span export. Raw tool content never rides the public event bus.

Scope: core embedded-runner tool path (canonical producer). Codex
(async-batched) and Claude CLI remain follow-ups tracked by the issue.

Refs #77391
2026-06-10 05:52:52 +10:00
Agustin Rivera
d2ddc26e89 fix(msteams): require admin for group actions (#91746) 2026-06-09 12:52:24 -07:00
Niels Kaspers
96a49caffa docs: clarify trusted-proxy websocket scopes (#85950) 2026-06-09 12:40:12 -07:00
Agustin Rivera
2649064548 fix(discord): require sender for moderation actions (#91745) 2026-06-09 12:33:38 -07:00
Dallin Romney
370cef2e3b docs: align Feishu DM policy defaults (#91755) 2026-06-09 12:31:47 -07:00
Dallin Romney
a2dd821908 docs: clarify matrix plugin upgrade repair (#91753) 2026-06-09 12:22:59 -07:00
Shubhankar Tripathy
443115c632 fix(config): warn for retired skill-workshop plugin entry instead of failing validation (#90244) (#90838) 2026-06-09 12:20:34 -07:00
openclaw-release-bot
5b9cb3bd3a chore(release): update appcast for 2026.6.5 2026-06-09 19:13:54 +00:00
Dallin Romney
8b84e951e5 perf(tui): prewarm runtime plugins before first send (#90782)
* perf: prewarm TUI runtime plugins before first send

* fix: satisfy TUI prewarm lint

* fix(tui): clarify runtime warmup submit block

* refactor(tui): warm embedded runtime during history load

* fix(tui): align runtime prewarm workspace
2026-06-09 11:30:28 -07:00
scotthuang
52154eda0d fix: preserve configured ACP deleted-agent guard 2026-06-09 18:31:40 +01:00
scotthuang
3853eb15af test(gateway): add store integration proof for ACP deleted-agent guard 2026-06-09 18:31:40 +01:00
scotthuang
696c1ecd20 fix(gateway): skip deleted-agent guard for ACP harness session keys
ACP session keys use agent:<harnessId>:acp:<uuid>, so sessions_send and
sessions.resolve must not treat harness ids as agents.list owners.
2026-06-09 18:31:40 +01:00
NVIDIAN
2da7dc9f2c test: cover auto-enable cache freshness 2026-06-09 18:08:28 +01:00
NVIDIAN
439b0582e2 perf(config): avoid stale implicit auto-enable cache 2026-06-09 18:08:28 +01:00
NVIDIAN
f13b6ea151 perf(config): dedupe plugin auto-enable fanout work 2026-06-09 18:08:28 +01:00
Shakker
56d201fa67 fix: retry workflow sanity checkout fetches 2026-06-09 17:39:01 +01:00
Shakker
9bb68b55dd fix: avoid gateway restart for tui footer config 2026-06-09 17:35:03 +01:00
Shakker
d48778994f fix: gate tui host footer behind config 2026-06-09 17:35:03 +01:00
WB
479e2aaae3 fix(tui): show connection host in footer 2026-06-09 17:35:03 +01:00
brokemac79
1893a0727a fix(status): restore Codex synthetic usage line
Restores the Codex/OpenAI usage line in status by routing Codex-harness usage through the Codex app-server provider hook. Preserves configured app-server startup options, selected OpenAI/Codex auth profiles, weekly-window cadence, and Codex credit wording while skipping unsupported API-key usage probes. Fixes #91694.
2026-06-10 01:32:33 +09:00
Shakker
61e93a800d fix: bound model catalog state cache 2026-06-09 17:27:33 +01:00
Shakker
da7f9c51df fix: isolate model catalog cache contexts 2026-06-09 17:27:33 +01:00
Shakker
55156a1241 fix: keep model catalog cache keys current 2026-06-09 17:27:33 +01:00
ai-hpc
cc856cde1b fix(models): refresh persisted catalog cache keys 2026-06-09 17:27:33 +01:00
ai-hpc
4106f446bd fix(models): persist agent catalog cache 2026-06-09 17:27:33 +01:00
Jacob Tomlinson
8c3ba33463 fix(mattermost): keep default replies in existing threads
Restores the documented Mattermost default where replyToMode="off" does not start new threads for top-level messages, but still preserves replies that arrive inside an existing Mattermost thread.

Manual Mattermost proof and focused monitor tests cover threaded channel replies, top-level off-mode messages, and direct messages.
2026-06-09 17:08:51 +01:00
Pavan Kumar Gondhi
86bab9699d fix: block git protocol env controls [AI] (#91619)
* fix: block git protocol env controls

* fix: preserve restrictive git protocol env

* fix: preserve restrictive git allowlists

* fix: filter inherited git protocol allowlists

* test: cover restrictive git allowlists

* test: avoid opengrep fixture false positives

* test: type env fixture helper narrowly

* fix: preserve zero git protocol booleans

* fix: preserve invalid git protocol booleans

* fix: force git protocol from user off

* fix: share git inherited env sanitization
2026-06-09 21:09:14 +05:30
Shakker
d2a6529f04 fix: avoid mcp shutdown response snapshot allocation 2026-06-09 16:05:14 +01:00
Shakker
2dcfd9f218 fix: close mcp loopback streams on shutdown 2026-06-09 16:05:14 +01:00
Shakker
d86069ded0 test: strengthen mcp loopback transport coverage 2026-06-09 16:05:14 +01:00
Cameron Beeley
7269b26926 fix(gateway): validate Origin before auth on GET/DELETE; merge-safe test token
Addresses review:
- Reorder the new GET and DELETE branches so rejectsBrowserLoopbackRequest()
  runs BEFORE bearer auth, matching the POST path — a browser-Origin loopback
  request is now rejected (403) before auth, preserving the loopback Origin
  boundary even for unauthenticated browser requests. Added focused tests:
  browser-Origin GET and DELETE with no bearer return 403 (before auth).
- The new transport tests now read the loopback owner token from
  getActiveMcpLoopbackRuntime().ownerToken instead of resolveMcpLoopbackBearerToken,
  so they don't depend on that helper's import (which current main's test file
  no longer carries).
2026-06-09 16:05:14 +01:00
Cameron Beeley
224ea76d29 fix(gateway): keep MCP loopback stateless; add DELETE no-op + transport tests
Addresses review feedback on the Streamable HTTP transport:

- Keep the loopback server stateless: drop the advertised Mcp-Session-Id header
  (the server owns no session lifecycle, so advertising a session id clients
  would echo back was misleading). Resolves the stateless-vs-sessionful concern.
- Add DELETE /mcp as an auth-gated 200 no-op (Streamable HTTP teardown), so
  clients that send DELETE on close get a clean 200 instead of 405; Allow now
  advertises GET, POST, DELETE.
- Keep the GET/SSE notification channel (the actual fix for the 'still
  connecting' hang) auth-gated and browser-origin-rejected.
- Add focused gateway tests: GET 200 + text/event-stream, GET 401 (no auth),
  GET 403 (browser origin), DELETE 200, DELETE 401, unsupported 405 with the
  correct Allow, and POST stays stateless (no Mcp-Session-Id).
2026-06-09 16:05:14 +01:00
Cameron Beeley
cc0a18da4f fix: remove DELETE handler — loopback MCP is stateless, no sessions to terminate
The DELETE path acknowledged Mcp-Session-Id without validating,
terminating, or expiring anything. Since the loopback server is
stateless (session ID is cosmetic for spec compliance), return 405
instead of pretending to support session teardown.
2026-06-09 16:05:14 +01:00
Cameron Beeley
48b3cb69b7 fix: route GET SSE through browser-origin gate 2026-06-09 16:05:14 +01:00
Cameron Beeley
cfe0bac99a fix: flush SSE response and gate DELETE behind auth + browser-origin check
- flushHeaders() + initial SSE comment so clients don't hang
- DELETE requires bearer auth (matching GET/POST gates)
- DELETE checks browser-origin rejection (matching POST gate)
2026-06-09 16:05:14 +01:00
Cameron Beeley
7f69fe009a fix(gateway): support Streamable HTTP MCP transport (GET/SSE + DELETE)
The MCP loopback server generates config with `"type": "http"` for
Claude Code, but only handled POST requests. Claude Code's Streamable
HTTP client sends GET to open an SSE notification channel before
completing initialization. The 405 rejection on GET caused Claude Code
to hang at "still connecting" indefinitely.

- Accept GET /mcp with bearer auth, return text/event-stream (idle SSE)
- Accept DELETE /mcp for session termination (spec compliance)
- Add Mcp-Session-Id header to POST responses (spec requirement)
- Update 405 Allow header to reflect supported methods
2026-06-09 16:05:14 +01:00
Pavan Kumar Gondhi
7cdec28706 fix: block rustup toolchain env overrides [AI] (#91615)
* fix: block rustup toolchain env overrides [AI]

* test: cover inherited rustup env stripping [AI]

* fix: preserve inherited rustup env [AI]

* fix: filter ignored opengrep changed paths [AI]

* fix: honor opengrep ignored directory globs [AI]

* fix: match ignored opengrep descendants [AI]

* fix: cover rustup mirror overrides [AI]

* fix: preserve opengrep directory-only ignores [AI]

* chore: drop opengrep cleanup from rustup fix [AI]
2026-06-09 20:03:32 +05:30
Pavan Kumar Gondhi
9f413acc18 fix: expand unsafe host env denylist (#91618)
* fix: expand unsafe host env denylist

* test: annotate host env security fixtures

* test: align opengrep fixture suppressions

* test: keep opengrep suppressions inline

* test: avoid opengrep fixture call patterns
2026-06-09 19:44:54 +05:30
Ayaan Zaidi
98d5c46530 fix(agents): keep compaction notices additive 2026-06-09 18:34:37 +05:30
Vincent Koc
65848d0b45 fix(discord): restore runtime timeout compatibility exports (#91686)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 20:43:04 +09:00
Vincent Koc
9ef67ea405 test(memory): drop backend config bridge test 2026-06-09 20:12:25 +09:00
Vincent Koc
66749a3713 test: consolidate wrapper facade coverage 2026-06-09 20:12:25 +09:00
Vincent Koc
73ce4fdcbb fix(plugin-sdk): align Discord component edit facade types (#91679)
* fix(plugin-sdk): align Discord component edit facade types

* test(plugin-sdk): satisfy Discord facade type lint

* test(upgrade): seed migrated survivor sessions

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 19:58:10 +09:00
mushuiyu_xydt
a36e05050a fix #88009: [Feature]: batched memory embedding should batch over files (#89138)
Merged via squash.

Prepared head SHA: 66d362a56d
Co-authored-by: mushuiyu886 <266724580+mushuiyu886@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-09 06:38:30 -04:00
Vincent Koc
2f02bbcbb3 fix: harden legacy session SQLite migration 2026-06-09 18:44:42 +09:00
openclaw-clownfish[bot]
5e1fbca3cb docs: clarify Android opt-in for release CI (#91665)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 18:20:39 +09:00
openclaw-clownfish[bot]
e949809f6e chore(plugin-sdk): refresh API baseline hash (#91661)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 18:20:35 +09:00
Vincent Koc
25160515e0 test(runner): skip deleted changed test targets 2026-06-09 17:34:23 +09:00
Vincent Koc
af5c7c5fd0 test(agents): drop duplicate prompt template wrapper coverage 2026-06-09 17:34:23 +09:00
Vincent Koc
cd0bca0823 test(qqbot): reduce group allways command scaffolding 2026-06-09 17:34:23 +09:00
Vincent Koc
0a1cf8a776 test(vitest): prune duplicate include wrapper coverage 2026-06-09 17:34:22 +09:00
Vincent Koc
446936d600 test(ui): prune duplicate presenter coverage 2026-06-09 17:34:22 +09:00
Mason Huang
257b251e26 fix(docs): continue partial i18n batches after file errors (#91642)
Summary:
- This PR passes the existing docs-i18n `--allow-partial` flag into sequential and parallel doc-mode schedulin ... ion as terminal, adds regression tests, and removes one non-null assertion in Microsoft Foundry onboarding.
- PR surface: Source 0, Other +286. Total +286 across 3 files.
- Reproducibility: yes. at source level: current main returns from sequential doc mode on the first `processFi ... d not run Go tests because this review is read-only, but the PR adds direct regression cases for that path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(docs): continue partial i18n batches after file errors
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-9164…

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

Prepared head SHA: b66c0983b4
Review: https://github.com/openclaw/openclaw/pull/91642#issuecomment-4656851389

Co-authored-by: Mason Huang <masonxhuang@tencent.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: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-09 08:10:54 +00:00
Vincent Koc
c517162536 fix(azure): narrow Foundry Responses client detection 2026-06-09 16:45:14 +09:00
Vincent Koc
1240de7588 fix(microsoft-foundry): filter unsupported Anthropic deployments
Co-authored-by: Otto Deng <ottodeng@users.noreply.github.com>
2026-06-09 16:45:14 +09:00
Vincent Koc
93d540d67b test(azure): keep reasoning replay assertions focused
Co-authored-by: thomas.krohnfuss <thomas.krohnfuss@hsu.hamburg>
2026-06-09 16:45:14 +09:00
Vincent Koc
1727ec7b2d fix(azure): use OpenAI-compatible client for Foundry Responses
Co-authored-by: thomas.krohnfuss <thomas.krohnfuss@hsu.hamburg>
2026-06-09 16:45:14 +09:00
Vincent Koc
b08e1109c6 fix(azure): support Responses text stream events
Co-authored-by: thomas.krohnfuss <thomas.krohnfuss@hsu.hamburg>
2026-06-09 16:45:13 +09:00
cxy
d12b7b0551 feat(qqbot): add /bot-group-allways command to toggle mention requirement (#91423)
* feat(qqbot): add /bot-group-allways command to toggle group mention requirement

Add slash command to configure defaultRequireMention for qqbot accounts.
Clear runtime config snapshot cache after config write to ensure
getRuntimeConfig() reads fresh values on next message.

- Add register-group-allways command (on/off/status)
- Support named accounts and default account
- Clear runtime config cache after write for immediate effect
- Add unit tests for group config resolution

* test(qqbot): fix group allways test imports

* feat(qqbot): add /bot-group-allways command to toggle group mention requirement (#91423) (thanks @cxyhhhhh)

---------

Co-authored-by: sliverp <870080352@qq.com>
2026-06-09 15:43:12 +08:00
openclaw-clownfish[bot]
994f4f99fe fix(line): canonicalize trailing-slash webhook paths (#91649)
* fix(line): canonicalize trailing-slash webhook paths

* fix(clownfish): address review for clawsweeper-commit-openclaw-openclaw-4cf228466770 (1)

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 16:26:16 +09:00
Vincent Koc
f57c3b55fd fix(microsoft-foundry): repair CI validation issues 2026-06-09 15:45:19 +09:00
openclaw-clownfish[bot]
56fe1e0c95 docs: include plugin prerelease in release validation approval (#91637)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-09 15:42:13 +09:00
Vincent Koc
7128fa8832 fix(image-generation): preserve explicit model defaults 2026-06-09 15:34:29 +09:00
Vincent Koc
e103d1231d fix(microsoft-foundry): classify manual MAI image setup 2026-06-09 15:34:29 +09:00
Vincent Koc
a1fb8cf304 test(image-generation): load live provider sources 2026-06-09 15:34:29 +09:00
Vincent Koc
ea51c3ea50 fix(microsoft-foundry): allow MAI image deployment prefixes 2026-06-09 15:34:28 +09:00
Vincent Koc
34e76e6e6f test(image-generation): include Foundry in live image sweep 2026-06-09 15:34:28 +09:00
Vincent Koc
090c492759 fix(microsoft-foundry): configure MAI image setup defaults 2026-06-09 15:34:28 +09:00
Vincent Koc
09a5cdaca3 fix(microsoft-foundry): expose auth for image setup 2026-06-09 15:34:28 +09:00
Vincent Koc
c93e837336 fix(microsoft-foundry): require deployment refs for MAI images 2026-06-09 15:34:28 +09:00
Vincent Koc
9cf46d7e5a fix(microsoft-foundry): trust only distinct MAI model metadata 2026-06-09 15:34:28 +09:00
Vincent Koc
5e4a160f54 fix(image-generation): allow explicit defaultless model refs 2026-06-09 15:34:28 +09:00
Vincent Koc
33cac9092b fix(microsoft-foundry): honor MAI image timeout deadlines 2026-06-09 15:34:28 +09:00
Vincent Koc
0c60bad890 fix(microsoft-foundry): require MAI image deployment defaults 2026-06-09 15:34:28 +09:00
Vincent Koc
a172db54b4 fix(microsoft-foundry): allow MAI deployment ids for image generation 2026-06-09 15:34:28 +09:00
Vincent Koc
5f13d0c817 docs(microsoft-foundry): document MAI image support 2026-06-09 15:34:28 +09:00
Vincent Koc
d0a84089a0 feat(microsoft-foundry): add MAI image provider 2026-06-09 15:34:28 +09:00
Vincent Koc
1ba782f286 feat(microsoft-foundry): classify MAI model metadata 2026-06-09 15:34:28 +09:00
Onur Solmaz
3137110167 fix(memory): move local llama.cpp runtime to provider plugin
* fix(memory): move local llama.cpp runtime to provider plugin

* chore: ignore llama cpp dynamic dependency

* test: remove invalid local provider alias fixture

* chore: refresh llama cpp shrinkwrap

* chore: drop stale memory embedding defaults facade
2026-06-09 14:30:35 +08:00
Vincent Koc
4c98a547d0 docs: redirect retired app sdk pages 2026-06-09 14:57:50 +09:00
Vincent Koc
634bcf6667 docs: clarify external app integration path 2026-06-09 14:56:17 +09:00
Josh Avant
e1978cf73c fix main session startup recovery (#91566) 2026-06-09 00:37:16 -05:00
colmbrogan
7e3100a120 fix(imessage): persist echo markers before send (#88969)
Persist short-lived pending iMessage echo markers before bridge sends so self-chat reflected rows cannot race ahead of post-send echo persistence. Keep monitor cache writes post-send, keep pending text out of generic echo matching, and observe skipped from-me catchup rows for self-chat dedupe.\n\nThanks @colmbrogan.
2026-06-08 22:25:25 -07:00
Pavan Kumar Gondhi
03a8d18cd4 fix(memory-lancedb): guard memory recall output [AI] (#91425)
* fix: guard memory recall output

* fix: overfetch memory recall candidates

* fix: avoid memory recall lint shadow
2026-06-09 10:31:55 +05:30
Vincent Koc
6fcc945702 fix(agents): trim dense text delta snapshots
Trim dense plain text-delta stream snapshots for OpenAI-compatible, Responses, and Ollama providers while preserving full snapshots on stream checkpoints and terminal events.

Reconstruct partial-less text deltas in the agent loop so live message updates continue to advance for immutable snapshot providers, and document the optional text_delta.partial contract.

Fixes #86599.
2026-06-09 13:21:23 +09:00
Vincent Koc
b3c946999d perf(control-ui): lazy load slash commands
Avoid clean Control UI startup command discovery, then hydrate slash commands only on explicit slash or palette intent. Proof: local focused unit tests, mocked Gateway E2E, Testbox check:changed, autoreview clean, and GitHub CI clean.
2026-06-09 13:09:50 +09:00
Patrick Erichsen
f05e9873c6 fix: let clawhub dry runs skip publish approval (#91591) 2026-06-08 21:04:32 -07:00
Josh Avant
9fdd56da21 fix(openai): require api-key auth for realtime voice (#91567)
* fix(openai): require api-key auth for realtime voice

* test(plugin-sdk): avoid auth profile store shadowing
2026-06-08 22:55:06 -05:00
BCM
4c55dd8549 fix(ui): guard WebRTC Talk startup cancellation
Stop stale async WebRTC setup after a user cancellation so a late microphone grant does not dereference a nulled peer or continue SDP setup.

Fixes #89434
2026-06-09 12:46:31 +09:00
Mason Huang
162957565a fix: make docs i18n frontmatter translation resilient (#91578)
Summary:
- The PR updates the docs i18n Go translator to bypass exact glossary matches, recover non-empty Codex last-me ... r a non-zero exit, fall back to source frontmatter scalars on translation errors, and add regression tests.
- PR surface: Other +201. Total +201 across 4 files.
- Reproducibility: yes. Current main source shows the relevant paths: exact glossary scalars still go through  ... re the last-message file; the PR body also describes a real translator smoke run that hit the failure mode.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: make docs i18n frontmatter translation resilient

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

Prepared head SHA: efd98bba14
Review: https://github.com/openclaw/openclaw/pull/91578#issuecomment-4655639231

Co-authored-by: Mason Huang <masonxhuang@tencent.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: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-09 03:43:01 +00:00
Sally O'Malley
c8a8152cd7 fix docker store seed target packages (#91547) 2026-06-08 23:38:46 -04:00
T-800
84acb74a6a fix(feishu): retry on send rate-limit errors (230020/230006) (#89659)
* fix(feishu): add retry with linear backoff for send rate-limit errors

When Feishu returns code 230020 (per-chat rate limit), requestFeishuApi
now retries up to 2 times with linear backoff (500ms, 1000ms). The reply
path (im.message.reply) is also covered via the same retry helper.

Confirmed by a real 20-concurrent-send stress test: all 20 messages
succeed after retry.

Closes #70879

* ci: retrigger CI

* fix(feishu): retry HTTP 429 and code 11232 for message send rate limits

Feishu Open API has three send-time rate limit signals: HTTP 429
(gateway-wide quota), business code 11232 (tenant-level message
service: 100/min, 5/sec), and 230020 (per-chat). Previously only
230020 was retried; HTTP 429 and 11232 propagated as fatal errors.

- Add 11232 to FEISHU_SEND_RATE_LIMIT_CODES.
- In getFeishuSendRateLimitCode, recognize HTTP 429 before reading
  the body code so gateway-level limits enter the retry loop.
- Update doc comment listing both gateway and business sources.

* test(feishu): add focused retry coverage for 11232 and HTTP 429

The previous send.retry.test.ts only exercised 230020 / 230006 / non-rate
codes / plain errors. After expanding the retry policy in 90c787096 to
cover code 11232 (tenant-level message rate limit) and gateway-level
HTTP 429, ClawSweeper review #89659 (P2) flagged the tests as no longer
matching the production behavior.

- getFeishuSendRateLimitCode: assert 11232 returns 11232, HTTP 429
  returns 429, and HTTP 429 wins over body code when both are present.
- requestFeishuApi: cover 11232 retry-then-success, 429 retry-then-success,
  exhaustion paths for both, and a mixed 230020 → 11232 → ok recovery.

* fix(feishu): retry on fulfilled rate-limit response bodies (no-throw)

The Feishu node SDK sometimes resolves a non-throwing response that
carries a rate-limit code in its body (e.g. { code: 11232, msg: ... })
instead of rejecting. requestFeishuApi previously returned that body
straight away and downstream assertFeishuMessageApiSuccess failed once
with no retry — the same shape that issue #28157 fixed earlier on the
typing/reaction path via getBackoffCodeFromResponse.

ClawSweeper review on #89659 (P1, comment-shared.ts:140) flagged the
gap. Mirror the typing-path pattern for the send helper:

- Add getFeishuSendRateLimitCodeFromResponse to classify fulfilled
  bodies against FEISHU_SEND_RATE_LIMIT_CODES (230020, 11232).
- In requestFeishuApi, after each fulfilled await, classify before
  returning. If the body is a retryable rate limit and there are
  attempts left, continue the loop. After exhaustion, wrap the last
  fulfilled body into a synthetic AxiosError-shaped error so callers
  see the same error shape as the throw path.
- Add 11 focused tests covering fulfilled 11232/230020 retry-then-ok,
  exhaustion, mixed throw → fulfilled → ok recovery, and pass-through
  for code 0 / non-rate-limit codes.

* fix(feishu): break loop on final-attempt fulfilled rate-limit body

ClawSweeper review on dc8d3be7d (P1, comment-shared.ts:166) caught a
real bug: when the final retry attempt also fulfilled with a rate-limit
body (e.g. { code: 11232, ... }), the guard `attempt < FEISHU_SEND_MAX_RETRIES`
was false so control fell through to `return result` — bypassing the
synthetic-error exhaustion path and handing the rate-limit body to the
caller as if it were a successful response. The fulfilled-exhaustion
test missed this because Vitest's local fs module cache served the
pre-fix shape; running with a fresh cache reproduces the failure.

Split the fulfilled-rate-limit branch so the body is always captured,
then continue on a non-final attempt or break on the final attempt.
Breaking falls through to the synthetic AxiosError-shaped throw below,
which is exactly what the existing exhaustion test asserts.

* fix(feishu): retry on send rate-limit errors 230020/11232/429 (#89659) (thanks @ladygege)

---------

Co-authored-by: marshall.m <marshall.m@binance.com>
Co-authored-by: sliverp <870080352@qq.com>
2026-06-09 11:34:21 +08:00
Vincent Koc
9bbde70458 fix(skills): avoid per-file skill watchers 2026-06-09 12:27:53 +09:00
Vincent Koc
099abea089 perf(control-ui): warn on slow first replies (#91583) 2026-06-09 12:26:00 +09:00
Patrick Erichsen
e8cf6df3a3 feat: dogfood reusable ClawHub package publish 2026-06-08 20:19:21 -07:00
Youssef Hemimy
9210d8f7d9 fix(whatsapp): route captured replies through successor controller after restart (#85823)
Merged via squash.

Prepared head SHA: 5df8c79654
Co-authored-by: itsuzef <53057646+itsuzef@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-06-09 00:05:44 -03:00
Vincent Koc
c9050c982d perf(control-ui): trace first assistant event
Add Control UI first-assistant chat send timing instrumentation and tests.
2026-06-09 12:03:30 +09:00
Vincent Koc
0933726574 fix(sdk): surface event pump failures 2026-06-09 11:50:53 +09:00
Josh Avant
9f48254f09 Fix config.patch explicit array replacement (#91551)
* fix config patch explicit array replacement

* fix generated config patch protocol model

* fix config patch test helper typing

* fix shared auth patch replacement tests

* update config patch prompt snapshots

* harden qa lab config patch replace paths
2026-06-08 21:48:46 -05:00
Vincent Koc
329fa44d23 fix(memory-core): write deep sleep summaries to dreams 2026-06-09 11:31:31 +09:00
Josh Avant
aef1fad58d Fix transcript image redaction (#91529)
* fix transcript image redaction

* fix image redaction type predicate

* tighten transcript image redaction boundary
2026-06-08 21:23:15 -05:00
harjoth
82afc4678a fix(config): use Start-Process -FilePath for Windows config opener (#90157) 2026-06-09 11:16:15 +09:00
Ayaan Zaidi
aa935ddeb2 fix(telegram): keep compact command replies visible 2026-06-09 07:43:01 +05:30
Ayaan Zaidi
fff5261ade fix(telegram): keep compact acknowledgements in dispatcher 2026-06-09 07:43:01 +05:30
joelnishanth
5ef0d6c693 fix: resolve CI lint/type/deadcode failures
- Remove unused import normalizeOptionalLowercaseString from commands-compact.ts
- Remove unused type import ReplyPayload from bot-native-commands.ts
- Replace spread-in-map with Object.assign to satisfy oxlint
- Delete orphaned native-command-ack-fallback.ts and its test (superseded by direct delivery)
- Add isStatusNotice to ReplyPayloadLike test type
- Update test to verify status notices bypass dispatch pipeline
2026-06-09 07:43:01 +05:30
joelnishanth
38a11944f4 fix(telegram): deliver native /compact ack directly, bypassing dispatch pipeline
The dispatch pipeline (foreground fence, operation-busy checks, hooks)
silently dropped status-notice replies for /compact. Resolve the command
reply directly via getReplyFromConfig and deliver status notices through
deliverReplies without entering the full dispatch pipeline. Non-status
commands still use the buffered dispatch for streaming/tools.

Adds deliverDespiteSourceReplySuppression metadata for command replies,
a dedicated native-command-ack-fallback module, and regression tests.

Fixes #89525

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 07:43:01 +05:30
joelnishanth
1559c16a76 fix(telegram): classify generic control commands as text slash
Generic message ingress lacked commandSource, so /compact and other
authorized control commands were dropped when they missed bot.command.
Derive text-slash command metadata like Mattermost/iMessage.

Fixes #89525
2026-06-09 07:43:01 +05:30
Vincent Koc
280d1cb977 fix(auto-reply): deliver queued compaction notices 2026-06-09 11:07:37 +09:00
Josh Avant
14b1ebd640 fix: bound native hook relay lifetime (#91550) 2026-06-08 21:06:58 -05:00
Dallin Romney
5097749de3 fix: canonicalize codex protocol JSON assets (#91507) 2026-06-08 18:59:51 -07:00
Marcus Castro
27189b3e74 test(whatsapp): seed group activation store via facade 2026-06-08 22:58:04 -03:00
Vincent Koc
79c6136a9e perf(control-ui): avoid startup catalog wait
Start the optional model catalog load early for chat.startup and cap startup-only catalog waiting at 25ms, while preserving the 750ms optional catalog wait for other gateway surfaces. Adds regressions for slow catalog omission, async cached metadata, and agent-scoped startup metadata.
2026-06-09 10:35:39 +09:00
Vincent Koc
c4a0ca0b7a perf(agents): cache subagent registry reads 2026-06-09 10:16:15 +09:00
Vincent Koc
dfb44912ed fix(acpx): normalize Claude ACP model refs 2026-06-09 10:01:57 +09:00
Vincent Koc
80f1ae6ffe fix(infra): fail fast for sync sqlite facade execution 2026-06-09 09:55:20 +09:00
Vincent Koc
2c6bdc8b28 perf(control-ui): reuse startup model metadata (#91531) 2026-06-09 09:44:32 +09:00
brokemac79
72e40833ba fix(doctor): report managed plugin version drift
Fixes #90891.

Doctor now reports official managed plugin version drift from the daemon-local status path, using the probed running gateway version and suppressing the advisory when probe auth is skipped or unsafe. The status probe also avoids re-entering config-backed exec SecretRef credential resolution when exec refs are disabled.

Verification:
- `node scripts/run-vitest.mjs src/commands/agent-via-gateway.test.ts src/cli/daemon-cli/probe.test.ts src/cli/daemon-cli/status.gather.test.ts src/flows/doctor-health-contributions.test.ts src/commands/doctor-workspace-status.test.ts src/gateway/probe-auth.test.ts`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- Crabbox delegated Blacksmith Testbox `tbx_01ktmwa5q0c2eb688dkbkw8v2b`: `OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`
2026-06-09 09:44:01 +09:00
Shakker
5b76436c45 test: satisfy cron cancellation lint 2026-06-09 01:22:29 +01:00
Shakker
9082233a43 fix: unblock timed cron cancellation 2026-06-09 01:22:29 +01:00
Shakker
24196e05f5 fix: unwind timeout-disabled cron cancellation 2026-06-09 01:22:29 +01:00
Shakker
93313c95a5 fix: preserve cron timeout terminal state 2026-06-09 01:22:29 +01:00
Shakker
be5bfdccd1 test: remove stale cron cancel harness hook 2026-06-09 01:22:29 +01:00
Shakker
372f85d368 fix: avoid cron cancel runtime cycle 2026-06-09 01:22:29 +01:00
ai-hpc
3cf94309d9 fix(cron): keep main-session cron cancel honest 2026-06-09 01:22:29 +01:00
ai-hpc
f45cd5e57e test(cron): type unresolved runner mock 2026-06-09 01:22:29 +01:00
ai-hpc
c13802c912 fix(cron): preserve timeout cleanup after cancel 2026-06-09 01:22:29 +01:00
ai-hpc
c3cdd4971b fix(cron): cancel active cron task runs 2026-06-09 01:22:29 +01:00
Vincent Koc
5f6ee9f913 fix(release): prepare ClawHub publish deps after target checkout 2026-06-09 08:48:07 +09:00
Vincent Koc
ebb9c6a013 test(release): dedupe gateway migration mock 2026-06-09 01:02:24 +02:00
Vincent Koc
0df7fe3056 chore(release): keep main changelog release-owned 2026-06-09 01:02:24 +02:00
Vincent Koc
50130d32a9 test(release): align qa tool coverage gate 2026-06-09 01:02:24 +02:00
Vincent Koc
7a0e65773a test(release): ignore terminal docker stats samples 2026-06-09 01:02:24 +02:00
Vincent Koc
c7b01cf201 test(release): stabilize qa runtime parity gate 2026-06-09 01:02:24 +02:00
Vincent Koc
bad449301f test(release): align kitchen sink rpc descriptors 2026-06-09 01:02:24 +02:00
Vincent Koc
2a611865f4 ci(release): retry Docker E2E image builds 2026-06-09 01:02:24 +02:00
Vincent Koc
1019b591d5 test(release): stabilize qa gateway restart readiness 2026-06-09 01:02:24 +02:00
Vincent Koc
ff5fac1439 ci(release): retry Docker BuildKit bootstrap 2026-06-09 01:02:24 +02:00
Vincent Koc
f29248fa62 ci(release): retry transient registry build failures 2026-06-09 01:02:23 +02:00
Vincent Koc
04b8c4f313 test(release): isolate trajectory export migration checks 2026-06-09 01:02:23 +02:00
Vincent Koc
f1a1fce982 test(release): isolate sandbox explain migration checks 2026-06-09 01:02:23 +02:00
Vincent Koc
9faa741536 test(release): isolate default session store migration tests 2026-06-09 01:02:23 +02:00
Vincent Koc
dc51c57e29 test(release): isolate sessions tail migration checks 2026-06-09 01:02:23 +02:00
Vincent Koc
95c72dde0f test(release): isolate sessions command migration tests 2026-06-09 01:02:23 +02:00
Vincent Koc
37c1e2725a test(release): stabilize beta three command shards 2026-06-09 01:02:23 +02:00
Vincent Koc
06b226e8b5 test(release): stabilize beta three validation 2026-06-09 01:02:23 +02:00
Vincent Koc
3ed8d5f2c3 fix(tasks): keep maintenance migration scoped 2026-06-09 01:02:23 +02:00
Vincent Koc
2d5bf186c1 test(release): stabilize task maintenance checks 2026-06-09 01:02:23 +02:00
Vincent Koc
3a2176267c test(release): stabilize beta three validation 2026-06-09 01:02:23 +02:00
Vincent Koc
4b55a0e04d test(release): clear beta validation blockers 2026-06-09 01:02:23 +02:00
Vincent Koc
9cdf853409 test(release): stabilize beta validation checks 2026-06-09 01:02:23 +02:00
Vincent Koc
cff8154954 fix(release): satisfy control ui registry lint 2026-06-09 01:02:22 +02:00
Vincent Koc
55de547b52 test(release): keep workshop state mocks current 2026-06-09 01:02:22 +02:00
Vincent Koc
505b23a137 fix(release): clear beta validation blockers 2026-06-09 01:02:22 +02:00
Vincent Koc
5496044f6d fix(release): cap docker e2e cpus 2026-06-09 01:02:22 +02:00
Vincent Koc
20604f7a8f test(memory-core): seed dreaming session store 2026-06-09 01:02:22 +02:00
Vincent Koc
a0f76b2b25 test(telegram): seed approval session store 2026-06-09 01:02:22 +02:00
Vincent Koc
fb97b3b4b3 test(discord): seed think autocomplete session store 2026-06-09 01:02:22 +02:00
Dallin Romney
112e98faa2 chore: bump codex app-server to 0.137.0 (#91496) 2026-06-08 15:42:41 -07:00
dependabot[bot]
646bc0d274 build(deps): bump the android-deps group in /apps/android with 3 updates (#91365)
* build(deps): bump the android-deps group in /apps/android with 3 updates

Bumps the android-deps group in /apps/android with 3 updates: androidx.core:core-ktx, [org.jetbrains.kotlin.plugin.compose](https://github.com/JetBrains/kotlin) and [org.jetbrains.kotlin.plugin.serialization](https://github.com/JetBrains/kotlin).


Updates `androidx.core:core-ktx` from 1.18.0 to 1.19.0

Updates `org.jetbrains.kotlin.plugin.compose` from 2.3.21 to 2.4.0
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.21...v2.4.0)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.3.21 to 2.4.0
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.21...v2.4.0)

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.3.21 to 2.4.0
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.21...v2.4.0)

---
updated-dependencies:
- dependency-name: androidx.core:core-ktx
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlin.plugin.compose
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlin.plugin.serialization
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(android): support compile SDK 37

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-08 15:34:28 -07:00
dependabot[bot]
c967172f69 build(deps): bump the actions group with 2 updates (#91367)
Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [docker/login-action](https://github.com/docker/login-action).


Updates `github/codeql-action` from 4.36.1 to 4.36.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4.36.1...v4.36.2)

Updates `docker/login-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](4907a6ddec...650006c6eb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 15:03:30 -07:00
dependabot[bot]
6aa89bb5f8 build(deps): bump actions/cache from 4 to 5 (#91369)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 15:00:44 -07:00
Kevin Lin
a54f50a41b chore: add taxonomy file (#91512)
* chore: add taxonomy file

* add maturity scores

* move taxonomy doc
2026-06-08 14:55:44 -07:00
dependabot[bot]
f9f7475dbf build(deps): bump actions/github-script from 8 to 9 (#91368)
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 14:54:48 -07:00
dependabot[bot]
b875c812f7 build(deps): bump github.com/steipete/peekaboo (#91364)
Bumps the swift-deps group in /apps/macos with 1 update: [github.com/steipete/peekaboo](https://github.com/steipete/Peekaboo).


Updates `github.com/steipete/peekaboo` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/steipete/Peekaboo/releases)
- [Commits](https://github.com/steipete/Peekaboo/compare/v3.3.0...v3.4.0)

---
updated-dependencies:
- dependency-name: github.com/steipete/peekaboo
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swift-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 14:54:44 -07:00
Peter Steinberger
6c4fb997e5 fix: refresh npm shrinkwrap 2026-06-08 22:34:23 +01:00
Peter Steinberger
b14923d1f3 fix: remove extension-owned root dependency 2026-06-08 22:25:58 +01:00
Kevin Lin
4c5d8afa38 Revert "docs: add maturity scorecard mirror (#91317)" (#91508)
This reverts commit 6cc6f5e210.
2026-06-08 14:18:42 -07:00
Peter Steinberger
9aa6bfccce chore: update dependencies 2026-06-08 21:44:57 +01:00
Vincent Koc
b0998f7d15 fix(browser): accept statement evaluate bodies 2026-06-09 05:07:44 +09:00
Vincent Koc
46f4db6bbd test(config): print missing label stubs 2026-06-09 04:18:07 +09:00
Vincent Koc
9220761fba fix(slack): surface arg menu fallback warning 2026-06-09 04:07:09 +09:00
Julian Missig
a7847ac484 fix(imessage): honor block streaming config (#91449)
Merged via squash.

Prepared head SHA: 6e4e04fb2d
Co-authored-by: jmissig <1448107+jmissig@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-06-08 12:07:03 -07:00
Peter Steinberger
d4c6662341 docs: bump claude proxy Node.js requirement 2026-06-08 20:01:19 +01:00
Vincent Koc
224086e28b fix(file-transfer): clarify missing paired node errors
When no nodes are paired, file-transfer tools now fail before node alias resolution with an actionable message instead of generic unknown-node retries. The paired-node schema wording also steers agents away from local/host/gateway/auto guesses.\n\nFixes #91482.
2026-06-09 03:53:38 +09:00
Vincent Koc
5f6d4277b1 docs: clarify skill workshop tool policy 2026-06-09 03:39:54 +09:00
Vincent Koc
105d77d486 fix(ui): open docs markdown links on docs host
Rewrite recognized docs-root markdown links in Control UI renderers to https://docs.openclaw.ai while preserving Control UI routes, base-mounted resources, and plugin viewer URLs.

Fixes #89465.
2026-06-09 03:11:50 +09:00
ai-hpc
4eb4b87c8e fix(cron): recover no-deliver tool warnings 2026-06-08 19:08:38 +01:00
Vincent Koc
0176429ad7 fix(context): report compactable transcript counts
Adds /context detail diagnostics for active transcript compactability so prompt/cache usage is not mistaken for compactable conversation history.

Fixes #91150. Supersedes #91158.

Co-authored-by: Rain <94058511+Pluviobyte@users.noreply.github.com>
2026-06-09 02:16:11 +09:00
Vincent Koc
f4e746bdfc fix(memory-wiki): render native links relative to generated pages 2026-06-09 02:04:26 +09:00
Shakker
4094ef4dcb test: isolate ACP Matrix plugin routing 2026-06-08 17:53:18 +01:00
Shakker
009ae442a4 test: refresh cron prompt snapshots 2026-06-08 17:53:18 +01:00
Ayaan Zaidi
e7f1b24d9d fix(delivery): treat internal artifacts as silent skips 2026-06-08 22:16:33 +05:30
joelnishanth
f658abae50 fix(delivery): suppress Codex/Harmony internal protocol artifacts from user-facing channels (#88128) 2026-06-08 22:16:33 +05:30
Vincent Koc
81234fbf12 feat(skills): expose content versions in skill prompts 2026-06-09 01:45:42 +09:00
Ayaan Zaidi
47fc1c288b test(reply-queue): cover overflow mutation during drain 2026-06-08 22:03:11 +05:30
yetval
51dbc2c60f fix(reply-queue): remove the drained item by reference instead of front index
drainNextQueueItem captured items[0], awaited the run, then shift()-ed
index 0 assuming it still held the item it ran. Concurrent inbound
messages mutate the same shared items array, and at or over cap
applyQueueDropPolicy splices items off the front, so a burst arriving
while item[0] is in flight can shift a different, still-undelivered
survivor into index 0. shift() then deletes that survivor: it is never
run and is not counted in the overflow summary, so the agent silently
ignores a message it should have answered.

Remove the item that actually ran by identity via a new
removeQueuedItemsByRef helper, and apply the same reference-based
removal to the collect path in drain.ts, which had the same positional
splice(0, groupItems.length) assumption after an awaited group run.
2026-06-08 22:03:11 +05:30
openperf
2ffbea20d2 fix(agents): drop stale exec approval followups after session rebind
Exec approval followups were dispatched by sessionKey only. When /new or
/reset rotates the sessionId under that key while an approval is pending,
the resolved followup landed in the new session, surfacing stale approval
output (or 'Exec denied' / continuation text) in a fresh conversation.

Capture the session UUID active when the approval is requested and drop the
followup once the key has been rebound to a different sessionId:
- agent-run followups: carry the expected id on the agent request and drop it
  at the gateway as an early preflight, before the handler touches the rebound
  session (session-store write, chat/agent run + active-run registration,
  dedupe, accepted ack) — not just before model dispatch. Covers elevated and
  non-elevated.
- denied / direct fallback followups: resolve the key's current sessionId from
  the session store and drop before the channel send.

Fixes #59349.
2026-06-08 17:29:15 +01:00
ly-wang19
303873e835 refactor(cron): replace store-load double casts with raw-boundary record types
ensureLoaded cast persisted rows through `as unknown as CronJob[]` and then
back to `Record<string, unknown>` per item, which mislabeled unvalidated data
as CronJob. Treat the rows as raw records at the store boundary and apply a
single trusted CronJob cast only after getInvalidPersistedCronJobReason passes,
preserving the normalize/validate/quarantine flow. Drops two redundant casts
and two lines with no behavior change.

Fixes #91314

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit e9e494c7fe)
2026-06-09 01:15:18 +09:00
Sasan Sotoodehfar
22bda60cbe fix(memory): rebind qmd collections when a collection root changes
ensureCollections() never rebound a managed collection after its root
path changed (e.g. an agent workspace repoint): listCollectionsBestEffort()
read `qmd collection list`, whose output carries no filesystem path, so
listed.path was always undefined and shouldRebindCollection took its
defensive branch and skipped the rebind. The collection stayed pinned to
the stale root and recall kept resolving the old location.

Enrich the listed collections with their path via `qmd collection show`
so path-change detection works and the rebind fires.

Closes #91251

(cherry picked from commit 4a225011e9)
2026-06-09 01:10:19 +09:00
Andy Ye
57633c42b6 Fix CLI silent reply fallback policy
(cherry picked from commit 2f3762d229)
2026-06-09 00:49:51 +09:00
宇宙熊Yzx
cfeaf6897f fix(cron): clear payload model overrides
(cherry picked from commit 87af108140)
2026-06-09 00:46:36 +09:00
Ayaan Zaidi
7a602c7385 fix(reply): forward queued commentary progress (#89834) (thanks @anagnorisis2peripeteia) 2026-06-08 21:13:22 +05:30
Ayaan Zaidi
66c9feb41d fix(cli): type claude live commentary flag (#89834) (thanks @anagnorisis2peripeteia) 2026-06-08 21:13:22 +05:30
Ayaan Zaidi
817a0910f3 fix(cli): suppress claude commentary answer partials 2026-06-08 21:13:22 +05:30
Ayaan Zaidi
26983877d7 fix(channels): keep commentary progress bounded 2026-06-08 21:13:22 +05:30
Cameron Beeley
5fef91f1de fix: apply backend output transforms to commentary progress text 2026-06-08 21:13:22 +05:30
Cameron Beeley
3a04c9a4bb fix: emit only new commentary segment, add text-tool-text-tool regression test 2026-06-08 21:13:22 +05:30
Cameron Beeley
d03952ccd4 feat: add commentary text emission to Claude CLI streaming parser
Detect text accumulated before tool_use blocks in the Claude CLI
streaming parser and emit it as commentary via a new onCommentaryText
callback. This enables the same commentary progress display that the
Codex backend already provides through preamble item events.

- Add onCommentaryText optional callback to createCliJsonlStreamingParser
- Flush accumulated assistantText as commentary when content_block_start
  with tool_use type is encountered
- Track last flushed position to avoid duplicate emissions on consecutive
  tool_use blocks without intervening text
- Wire the callback in both execute.ts (regular CLI spawn + live session)
  and claude-live-session.ts, emitting AgentItemEventData with
  kind=preamble and progressText
- Add 3 test cases covering: text before tool_use, empty text before
  tool_use, and consecutive tool_use dedup
2026-06-08 21:13:22 +05:30
Cameron Beeley
c1300455d9 fix(channels): render inter-tool commentary in full
Commentary lines carry noCompact so the progress-draft renderer does not compact
them like tool lines — assistant prose renders in full, spilling to a new message
at the channel limit rather than truncating mid-sentence.
2026-06-08 21:13:22 +05:30
Pavan Kumar Gondhi
53357e8e7f fix: neutralize browser media directives (#91422) 2026-06-08 21:11:14 +05:30
宇宙熊Yzx
0911f86916 fix(cron): keep session model overrides strict
(cherry picked from commit 4562a3850b)
2026-06-09 00:21:47 +09:00
宇宙熊Yzx
14430ca588 fix(cron): inherit default fallbacks for string agent jobs
(cherry picked from commit 4b5cb68d39)
2026-06-09 00:21:47 +09:00
Vincent Koc
67dc805314 fix(agents): retry post-tool empty replies
Post-tool empty OpenAI-compatible stop turns no longer qualify as intentional silence, so replay-safe attempts use the existing empty-response retry and unsafe attempts surface the existing incomplete-turn error instead of disappearing.

Fixes #91394
2026-06-09 00:18:04 +09:00
Kevin Lin
6cc6f5e210 docs: add maturity scorecard mirror (#91317)
* docs: add maturity scorecard mirror

* docs: format maturity scorecard mirror

* docs: drop stray maturity note

* docs: fix maturity scorecard docs checks
2026-06-08 08:07:32 -07:00
Ben Badejo
60d716e652 fix: normalize Codex dynamic tool progress results
Normalize Codex dynamic tool progress result payloads to TUI-compatible content arrays after sanitization, while stripping protocol-only fields from the emitted event.

Includes regression coverage for sanitized dynamic tool text/image progress output.

Thanks @bdjben.
2026-06-08 16:05:16 +01:00
Vincent Koc
d46dc39b18 fix(memory): rebuild missing index metadata safely
Gateway/background sync now repairs missing memory index metadata with the existing full reindex path when the configured embedding provider is available, while preserving dirty/paused state instead of downgrading semantic chunks when embeddings are unavailable.

Fixes #90338
2026-06-08 23:58:10 +09:00
rudi193-cmd
e3ef136bca fix(memory): keep FTS keyword search model agnostic
Make lexical FTS/LIKE search ignore embedding model identity so exact keyword recall survives provider/model changes. Vector search remains model-scoped, and refreshed or stale FTS rows are cleaned by path/source with live-chunk filtering to prevent old orphan rows from surfacing.

Fixes #48300
2026-06-08 23:38:46 +09:00
Mason Huang
7499a020d9 docs: preserve LINE across localized docs glossaries (#91442)
Summary:
- The PR adds `LINE -> LINE` entries to 16 localized docs glossary JSON files so generated localized docs preserve the LINE brand term.
- PR surface: Docs +64. Total +64 across 16 files.
- Reproducibility: not applicable. this is a docs glossary maintenance PR, not a runtime bug report. The relevant checks are source inspection, PR-head JSON validation, and docs-i18n policy alignment.

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

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

Prepared head SHA: 2ef712ff7a
Review: https://github.com/openclaw/openclaw/pull/91442#issuecomment-4649882666

Co-authored-by: Mason Huang <masonxhuang@tencent.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-06-08 14:33:41 +00:00
张贵萍0668001030
3e0f7e4931 fix(memory-core): preserve sqlite-vec table on unsafe reindex 2026-06-08 23:29:42 +09:00
Vincent Koc
355a9cbf35 fix(memory): fall back to sqlite-vec platform variant 2026-06-08 23:25:24 +09:00
Ayaan Zaidi
6d7eb9bb84 fix(android): use connected device foreground service 2026-06-08 19:53:25 +05:30
Dave Lutz
7d357a75fd fix(android): avoid data sync fgs for node service 2026-06-08 19:53:25 +05:30
Vincent Koc
9c5ac9f42d fix(memory): verify sqlite-vec loads usable functions 2026-06-08 23:16:54 +09:00
Shakker
da401341b6 fix: preserve fallback approval runtime auth 2026-06-08 14:49:01 +01:00
Shakker
f366922e01 fix: limit approval runtime token to local clients 2026-06-08 14:49:01 +01:00
Shakker
1c28c3914a fix: require stable approval requester identity 2026-06-08 14:49:01 +01:00
fuller-stack-dev
43acf3a4a2 fix(gateway): gate env approval runtime auth 2026-06-08 14:49:01 +01:00
fuller-stack-dev
2affecc720 fix(gateway): share approval runtime socket token 2026-06-08 14:49:01 +01:00
Mason Huang
9a82b60024 docs: preserve channel brand terms in Chinese i18n (#91419)
Summary:
- Adds Simplified and Traditional Chinese docs i18n glossary mappings to preserve channel and product brand terms in generated Chinese translations.
- PR surface: Docs +144. Total +144 across 2 files.
- Reproducibility: not applicable. this is a docs i18n glossary maintenance PR, not a bug report. The relevant check is source inspection plus PR-head JSON and coverage validation.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: preserve channel brand terms in Chinese i18n

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

Prepared head SHA: 45d54b370f
Review: https://github.com/openclaw/openclaw/pull/91419#issuecomment-4649184716

Co-authored-by: Mason Huang <masonxhuang@tencent.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: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-08 13:25:30 +00:00
Shakker
a04de1a0ce test: scope subagent attachment home env 2026-06-08 14:19:41 +01:00
Shakker
ca7047e460 fix: restore cli prepare session env 2026-06-08 14:19:41 +01:00
Shakker
226341e847 test: scope bundle mcp harness env 2026-06-08 14:19:41 +01:00
Shakker
1621e58ff1 fix: restore cli runner session env 2026-06-08 14:19:41 +01:00
Shakker
9403ea805d test: scope models config host env 2026-06-08 14:19:41 +01:00
Shakker
71f6620ba3 fix: scope model auth marker env 2026-06-08 14:19:41 +01:00
Shakker
35eb63e692 test: stabilize agent SIGTERM tests 2026-06-08 14:11:29 +01:00
Ayaan Zaidi
2858c629bd build(plugin-sdk): refresh api baseline for cli commentary bridge 2026-06-08 18:06:18 +05:30
Ayaan Zaidi
e1ac2d0925 refactor(cli): unify agent event bridge cleanup 2026-06-08 18:06:18 +05:30
Cameron Beeley
d7b9b21fb8 fix(cli): bridge inter-tool commentary events to channel progress
Inter-tool commentary (assistant text emitted before a tool call, surfaced by
the CLI parser as a stream:"item", kind:"preamble" agent event) landed on the
agent-event bus with no subscriber and was silently dropped: runCliAgentWithLifecycle
bridges the assistant, reasoning, and tool streams to channel callbacks, but the
item/preamble stream had no bridge. Add createCommentaryEventBridge to forward it
to onItemEvent, so CLI commentary reaches the channel's commentary render hook.

This is the CLI-dispatch delivery half of the commentary feature: the parser
emission (claude-cli) feeds the bus; this bridge delivers it to the channel.
Addresses the claude-cli case of intermediate-text-lost (#87326 / #84486).
2026-06-08 18:06:18 +05:30
Mason Huang
439dcbde3b fix: clarify provider quota errors (#91390)
Summary:
- The branch adds provider error classification for generic HTTP 429 runtime failures and Volcengine `InvalidSubscription` billing errors, plus focused regression tests and SIGTERM test stabilization.
- PR surface: Source +62, Tests +137. Total +199 across 8 files.
- Reproducibility: yes. at source level. Current main lacks the HTTP 429 metadata classifier and Volcengine subscription billing matcher, and the PR body reports a live Volcengine failure shape plus after-fix tests.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: clarify provider quota errors

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

Prepared head SHA: 5e10848a37
Review: https://github.com/openclaw/openclaw/pull/91390#issuecomment-4647819660

Co-authored-by: Mason Huang <masonxhuang@tencent.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: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-08 11:12:56 +00:00
Ayaan Zaidi
310d28f719 test(telegram): trim block rotation coverage 2026-06-08 15:30:35 +05:30
Ayaan Zaidi
5b0061e7a2 refactor(telegram): distill streamed block rotation cleanup 2026-06-08 15:30:35 +05:30
Alexazhu
4f31967141 Preserve stale Telegram block drafts before rotation
Queued answer block rotations can split assistant messages while the previous Telegram preview is stale. Materialize the previous block through the existing delivery fallback before resetting the draft lane, and keep assistant-message correlation on the internal dispatcher path instead of expanding the public Plugin SDK payload API.

Constraint: ClawSweeper flagged missing live Telegram proof and a public Plugin SDK helper surface; the code path must stay a Telegram/channel bugfix without adding a third-party SDK contract.

Rejected: Export getReplyPayloadAssistantMessageIndex from openclaw/plugin-sdk/reply-payload | it exposes internal reply metadata solely for this Telegram fix.

Rejected: Match queued block rotations only by text | plugin rewrites, repeated text, and media-only transformed block deliveries need assistant-message correlation.

Confidence: high

Scope-risk: narrow

Directive: Keep intermediate block materialization non-durable unless it is the actual final answer path, and keep assistant-message correlation off public reply-payload SDK exports.

Tested: OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/lane-delivery.test.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/dispatch.test.ts src/auto-reply/reply/dispatch-from-config.test.ts src/plugins/wired-hooks-reply-payload-sending.test.ts

Tested: node_modules/.bin/oxlint --tsconfig config/tsconfig/oxlint.extensions.json extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.test.ts

Tested: node_modules/.bin/oxlint --tsconfig config/tsconfig/oxlint.core.json src/auto-reply/reply/reply-dispatcher.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/dispatch.ts src/auto-reply/dispatch.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/reply-payload-sending-hook.ts

Tested: git diff --check origin/main

Not-tested: Redacted live Telegram Bot API/Desktop proof; no Telegram credentials or chat target are configured in this local environment.

Not-tested: tsgo extensions test command was attempted locally and terminated after running over six minutes without output; prior known local run failed on unrelated Discord voice libopus-wasm errors.
2026-06-08 15:30:35 +05:30
Alexazhu
3fdc17b921 Preserve Telegram block previews across assistant boundaries
Constraint: Telegram draft previews are mutable until stopped, while OpenClaw block streaming can emit multiple chunks for one assistant message and separate assistant messages around tool calls.
Rejected: Finalizing every answer block | same-message chunks would become duplicate standalone Telegram messages before the final payload.
Rejected: Exposing full reply payload metadata through the public plugin SDK | Telegram only needs assistant block identity, and broader metadata would make internal dispatch fields a third-party API contract.
Rejected: Leaving queued block rotations as FIFO text-only state | delivery hooks can rewrite, cancel, skip, or remove answer text after queueing.
Confidence: high
Scope-risk: moderate
Directive: Keep same-message block chunks in one draft; rotate only at assistant-message/tool-progress boundaries, and expire cancelled, skipped, or non-answer queued blocks without deleting still-pending earlier rotations.
Tested: git diff --check origin/main; /Users/alex/PR/projects/openclaw__openclaw/repo/node_modules/oxfmt/bin/oxfmt --check --threads=1 docs/.generated/plugin-sdk-api-baseline.sha256 extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/draft-stream.test-helpers.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.test.ts src/auto-reply/dispatch.test.ts src/auto-reply/dispatch.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/reply-dispatcher.ts src/auto-reply/reply/reply-payload-sending-hook.ts src/plugin-sdk/reply-payload.ts; ./node_modules/.bin/oxlint extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.test.ts extensions/telegram/src/draft-stream.test-helpers.ts src/plugin-sdk/reply-payload.ts src/auto-reply/dispatch.ts src/auto-reply/dispatch.test.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/reply-dispatcher.ts src/auto-reply/reply/reply-payload-sending-hook.ts; node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --check; CI=1 NODE_OPTIONS=--max-old-space-size=4096 node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo; CI=1 NODE_OPTIONS=--max-old-space-size=4096 node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.non-agents.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-non-agents.tsbuildinfo; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs extensions/telegram/src/bot-message-dispatch.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/lane-delivery.test.ts extensions/telegram/src/draft-stream.test.ts src/plugin-sdk/reply-payload.test.ts src/plugins/contracts/plugin-sdk-index.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs src/auto-reply/reply/before-deliver.test.ts src/auto-reply/dispatch.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs src/auto-reply/reply/dispatch-from-config.test.ts -t "forwards payload metadata into onBlockReplyQueued context"
Not-tested: Live Telegram bot roundtrip with real credentials. codex review --base origin/main was run twice but did not complete a final verdict after hitting local heavy-check lock, unsupported --runInBand, and .vite-temp EPERM in the review tool sandbox; no actionable P1/P2 finding was emitted before termination.
2026-06-08 15:30:35 +05:30
Ayaan Zaidi
b75d1a0b85 fix(telegram): keep forum topic sessions stable 2026-06-08 14:50:48 +05:30
Cody's OpenClaw
e2db55373d fix(telegram): satisfy topic reply lint
Co-authored-by: Codex <codex@openai.com>
2026-06-08 14:50:48 +05:30
Cody's OpenClaw
733152127b fix(telegram): route account-scoped topic agents
Co-authored-by: Codex <codex@openai.com>
2026-06-08 14:50:48 +05:30
Omar Shahine
fc6400ede3 fix(imessage): always-on inbound recovery and dedupe (#91335)
* feat(imessage): always-on inbound recovery, deprecate catchup

Replaces the opt-in catchup subsystem with always-on inbound replay
protection that brings iMessage in line with the other channels, and
fixes #89237 (stale backlog dispatched as fresh after bridge recovery).

- New inbound-dedupe.ts: persistent claimable GUID dedupe (claim/commit/
  release) plus a stale-backlog age fence that suppresses live rows whose
  send date is materially older than arrival (logged, never silent).
- monitor-provider: claim at ingestion, carry the exact claimed key on the
  debouncer entry, commit on successful flush / release on dispatch failure
  (per-unit so a coalesced bucket cannot strand a sibling claim). Keeps the
  local startup since_rowid watermark so startup-window rows are not skipped.
- Deprecate catchup: delete catchup.ts + catchup-bridge.ts, remove the
  channels.imessage.catchup schema, cursor migration, and config-guard nag.
  Back-compat: strip the retired key before validation; new imessage doctor
  contract reports + removes it on doctor --fix.
- Docs updated for the new recovery model.

Net -947 prod LOC.

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

* feat(imessage): recover downtime messages via since_rowid replay

Builds downtime recovery on the new inbound dedupe instead of restoring the
old catchup subsystem. On startup the monitor passes the last dispatched rowid
(a persisted per-account cursor) to imsg watch.subscribe as since_rowid, so imsg
replays the messages that landed while the gateway was down, then tails live.
The GUID dedupe drops anything already handled, so no cursor/retry bookkeeping
is needed.

- recovery-cursor.ts: minimal persisted per-account lastDispatchedRowid.
- monitor-provider: since_rowid = cursor (capped to the most recent
  IMESSAGE_RECOVERY_MAX_ROWS); split the age fence on the startup rowid boundary
  so replayed rows (<= boundary) use the wider recovery window and live rows
  (> boundary) keep the tight #89237 fence; advance the cursor on commit.
- Local only: remote SSH cliPath cannot read chat.db, so it tails from the
  current rowid (suppress-and-move-on) as before.

Restores missed-message recovery that the catchup removal dropped, with no
config and a fraction of the old LOC.

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

* fix(imessage): make recovery cursor advance failure- and suppression-safe

Addresses two cursor-state regressions in the downtime-recovery path:

- Failed replay rows could be skipped forever: a released (failed) row keeps
  its dedupe claim for retry, but a later successful row in the same flush
  advanced the cursor past it, so the next startup's since_rowid skipped it.
  Hold a per-session floor at the lowest released rowid and never advance the
  cursor past it.
- Suppressed live backlog could be re-delivered after a restart: a live row
  suppressed under the tight live fence was not recorded, so after a restart it
  fell under the wider recovery window (its rowid now below the new boundary)
  and was delivered. Commit its dedupe key on suppression so the recovery
  replay treats it as already handled.

Both caught by Codex autoreview. Adds regression tests for the floor and the
suppression record.

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

* fix(imessage): bound the GUID-less replay key length

Hash the composite fallback key's variable parts (conversation, sender,
created_at, text) so the key is length-bounded regardless of message text.
The persistent dedupe store already hashes keys internally, so this was not a
live overflow, but the bounded key removes the dependency on that and keeps the
fallback fail-open. Flagged by autoreview.

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

* fix(imessage): recover downtime messages on remote cliPath setups too

The since_rowid replay runs over the imsg RPC client, so driving it from the
persisted recovery cursor (not the local chat.db boundary) makes downtime
recovery work for remote SSH cliPath gateways — the topology the old RPC-based
catchup served and that the rowid-boundary-only version regressed. Local setups
keep the wider, capped recovery window via the chat.db boundary; remote uses the
live age-fence window. Flagged by autoreview.

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

* fix(imessage): seed recovery cursor from retired catchup cursor on upgrade

A one-time, self-cleaning migration: when the recovery cursor is empty on the
first startup after upgrade, seed it from the retired imessage.catchup-cursors
lastSeenRowid and consume the legacy entry. Without this a user who had catchup
enabled would not replay messages missed across the upgrade restart. Flagged by
autoreview.

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

* fix(imessage): preserve catchup recovery on upgrade

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-08 16:54:10 +09:00
Peter Steinberger
538d36eaaa refactor: move session metadata to SQLite (#91322)
* refactor: move session metadata to sqlite

* test: seed session stores with sqlite fixtures

* test: seed remaining session stores with sqlite fixtures

* fix: stabilize sqlite session cache freshness

* test: seed cli transcript metadata in sqlite
2026-06-07 23:17:35 -07:00
mushuiyu_xydt
b2c1de77ac fix #90452: Regression: Heartbeat exec completion still shows generic fallback text instead of actual output (#90897)
Summary:
- The PR threads heartbeat trigger state into embedded-runner payload formatting so heartbeat exec-like failures include captured error details, with a focused regression test.
- PR surface: Source +12, Tests +18. Total +30 across 3 files.
- Reproducibility: yes. Source inspection shows current main and v2026.6.1 only include raw exec details for v ... follows the generic warning path; I did not run the wall-clock heartbeat scenario in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix #90452: Regression: Heartbeat exec completion still shows generic…

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

Prepared head SHA: 85c5d6fb9f
Review: https://github.com/openclaw/openclaw/pull/90897#issuecomment-4638158130

Co-authored-by: 杨浩宇0668001029 <yang.haoyu@xydigit.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-06-08 05:47:53 +00:00
Yzx
ccb9f2ca2b fix(anthropic): drop reasoning_content replay signatures (#91231)
Summary:
- The PR filters persisted OpenAI-compatible `reasoning_content` thinking placeholders from direct Anthropic replay payloads and updates the focused Anthropic provider test.
- PR surface: Source +1, Tests -4. Total -3 across 2 files.
- Reproducibility: yes. from source: current main serializes `thinkingSignature: "reasoning_content"` as a nat ... rror. The PR body also provides after-fix captured outbound payload proof for the production provider path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(anthropic): drop reasoning_content replay signatures

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

Prepared head SHA: 6eaa72f3a3
Review: https://github.com/openclaw/openclaw/pull/91231#issuecomment-4643786130

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-08 05:45:48 +00:00
snowzlm
a4f0e508df fix(gateway): preserve stale channel restart diagnostics (#90937)
Summary:
- This PR sanitizes status patches from aborted channel tasks in the gateway manager and adds regression tests for stale restart diagnostics.
- PR surface: Source +56, Tests +78. Total +134 across 2 files.
- Reproducibility: yes. Source inspection and the PR's before-fix regression show the sequence: non-manual sto ... while the stale task remains, then a late `connected=true` / `lastError=null` status patch on current main.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(gateway): preserve stale restart diagnostics
- PR branch already contained follow-up commit before automerge: fix(gateway): preserve stale channel restart diagnostics

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

Prepared head SHA: 53b37e5073
Review: https://github.com/openclaw/openclaw/pull/90937#issuecomment-4638942823

Co-authored-by: snowzlm <snowzlm@noreply.codeberg.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-06-08 05:29:11 +00:00
Mariano
b8adc11977 feat(cron): support command jobs
Add command-backed cron jobs with timeout-safe process-tree cleanup for shell wrappers. Ensures POSIX command jobs run in a killable process group, adds Windows tree cleanup fallback handling, and covers timeout cleanup behind sh -lc.
2026-06-08 12:06:16 +09:00
Marcus Castro
181238fb53 feat(whatsapp): expand live QA coverage (#90480)
* feat(whatsapp): expand qa driver message support

* feat(qa-lab): add deterministic whatsapp mock replies

* feat(qa-lab): expand whatsapp live qa scenarios

* docs(qa): document whatsapp live qa coverage
2026-06-08 00:03:23 -03:00
Yzx
4780546c12 fix(cron): preserve isolated agent turn payload message (#91230)
Summary:
- The PR changes isolated cron agent prompt construction to read agentTurn text from `job.payload.message` and adds regression coverage for malformed dispatch messages plus SQLite-rehydrated manual runs.
- PR surface: Source +8, Tests +60. Total +68 across 3 files.
- Reproducibility: yes. source-level: current main interpolates `input.message` into the isolated cron prompt, ... release report supplies operator repro evidence; I did not run it locally because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(cron): preserve isolated agent turn payload message

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

Prepared head SHA: 4d33607efd
Review: https://github.com/openclaw/openclaw/pull/91230#issuecomment-4643779241

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-08 02:23:02 +00:00
Yzx
766c5b3d32 fix(codex): preserve native subagent completion results (#91235)
Summary:
- The branch updates the Codex plugin native subagent parser, monitor, and tests so successful null or blank c ... final result and transcript reconciliation can override early empty notifications before fallback delivery.
- PR surface: Source +92, Tests +176. Total +268 across 4 files.
- Reproducibility: yes. at source level: current main maps successful null/blank Codex completions to `(no out ... n recover final text. I did not run a live current-main Telegram/Codex reproduction in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): preserve native subagent completion results

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

Prepared head SHA: f9270c28e7
Review: https://github.com/openclaw/openclaw/pull/91235#issuecomment-4643854708

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-08 02:22:05 +00:00
Omar Shahine
9caff5f873 fix(imessage): gate split-send coalescing on imsg balloon metadata with back-compat (#90858)
Gate iMessage same-sender DM split-send coalescing on imsg's structural
`balloon_bundle_id` URL-balloon marker (openclaw/imsg#137) instead of timing/
text-shape inference, with a session capability latch and a back-compat path:

- URL-balloon marker present -> merge (precise split-send).
- Build known to emit balloon metadata (session latch) -> keep non-marker
  buckets separate (the precision win).
- Build that never emits balloon metadata (older imsg) -> preserve the legacy
  unconditional merge, so split-send users do not regress to two turns.

Never merges more than shipped main already did. Verified live end-to-end: the
patched gateway, watching a real chat.db via an imsg #137 build, merged a real
iPhone-sent `Dump <url>` split-send into one turn. Client-side removal once imsg
coalesces upstream is tracked in #91243 (openclaw/imsg#141).

Closes #90795
2026-06-07 19:14:13 -07:00
Chunyue Wang
f2530de832 fix(agents): do not refresh lastUsedAt on MCP lease release (#91124)
Summary:
- The PR removes release-time `lastUsedAt` refresh from session MCP runtime lease cleanup and adds regression tests for idle eviction after a lease expires while active.
- PR surface: Source 0, Tests +74. Total +74 across 2 files.
- Reproducibility: yes. from source inspection: current main refreshes `lastUsedAt` in the release callback, a ...  timestamp after active leases drop to zero. I did not execute the focused Vitest in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): do not refresh lastUsedAt on MCP lease release

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

Prepared head SHA: c9144789fd
Review: https://github.com/openclaw/openclaw/pull/91124#issuecomment-4641967555

Co-authored-by: openperf <16864032@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-06-08 02:08:28 +00:00
Yzx
75c1790b50 fix(outbound): preserve retries for budget-deferred deliveries (#91241)
Summary:
- The branch removes the budget-deferred `failDelivery` path from outbound recovery and updates the `maxRecoveryMs` regression expectation so unattempted deliveries keep retry counts at zero.
- PR surface: Source -11, Tests -1. Total -12 across 2 files.
- Reproducibility: yes. at source level: current main reaches `failDelivery` from the exhausted recovery-budge ...  in this read-only review, but the PR body also supplies terminal output showing the after-fix queue state.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(outbound): preserve retries for budget-deferred deliveries

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

Prepared head SHA: aff2b9d16e
Review: https://github.com/openclaw/openclaw/pull/91241#issuecomment-4644024479

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-08 01:43:02 +00:00
Vincent Koc
b16a43597d fix(agents): guard prompt cache tool names
Make prompt-cache observability collect diagnostic tool names through guarded descriptor reads so an unreadable tool-name getter cannot abort cache tracing/debug collection. Preserve readable trimmed names and keep runtime tool registration/schema behavior strict and unchanged.
2026-06-08 10:36:50 +09:00
Vincent Koc
8b03fd1f5f fix(agents): compact lean local tool catalogs
Default localModelLean runs to compact Tool Search controls when the operator has not configured tools.toolSearch, while preserving explicit Tool Search settings and direct message-tool delivery semantics.

Verification: local focused Vitest/docs/format/lint/diff/autoreview proof; GitHub CI, CodeQL/Security High, CodeQL Critical Quality, OpenGrep PR Diff, Real behavior proof, Dependency Guard, and Workflow Sanity passed on 6153fb5ecb.

Refs https://github.com/openclaw/openclaw/issues/86599
2026-06-08 10:33:41 +09:00
Vincent Koc
3ffb3609a1 fix(codex): quarantine unreadable dynamic tools (#90022) 2026-06-08 10:30:13 +09:00
joshavant
5c5391836b fix(android): remove inert appearance palette preview 2026-06-07 17:43:21 -05:00
Pavan Kumar Gondhi
2a21de6322 fix: gate owner-only HTTP tools (#90261)
* fix: gate owner-only HTTP tools

* fix: inherit HTTP owner tool denies

* fix: use mutable HTTP owner deny policy

* fix: preserve RPC owner tool access

* docs: clarify owner-only gateway tool allowlist

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-06-07 17:26:12 -05:00
Voscko
3c73ff7689 feat(android): add theme mode selection (#90752)
* feat(android): add theme mode selection

* refine Android theme mode handling

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-06-07 17:24:57 -05:00
Jason (Json)
57e0bdaabe feat: add live provider model catalog helper
Summary:
- Add a shared live provider catalog runtime for SDK-backed providers.
- Route OpenAI, xAI, OpenCode Go, Chutes, DeepInfra, Venice, NVIDIA, and Vercel AI Gateway live model discovery through the shared helper.
- Remove duplicated provider-local live catalog caching and harden auth marker stripping, empty live-result retries, and OpenAI custom-base-url handling.

Verification:
- node scripts/run-vitest.mjs extensions/openai/openai-provider.test.ts src/plugin-sdk/provider-catalog-live-runtime.test.ts src/commands/models/list.source-plan.test.ts extensions/opencode-go/index.test.ts extensions/nvidia/provider-catalog.test.ts
- pnpm plugin-sdk:api:check
- pnpm lint --threads=8
- pnpm run lint:extensions:bundled
- pnpm run test:extensions:package-boundary:compile
- pnpm check:import-cycles
- pnpm exec oxfmt --check extensions/openai/openai-provider.ts extensions/openai/openai-provider.test.ts
- git diff --check origin/main...HEAD
- autoreview clean: no accepted/actionable findings reported
- AWS Crabbox focused remote proof: run_364680d1bff8 / cbx_2456fffafe01
- Earlier same-PR AWS Crabbox live proof: run_1f05ccab368e / cbx_7375c79fcf9b

Known proof gap:
- Final current-code true live-provider smoke was blocked by Crabbox secret hydration, documented in the PR proof comment.
2026-06-07 14:16:00 -07:00
Omar Shahine
6c35c0d965 fix(imessage): self-explaining private-API failures and dedicated send timeout (#91041)
Append imsg's own status message (SIP / library validation / macOS 26 AMFI gate)
to iMessage private-API blocked-action errors so operators see the real blocker
instead of a generic "run imsg launch". Add a dedicated 150s default timeout for
iMessage send RPCs (explicit opts and probeTimeoutMs still win) so macOS 26
bridge stalls are not aborted mid-send.

Staged mitigation: the longer wait fully activates once the companion bridge
timeout (openclaw/imsg#139) ships; on current imsg the bridge still returns at
its own 10s, so there is no regression. Diagnostics half is live-proven; the
delayed-send timeout is covered by source + unit proof + maintainer waiver.
2026-06-07 14:07:31 -07:00
Peter Steinberger
af79cd6a9d fix: preserve live Ollama catalog metadata 2026-06-07 14:00:09 -07:00
brokemac79
3b6bcbfb50 fix: make sandbox skills readable in writable sandboxes
Materializes prompt-visible skills into a protected sandbox-readable workspace for rw sandboxes, refreshes Docker/SSH/OpenShell views, and hardens stale or poisoned remote skill copies. Fixes #90410.
2026-06-07 13:47:56 -07:00
clawsweeper[bot]
e498d39bed fix(agents): prevent ReDoS in background-session name derivation (#91233)
Summary:
- The PR updates background-session command tokenization to avoid catastrophic regex backtracking and adds `deriveSessionName` regression tests for quoted and backslash-heavy commands.
- PR surface: Source 0, Tests +26. Total +26 across 2 files.
- Reproducibility: yes. with high confidence from source inspection and supplied terminal proof: current `main ...  shows before/after timing for the production helper. I did not run tests because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): treat backslash as literal inside single-quoted session …
- PR branch already contained follow-up commit before automerge: fix(agents): prevent ReDoS in background-session name derivation

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

Prepared head SHA: 0a38952fc8
Review: https://github.com/openclaw/openclaw/pull/91233#issuecomment-4643821335

Co-authored-by: yetval <yetvald@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-06-07 20:30:56 +00:00
Shakker
0c33f4e078 fix: stabilize docker stats heartbeat test 2026-06-07 19:06:24 +01:00
Nimrod Gutman
47dbc675e9 feat(ios): clarify talk realtime fallback (#91201)
Merged via squash.

Prepared head SHA: b6fd32ed6e

Local prep note: pnpm build passed. pnpm check hit the npm shrinkwrap guard because @anthropic-ai/sdk@0.100.1 is no longer resolvable before 2026-05-24T20:18:43Z; the same shrinkwrap guard failure reproduces on current origin/main at 66b91d78fe, and this PR does not touch dependency manifests or lockfiles.

Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-06-07 20:21:34 +03:00
Vincent Koc
66b91d78fe fix(e2e): bound release user journey JSON artifacts 2026-06-07 12:45:43 +02:00
zenglingbiao
3753c5e2c8 fix(inbound-meta): preserve reply-context body tails
Preserve actionable tail content in long reply-context bodies before they enter prompt JSON or inline reply context formatting.

- Apply UTF-16-safe head+tail truncation to ReplyChain JSON bodies and fallback ReplyToBody JSON blocks.
- Use the same body-aware truncation for Telegram inline ReplyToBody fallback and chat_window message bodies, so those paths cannot suppress the JSON fallback and still lose the tail.
- Adds regression coverage for ReplyChain, fallback ReplyToBody, Telegram inline ReplyToBody, chat_window reply targets, and emoji-heavy heads.

Verification:
- node scripts/run-vitest.mjs src/auto-reply/reply/inbound-meta.test.ts
- node_modules/.bin/oxfmt --check --threads=1 src/auto-reply/reply/inbound-meta.ts src/auto-reply/reply/inbound-meta.test.ts
- node scripts/run-oxlint.mjs src/auto-reply/reply/inbound-meta.ts src/auto-reply/reply/inbound-meta.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Testbox-through-Crabbox check:changed: provider=blacksmith-testbox id=tbx_01ktgthbb5xa9d5ap58h4134s0 exit=0
- PR CI: 158/158 completed, no failures, MERGEABLE/CLEAN

Fixes #91042
2026-06-07 19:45:00 +09:00
Vincent Koc
9bafa2a2b6 fix(e2e): bound release scenario JSON artifacts 2026-06-07 12:43:33 +02:00
Vincent Koc
1703fbc2ad fix(e2e): bound browser snapshot diagnostics 2026-06-07 12:39:48 +02:00
Chunyue Wang
afcbdd7416 fix(infra/agents): session-routing guard for coalesced gateway restart continuations (#86742) (#87323)
* fix(infra/agents): session-routing guard for coalesced gateway restart continuations (#86742)

When two sessions issue gateway.restart with continuationMessage close
together, the scheduler Path B updatePendingRestartEmitHooks
unconditionally overwrote the existing pending hooks, silently dropping
the first sessions continuation and potentially routing the second
sessions continuation back to the first session (CWE-200 finding
flagged by aisle-research-bot on prior attempt #74443).

Add a session-routing guard: scheduleGatewaySigusr1Restart now accepts
an optional sessionKey and tracks the pending restarts owning session.
Coalesced callers from a different session are rejected at the hook-
update step and the new ScheduledRestart.emitHooksQueued: false field
surfaces the drop to the caller. The gateway tool propagates this as
continuationQueued: false in the tool response, matching #83370 narrow
report-only surface.

Same-session debounce/replace and legacy hookless callers behave the
same as before.

Refs #86742

* fix(infra): preserve queued restart continuation on forced bypass

* fix(infra): make forced restart hook preservation explicit

* fix(infra): guard restart continuation ownership before reschedule

* fix(infra): report hookless coalesced restarts accurately

* fix(infra): trust runtime session for restart sentinel routing

* fix(infra): preserve earlier restart reschedule semantics

* fix(agents): trust runtime session for update continuations

* fix(infra): preserve hookless forced restart continuations

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-07 03:38:58 -07:00
Vincent Koc
2b43315933 fix(tooling): bound extension boundary source reads 2026-06-07 12:36:13 +02:00
Vincent Koc
f5935bbca1 fix(e2e): cancel timed out response reads 2026-06-07 12:32:56 +02:00
liuweiqin
a1af47e5da fix(codex): surface lastToolError on degraded orphan-tool delivery
Completed turns with deliverable assistant text still synthesize failed
tool.result rows but no longer set promptError. Record lastToolError on
that degraded path and treat whitespace-only assistant items as non-
deliverable so orphan tools still fail closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit cb6fbe36c73982e1043186b983f0c03334989b34)
2026-06-07 19:27:58 +09:00
weiqinl
ed3a0241f3 fix(codex): deliver assistant reply when orphan tool.call lacks result
Keep synthesizing failed tool.result records for transcript consistency,
but skip promptError when a completed Codex turn has deliverable assistant
text so channel users still receive the composed reply.

Fixes #91067

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

(cherry picked from commit ffac77ce811eab528bcce81eec99fd8bd6c70cca)
2026-06-07 19:27:58 +09:00
Vincent Koc
b00e1b2e7b fix(diagnostics): make memory pressure logs actionable
Log warning memory pressure at WARN level, add readable RSS/heap/threshold units, threshold ratio, and a concrete operator next step while preserving raw byte fields for diagnostics consumers.

Fixes #90783
2026-06-07 19:27:29 +09:00
Vincent Koc
cfe5d24889 fix(test): bound remaining child output collectors 2026-06-07 12:25:30 +02:00
Vincent Koc
bae607b9f1 fix(test): execute docker observability proof 2026-06-07 12:22:10 +02:00
Peter Steinberger
f08ee9eb54 fix(protocol): refresh generated send params 2026-06-07 03:21:26 -07:00
Jason Yao
d1cb6cd0b5 fix(media-understanding): preserve native vision skip with imageModel fallback
Fixes #91084

(cherry picked from commit 8aa5148338)
2026-06-07 19:19:27 +09:00
Vincent Koc
8291cfc2f4 fix(test): bound child output buffers 2026-06-07 12:19:08 +02:00
Vincent Koc
bf27221753 fix(tooling): bound source scan file reads 2026-06-07 12:14:09 +02:00
Vincent Koc
88c1af0a2c fix(tooling): bound generated formatter execution 2026-06-07 12:11:21 +02:00
Vincent Koc
6a0fdea90a fix(outbound): materialize buffer-only sends
Fixes #90768

Incorporates the send-buffer materialization shape proposed in #90794 by @LiuwqGit, with maintainer fixes for dry-run, gateway delivery, byte-cap, target-validation, and downstream plugin dispatch paths.
2026-06-07 19:09:49 +09:00
Vincent Koc
85840eb10e fix(dev): align gateway smoke auth contract 2026-06-07 12:07:05 +02:00
Vincent Koc
f7f2532cac test(agents): widen overflow model mock 2026-06-07 19:05:27 +09:00
Vincent Koc
e2524e0438 fix(ci): break plugin import cycles 2026-06-07 19:03:38 +09:00
兰之
58bab0c276 fix(agents): dispatch subagent spawn in process (#90612)
* fix(agents): dispatch subagent spawn in process

* docs: update subagent gateway dispatch note

* fix(gateway): keep in-process dispatch timeout budget

* test(gateway): avoid promise executor timer returns

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-07 03:01:51 -07:00
Vincent Koc
48da8d83d9 fix(e2e): bound parallels update logs 2026-06-07 12:00:57 +02:00
Vincent Koc
363c6923a1 fix(e2e): bound web search smoke logs 2026-06-07 11:57:04 +02:00
Vincent Koc
be617fdd62 fix(e2e): bound telegram docker logs 2026-06-07 11:54:47 +02:00
Vincent Koc
8dff529587 fix(e2e): bound corrupt update logs 2026-06-07 11:52:39 +02:00
Vincent Koc
901f963f62 fix(e2e): bound cleanup smoke logs 2026-06-07 11:50:33 +02:00
Vincent Koc
cdbf6d95ac fix(e2e): bound scenario client logs 2026-06-07 11:48:05 +02:00
Vincent Koc
5d7e0b73a7 fix(e2e): bound mcp client logs 2026-06-07 11:44:55 +02:00
Peter Steinberger
bab18d567b refactor(plugin-sdk): persist dedupe state in sqlite 2026-06-07 02:41:45 -07:00
Vincent Koc
a4e78aec4b fix(test): bound group report child output 2026-06-07 11:40:47 +02:00
Vincent Koc
0f855ea71a fix(e2e): require dashboard smoke assets 2026-06-07 11:38:25 +02:00
Vincent Koc
a7d5d92989 fix(e2e): require zai fallback evidence 2026-06-07 11:33:36 +02:00
Peter Steinberger
6f2b3830f1 fix(qqbot): migrate group tool policy config (#91128)
* fix(qqbot): migrate group tool policy config

* test: stabilize changed check lanes

* style: format changed main files

* test: align CI matrix expectations
2026-06-07 02:33:06 -07:00
clawsweeper[bot]
58b68e92f2 fix(outbound): keep Discord runtime adapters resolvable (#91119)
Summary:
- The branch changes outbound channel bootstrap and resolution so delivery paths prefer send-capable runtime a ...  avoid setup-only shells for runtime delivery, retry non-send-capable bootstraps, and add regression tests.
- PR surface: Source +121, Tests +294. Total +415 across 4 files.
- Reproducibility: yes. The linked stable-release report supplies the user-visible Discord failure, and curren ... p-shell/direct-registry path that can satisfy runtime resolution before a send-capable adapter is verified.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(outbound): keep Discord runtime adapters resolvable
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): reconcile automerge-openclaw-openclaw-90198 with ma…

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

Prepared head SHA: 8711ada0c4
Review: https://github.com/openclaw/openclaw/pull/91119#issuecomment-4641934231

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: thewilloftheshadow
Co-authored-by: thewilloftheshadow <35580099+thewilloftheshadow@users.noreply.github.com>
2026-06-07 09:30:41 +00:00
Vincent Koc
dcba17d019 fix(e2e): stream installer session scans 2026-06-07 11:28:31 +02:00
Peter Steinberger
c2d825ae53 fix: migrate legacy agent registry schema via doctor
Move the shipped legacy shared-state agent database registry repair into doctor. Runtime now fails fast with a doctor repair hint when the old primary-key shape remains.
2026-06-07 02:23:51 -07:00
Vincent Koc
f36e54cd68 fix(e2e): require secret probe success 2026-06-07 11:22:43 +02:00
Vincent Koc
3dc6ac3802 fix(qa): fail closed on skipped suite summaries 2026-06-07 11:20:25 +02:00
Peter Steinberger
ce015cef57 refactor: store sandbox registry in sqlite
Move sandbox registry runtime state to SQLite and migrate legacy JSON registry files/directories via doctor.
2026-06-07 02:05:28 -07:00
Chunyue Wang
e06f6ffc3e fix(doctor): merge legacy codex models safely
Merge disjoint legacy openai-codex model entries into the canonical openai provider without losing safe per-model metadata, params, or models-add markers.

Unsafe provider-level defaults, auth/header/request state, and blocked normalized legacy providers are now preserved for manual cleanup with doctor preview warnings instead of being silently copied into models or repeatedly reported as auto-fixable.

Fixes #90047

Co-authored-by: openperf <16864032@qq.com>
2026-06-07 01:59:31 -07:00
Peter Steinberger
3e4b10fa1c fix: strip Google provider prefix from Gemini paths (#91125)
* fix: strip Google provider prefix from Gemini paths

* test: align qa exit code type
2026-06-07 01:49:45 -07:00
Vincent Koc
e5a9c60851 fix(e2e): bound codex live failure logs 2026-06-07 10:44:06 +02:00
Vincent Koc
677358f4a9 fix(e2e): bound telegram desktop proof logs 2026-06-07 10:42:28 +02:00
Vincent Koc
9e87d316c7 fix(e2e): bound telegram rtt mock logs 2026-06-07 10:41:07 +02:00
Vincent Koc
8cba5f7efd fix(e2e): bound upgrade survivor logs 2026-06-07 10:38:29 +02:00
Vincent Koc
440f315e83 fix(e2e): bound update channel logs 2026-06-07 10:33:14 +02:00
Vincent Koc
b9d530e292 fix(e2e): bound doctor switch logs 2026-06-07 10:31:53 +02:00
Vincent Koc
9b85b36d92 fix(qa): fail whatsapp skipped scenarios 2026-06-07 10:30:15 +02:00
Vincent Koc
9fb8d87f91 fix(e2e): bound plugin update logs 2026-06-07 10:26:59 +02:00
Peter Steinberger
248dfb22ec fix: preserve Foundry Responses reasoning replay ids
Preserve Microsoft Foundry encrypted reasoning replay item ids for Responses continuations while leaving chat-completions streams untouched.

Fixes #91033.
2026-06-07 01:24:07 -07:00
Vincent Koc
e64f2324b9 fix(dev): bound anthropic prompt log tails 2026-06-07 10:22:50 +02:00
Vincent Koc
eae4d284e7 fix(e2e): bound shared helper log output 2026-06-07 10:19:16 +02:00
Vincent Koc
3643a68e49 fix(qa): verify config after restart races 2026-06-07 10:17:06 +02:00
Vincent Koc
a58a6f63ca fix(qa): stream session transcript summaries 2026-06-07 10:14:45 +02:00
Vincent Koc
a931884eb5 fix(qa): require runtime tool failure proof 2026-06-07 10:09:38 +02:00
Peter Steinberger
7a3d24e70c refactor(memory-wiki): store import runs in sqlite (#91108) 2026-06-07 01:04:43 -07:00
Peter Steinberger
d6dffd6ef8 fix: align Xiaomi completions replay compat
Fixes #91106.

Behavior:
- Xiaomi MiMo OpenAI-compatible completions now replay assistant tool-call messages with `reasoning_content: ""` and DeepSeek-style thinking format.
- Adds a provider payload regression for the outbound Xiaomi request.
- Includes a small script fixture lint repair needed after rebasing onto current main.

Proof:
- `OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs src/llm/providers/openai-completions.test.ts src/agents/openai-completions-compat.test.ts`
- `node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.scripts.json scripts/e2e/lib/fixtures/workspace.mjs`
- `pnpm check:test-types`
- `pnpm tsgo:core`
- `pnpm exec oxfmt --check --threads=1 src/llm/providers/openai-completions.ts src/llm/providers/openai-completions.test.ts scripts/e2e/lib/fixtures/workspace.mjs`
- `git diff --check`
- `/Users/steipete/Projects/agent-skills/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- `gh pr checks 91113 --watch --fail-fast`
2026-06-07 01:00:59 -07:00
Jason (Json)
cf378e4cc8 fix(codex): preserve post-tool reasoning liveness
Preserve the Codex post-tool continuation guard for raw reasoning completions and streamed reasoning progress so valid post-tool synthesis stays on the intended completion watchdog instead of falling through to terminal idle behavior.

Verified with focused Codex watchdog tests, test typecheck, scripts lint, autoreview, and CI run 27086637988.

Thanks @fuller-stack-dev.

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-06-07 00:57:14 -07:00
Vincent Koc
589ea28dab test(gateway): smoke real websocket client 2026-06-07 09:46:00 +02:00
Vincent Koc
451765ad27 fix(e2e): require live tool result proof 2026-06-07 09:44:35 +02:00
Vincent Koc
4f9f7e20d4 fix(test): bound otel collector output 2026-06-07 09:42:15 +02:00
Dallin Romney
ebabf5022f perf(qqbot): narrow tool discovery cold load (#90780)
* perf: narrow qqbot tool discovery load

* fix(qqbot): load bridge entries through sidecars
2026-06-07 00:41:11 -07:00
Vincent Koc
1de4a3e9ea fix(test): stream group report logs 2026-06-07 09:40:46 +02:00
Vincent Koc
ea3a915cb5 fix(e2e): bound plugin fixture logs 2026-06-07 09:34:36 +02:00
Vincent Koc
ef52798254 fix(e2e): require tool-search session proof 2026-06-07 09:31:21 +02:00
Vincent Koc
78f2af9ac9 fix(e2e): bound workspace fixture output 2026-06-07 09:27:18 +02:00
Christine Yan
22276e6de0 fix(lmstudio): preserve wizard prompter binding
Bind LM Studio wizard prompter callbacks before storing them so class-backed gateway setup sessions keep their receiver and no longer crash when selecting LM Studio.

Thanks @christineyan4.

Co-authored-by: Christine Yan <christine.yan4@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-07 00:26:39 -07:00
Chunyue Wang
697d2d040c fix(agents): block message send loops with volatile delivery ids
Fixes #89090.

Release note: prevent repeated visible message sends from bypassing loop blocking when delivery results include fresh message, file, poll, receipt, run, idempotency, or timestamp fields. Normalizes send-like result hashing for the core message tool, sessions_send, and provider-docked messaging tools while preserving stable routing and outcome facts.

Verification:
- node scripts/run-vitest.mjs src/agents/tool-loop-detection.test.ts src/agents/tools/message-tool.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode local --parallel-tests "node scripts/run-vitest.mjs src/agents/tool-loop-detection.test.ts src/agents/tools/message-tool.test.ts"
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --parallel-tests "node scripts/run-vitest.mjs src/agents/tool-loop-detection.test.ts src/agents/tools/message-tool.test.ts"
- gh pr checks 89109 --watch --interval 30

Co-authored-by: openperf <16864032@qq.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-07 00:24:58 -07:00
Vincent Koc
0a2cad7e68 fix(e2e): bound live plugin transcript scans 2026-06-07 09:24:31 +02:00
Vincent Koc
0bf487e4cb fix(e2e): bound kitchen sink fixture logs 2026-06-07 09:22:36 +02:00
Vincent Koc
a3ab0e2534 fix(e2e): stream kitchen sink log scans 2026-06-07 09:20:45 +02:00
Vincent Koc
ab33fe33d1 fix(e2e): invoke kitchen sink image job 2026-06-07 09:18:22 +02:00
Yzx
054312672a fix(llm): preserve LM Studio Responses tool arguments
Preserve streamed Responses tool-call argument deltas when the final done event omits or sends empty arguments, fixing LM Studio argument-bearing tools from arriving as `{}`.

Fixes #90585.

Thanks @849261680.
2026-06-07 00:13:26 -07:00
Vincent Koc
c9f884fb28 fix(e2e): restrict degraded runtime readiness 2026-06-07 09:11:36 +02:00
Vincent Koc
f8db47e340 fix(e2e): verify bundled plugin source roots 2026-06-07 09:07:27 +02:00
Vincent Koc
cd1a90b310 fix(e2e): verify kitchen sink inspect-all 2026-06-07 09:03:48 +02:00
Vincent Koc
fff3b15fd7 fix(e2e): bound kitchen sink failure logs 2026-06-07 09:01:52 +02:00
Yuval Dinodia
bb27cbd46d fix(agents): decode xai and venice tool-call arguments exactly once
Decode HTML-entity escaped xAI and Venice tool-call arguments through the shared core compat path exactly once, preventing literal entities such as &amp; from being over-decoded before tool execution and transcript persistence. Removes xAI's duplicate provider-local decoder and keeps regression coverage for the shared core wrapper, xAI stream wrapper, and Venice compat path. Thanks @yetval for the fix.
2026-06-06 23:59:01 -07:00
Vincent Koc
8cb018e1f7 fix(e2e): require strict survivor readiness 2026-06-07 08:58:53 +02:00
Peter Steinberger
0566b96927 refactor(matrix): store crypto sidecars in sqlite (#91100) 2026-06-06 23:57:10 -07:00
Vincent Koc
b38e7105ec fix(e2e): bound parallels log version reads 2026-06-07 08:51:41 +02:00
Vincent Koc
6f35f96274 fix(dev): lazy-load telegram pairing smoke 2026-06-07 08:48:40 +02:00
Vincent Koc
f7aea2ad33 fix(e2e): report skipped secret proofs 2026-06-07 08:44:26 +02:00
Andi Liao
97d68b6902 fix(google): handle compressed Vertex ADC token responses
Decode Google Vertex authorized_user ADC OAuth token refresh responses from bytes so gzip-compressed token payloads still expose access_token. Adds a regression test for the compressed token response path while preserving plain JSON handling and the custom fetch seam.

Proof: OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs extensions/google/transport-stream.test.ts; pnpm exec oxfmt --check extensions/google/vertex-adc.ts extensions/google/transport-stream.test.ts; pnpm tsgo:extensions; git diff --check origin/main...HEAD; autoreview --mode branch --base origin/main. PR CI check-test-types failure was reproduced on current origin/main 607bbe4f5c and is unrelated to this two-file Google provider change.

Thanks @liaoandi for the fix and live Google Vertex ADC proof.
2026-06-06 23:42:35 -07:00
Vincent Koc
2fe7b5e8c9 fix(dev): harden smoke log diagnostics 2026-06-07 08:42:16 +02:00
Dallin Romney
a77d0fdd97 fix(test): type overflow resolver mock (#91098) 2026-06-06 23:40:37 -07:00
Vincent Koc
607bbe4f5c fix(dev): validate ios node smoke payloads 2026-06-07 08:36:06 +02:00
Vincent Koc
cd9c643dc6 fix(e2e): require causal telegram rtt canary 2026-06-07 08:25:11 +02:00
Vincent Koc
6bfd47af38 fix(e2e): clean interrupted docker harness runs 2026-06-07 08:17:16 +02:00
Peter Steinberger
08ae0e6d29 refactor: store Zalo hosted media in plugin state
Move Zalo hosted outbound media metadata and expiry into plugin state, add SDK chunked hosted media storage, and keep CI/type/lint gates green after rebase.
2026-06-06 22:56:48 -07:00
Vinayaka Jyothi
443ac732a1 fix(minimax): keep thinking active for M3
Fix MiniMax-M3 Anthropic-compatible requests so OpenClaw no longer sends the disabled-thinking payload that makes M3 return empty content. M3 defaults now stay on MiniMax's omitted/adaptive thinking path, explicit `/think off` is still respected, and MiniMax-M2.x keeps the disabled-thinking default that prevents reasoning_content leaks.

Also wires the MiniMax thinking policy through bundled provider-policy loading so pre-runtime and configless embedded-agent paths resolve the same defaults.

Thanks @IamVNIE for the live MiniMax API repro and initial patch.
2026-06-06 22:56:17 -07:00
Vincent Koc
0880fd94c6 fix(e2e): preserve dist during bun restore 2026-06-07 07:54:05 +02:00
Vincent Koc
ab41e25b2c fix(dev): require telegram final message id 2026-06-07 07:48:13 +02:00
Vincent Koc
03ae553ecd fix(e2e): bound telegram package logs 2026-06-07 07:46:26 +02:00
Vincent Koc
d943d36677 fix(e2e): require cron cleanup success status 2026-06-07 07:43:33 +02:00
Vincent Koc
b6c45a9301 fix(e2e): require web search success marker 2026-06-07 07:42:24 +02:00
Vincent Koc
5ab3104a15 test(e2e): cover openwebui chat smoke 2026-06-07 07:38:04 +02:00
Vincent Koc
d59d3f87b0 fix(e2e): bound bundled lifecycle log output 2026-06-07 07:36:51 +02:00
Vincent Koc
d801bb5be0 fix(e2e): report resource breaches on failed runs 2026-06-07 07:34:21 +02:00
Vincent Koc
3597cfc7bc fix(qa): allow gauntlet observations to fail 2026-06-07 07:31:56 +02:00
Vincent Koc
6590f764b5 fix(e2e): bound docker failure log printing 2026-06-07 07:29:35 +02:00
Vincent Koc
0b0893aa21 fix(e2e): bound kitchen sink log traversal 2026-06-07 07:25:46 +02:00
Vincent Koc
251bd61e22 fix(e2e): reject kitchen sink rpc error envelopes 2026-06-07 07:23:33 +02:00
Vincent Koc
db24e8e76b fix(e2e): validate bundled runtime health smoke 2026-06-07 07:21:34 +02:00
Vincent Koc
22466240a5 fix(e2e): run openwebui release chat smoke 2026-06-07 07:18:56 +02:00
Peter Steinberger
e8348c0dc8 refactor(matrix): store sync cache in sqlite
Move Matrix sync cache state into plugin SQLite storage, with startup and doctor migrations for readable legacy bot-storage.json files.\n\nVerification: focused Matrix and QA tests passed locally; focused touched-file oxlint and git diff --check passed; autoreview clean. CI failures are current main/unrelated: lint/type/madge/gateway-watch issues outside the Matrix diff.
2026-06-06 22:17:41 -07:00
Vincent Koc
690a04f81e fix(e2e): require gateway network health payload 2026-06-07 07:16:19 +02:00
Vincent Koc
ed7f259ce7 fix(e2e): bound onboard log polling reads 2026-06-07 07:14:37 +02:00
Vincent Koc
892dbb5ebb fix(dev): require gateway smoke health payload 2026-06-07 07:11:21 +02:00
Vincent Koc
7e7ea0fed1 fix(qa): validate rpc rtt smoke payloads 2026-06-07 07:09:01 +02:00
Vincent Koc
fa614d0907 fix(qa): tighten kitchen sink rpc proof 2026-06-07 07:03:43 +02:00
Vincent Koc
1d2bebbb41 fix(mac): scope restart app cleanup 2026-06-07 06:58:46 +02:00
Vincent Koc
ab645aca31 fix(test): require enabled live shard proof 2026-06-07 06:47:02 +02:00
Vincent Koc
1f0cf074cf fix(mac): scope build-and-run cleanup 2026-06-07 06:35:44 +02:00
Vincent Koc
03f1bf9a4d fix(qa-lab): fail missing parity tool results 2026-06-07 06:25:28 +02:00
Vincent Koc
0e1d2b3ef4 fix(qa-lab): require tool evidence in parity metrics 2026-06-07 06:20:16 +02:00
Vincent Koc
9b1c0dac68 fix(e2e): bound codex live assertion reads 2026-06-07 06:15:42 +02:00
Vincent Koc
cfaae7761d fix(e2e): fail lifecycle resource spikes 2026-06-07 06:11:15 +02:00
Vincent Koc
a372429a96 fix(e2e): cap docker harness resources 2026-06-07 06:06:12 +02:00
Vincent Koc
a737826320 fix(qa-lab): require live runtime tool evidence 2026-06-07 05:57:37 +02:00
Peter Steinberger
f4098e64e4 docs(config): document reasoning content compat flag 2026-06-07 04:52:37 +01:00
Krasimir Kralev
c45295cc33 fix(config): honor reasoning content compat flag
Allow custom OpenAI-compatible providers to opt into the existing DeepSeek assistant reasoning replay contract through persisted model compat config. Closes #89660.
2026-06-06 20:50:50 -07:00
Vincent Koc
e7b09fba37 fix(e2e): guard openwebui docker resources 2026-06-07 05:48:01 +02:00
wsyjh8
a1f1895b1b fix(config): allow thinkingLevelMap in persisted model schema
Allow persisted provider model entries to carry strict thinkingLevelMap values so Microsoft Foundry Entra onboarding can save generated reasoning model config. Closes #91011.
2026-06-06 20:44:15 -07:00
Mukunda Rao Katta
78135c3a29 fix(agents): suppress DeepSeek thinking for Foundry aliases
Fix Microsoft Foundry DeepSeek V4 alias providers by suppressing the DeepSeek-native `thinking` fallback and stripping DeepSeek replay fields on Foundry/non-native compat paths while preserving native DeepSeek and OpenRouter/Together reasoning controls.

Verified with focused embedded-runner tests, formatting/diff checks, autoreview, passing real-behavior proof gate, CI embedded-agent shard, issue #90520 reporter live A/B proof, and a local attempted gateway probe blocked before provider dispatch by model allowlist.

Known red CI lanes are unrelated to the touched files and documented in the pre-merge PR comment.

Fixes #90520
2026-06-06 20:37:52 -07:00
Vincent Koc
f94d3b1d8c fix(e2e): scan gateway readiness logs incrementally 2026-06-07 05:36:12 +02:00
Vincent Koc
586cf18b8d fix(e2e): bound docker log printing 2026-06-07 05:30:00 +02:00
Vincent Koc
c7a9dc1cc2 fix(qa-lab): link runtime tool output to planned calls 2026-06-07 05:27:34 +02:00
Omar Shahine
203dee9033 docs(imessage): clarify macOS library validation setup
Clarify that modern macOS iMessage private-API injection needs SIP disabled plus Library Validation relaxed, and document the verified macOS 26.5.1 Tahoe behavior without publishing unsupported AMFI boot-args guidance.\n\nVerification:\n- pnpm docs:list\n- git diff --check\n- pnpm check:docs\n\nThanks @omarshahine!
2026-06-06 20:20:57 -07:00
Vincent Koc
480c9a97b6 fix(e2e): tighten kitchen sink rpc assertions 2026-06-07 05:15:12 +02:00
Vincent Koc
cfeed10b01 fix(e2e): bound live plugin agent output 2026-06-07 05:09:31 +02:00
Peter Steinberger
3006b85db0 fix(openrouter): reconcile streamed generation cost
Fix OpenRouter streamed billing reconciliation by replacing the streamed estimated cost with the provider generation metadata total when the final streamed response includes a response id.

Verified with focused OpenRouter tests, full OpenRouter extension tests, formatting/diff checks, autoreview, official OpenRouter generation metadata docs, and a live OpenRouter API stream plus delayed generation lookup. Remaining CI failures were inspected and are unrelated existing failures outside the OpenRouter surface.

Fixes #68066
2026-06-06 20:06:57 -07:00
Peter Steinberger
a4236bd6fa refactor(memory-wiki): store source sync state in sqlite
* refactor(memory-wiki): store source sync state in sqlite

* fix(memory-wiki): satisfy source sync migration lint
2026-06-06 20:04:27 -07:00
Vincent Koc
ec55179504 fix(parallels): stream host command logs 2026-06-07 05:00:48 +02:00
Vincent Koc
416008dd10 fix(qa): reject empty gauntlet pass evidence 2026-06-07 04:56:32 +02:00
Vincent Koc
f55433bf31 fix(qa-lab): require runtime tool output evidence 2026-06-07 04:52:10 +02:00
Vincent Koc
f8f53de45a fix(e2e): bound kitchen sink log scans 2026-06-07 04:47:23 +02:00
Vincent Koc
6c9e7de04d fix(e2e): bound plugin assertion log reads 2026-06-07 04:42:15 +02:00
Vincent Koc
bcfd7164de fix(e2e): bound plugin update log assertions 2026-06-07 04:40:38 +02:00
Vincent Koc
e32707458d fix(e2e): bound secret configure pty output 2026-06-07 04:37:41 +02:00
Vincent Koc
7f7614276b fix(gateway): timeout stalled mcp bodies 2026-06-07 04:32:33 +02:00
Vincent Koc
c40d2c45bf fix(e2e): honor kitchen sink output caps 2026-06-07 04:26:55 +02:00
Vincent Koc
4911615e72 fix(e2e): bound credential payload chunks 2026-06-07 04:24:35 +02:00
Vincent Koc
326c4e0e35 fix(e2e): bound secret resolver stdin 2026-06-07 04:19:16 +02:00
Vincent Koc
06e8a74473 fix(apns): bound response body capture 2026-06-07 04:12:59 +02:00
Vincent Koc
801df108f0 fix(cli): bound exec approvals stdin 2026-06-07 04:08:13 +02:00
Vincent Koc
51b64b8198 fix(proxy): stream debug capture bodies 2026-06-07 04:06:26 +02:00
Vincent Koc
b804d20da7 refactor(test): share channel contract file discovery 2026-06-07 03:59:03 +02:00
Vincent Koc
2b7d7841d2 fix(test): bound prompt capture bodies 2026-06-07 03:55:48 +02:00
Peter Steinberger
0551af92b0 fix(gemini): accept empty grounding metadata
Fixes #88528.

Gemini web_search now accepts successful Google Search grounding responses that include candidate text and an empty `groundingMetadata` object without `groundingChunks`, returning wrapped content with `citations: []` instead of throwing `Gemini API error: malformed JSON response`.

Proof: live direct Gemini API reproduced the empty-grounding response shape; live OpenClaw provider failed before and succeeded after; `node scripts/run-vitest.mjs extensions/google/web-search-provider.test.ts`; `pnpm lint:web-search-provider-boundaries`; targeted oxfmt check; `git diff --check`; autoreview clean.

CI note: admin bypass used for unrelated failures in memory-core/device-pair/scripts, an existing core architecture cycle, and gateway-watch; PR diff touched only the two Gemini files.
2026-06-06 18:54:18 -07:00
Vincent Koc
344db67a00 fix(test): flush prompt probe gateway logs 2026-06-07 03:52:45 +02:00
Vincent Koc
08e3846470 fix(test): close otel receiver sockets 2026-06-07 03:49:48 +02:00
Vincent Koc
dbde27af4b fix(test): flush static artifact logs 2026-06-07 03:46:44 +02:00
alkor2000
2bd1c7b1c9 fix(vertex): route eu/us multi-region to .rep.googleapis.com host
Fixes #89891.

Route Google Vertex `eu` and `us` multi-region locations to the REP hosts used by `@google/genai`, and keep native Vertex endpoint trust exact to those two hosts.

Verification before merge:
- Live 1Password-backed GCP service-account probe: `eu-aiplatform.googleapis.com` returned Google HTML 404; `aiplatform.eu.rep.googleapis.com` reached Vertex JSON `PERMISSION_DENIED` with the same token.
- `node scripts/run-vitest.mjs src/agents/provider-attribution.test.ts extensions/google/vertex-multi-region-host.test.ts extensions/google/api.test.ts` passed.
- `git diff --check` passed.
- `autoreview --mode branch --base origin/main` clean.
- Real behavior proof passed on latest head.
- ClawSweeper re-review: ready for maintainer review, proof sufficient.

CI note: merged with maintainer approval despite red CI because the failures were unrelated to this PR and reproduced on untouched paths: `extensions/acpx/doctor-contract-api.ts`, `extensions/device-pair/notify.ts`, script lint, and existing architecture/gateway-watch checks.

Co-authored-by: alkor2000 <200923177@qq.com>
2026-06-06 18:39:49 -07:00
Peter Steinberger
3f5e001844 fix: store memory-core dreams state in sqlite (#91056) 2026-06-06 18:38:45 -07:00
Vincent Koc
1222f7a6bc fix(test): wait for cross os command logs 2026-06-07 03:35:54 +02:00
Vincent Koc
c4bc366a4c fix(test): clean up codex bind startup failures 2026-06-07 03:31:12 +02:00
Vincent Koc
50437d02c1 fix(test): clean up codex harness startup failures 2026-06-07 03:28:56 +02:00
Vincent Koc
61bb7d5523 fix(test): clean up cli backend live startup failures 2026-06-07 03:27:37 +02:00
Vincent Koc
3060ebf052 fix(test): restore tool search gateway e2e env 2026-06-07 03:24:24 +02:00
Vincent Koc
ecec1b9a59 fix(test): clean up acp live startup failures 2026-06-07 03:21:46 +02:00
Vincent Koc
7f885d5a39 fix(test): restore mcp code mode e2e env 2026-06-07 03:17:46 +02:00
Vincent Koc
3a6696951e fix(test): close rpc rtt websocket on failure 2026-06-07 03:14:57 +02:00
Vincent Koc
1d371eb5ae fix(qa): stop queued gateway rpc calls 2026-06-07 03:12:03 +02:00
Vincent Koc
e75d7cda8f fix(test): fail live gateway startup skips 2026-06-07 03:09:19 +02:00
Vincent Koc
919befbbb6 fix(qa): gate character eval on suite summary 2026-06-07 03:04:53 +02:00
Vincent Koc
d034e9698a fix(qa): trust parity scenario rows for metrics 2026-06-07 02:59:19 +02:00
Vincent Koc
51848de462 fix(test): gate package telegram on summary failures 2026-06-07 02:57:24 +02:00
Vincent Koc
e12141fa9f fix(qa): gate live transport exits on summaries 2026-06-07 02:53:50 +02:00
Vincent Koc
6d2566682a fix(qa): fail suite on summary scenario failures 2026-06-07 02:49:02 +02:00
Vincent Koc
154ee9fd23 fix(test): require source summary scenario evidence 2026-06-07 02:43:27 +02:00
Vincent Koc
0a37f797f2 fix(test): validate gauntlet qa summary counts 2026-06-07 02:41:39 +02:00
Vincent Koc
b45e3028e0 fix(test): require kitchen sink install rss proof 2026-06-07 02:38:58 +02:00
Matt H
983b65b0e0 feat(parallel): add free Parallel Search MCP as the zero-config default web_search provider (#90849)
* feat(parallel): add free Parallel Search MCP as the zero-config default web_search provider

Registers two Parallel web_search providers in the parallel plugin:
- parallel-free: keyless, always the free hosted Search MCP (search.parallel.ai/mcp);
  the zero-config default (autoDetectOrder 76) so web_search works with no key.
- parallel: the existing paid v1 REST API (requires PARALLEL_API_KEY).

Shared query/result normalization lives in parallel-search-normalize.ts (used by both
transports); a minimal Streamable-HTTP JSON-RPC client (parallel-mcp-search.runtime.ts)
backs the free path. UI brands the tool-call chip 'Parallel Web Search' on the free path
via a searchTransport marker; setup default mirrors runtime auto-detect.

* chore(parallel): register parallel-free in doctor legacy-web-search owners

parallel-free is a bundled web_search provider, so add it to the doctor's
exhaustive BUNDLED_LEGACY_WEB_SEARCH_OWNERS map (owned by the parallel plugin)
and the NON_MIGRATED set — it has no legacy tools.web.search.* shape, so this is
a no-op for migration, matching paid parallel/tavily. Keeps the registry
complete. (Spotted by diffing the earlier local WIP branch.)

* docs(parallel): restore concise frontmatter summary

* docs(parallel): clearer, professional copy; drop v1 REST jargon and UI-label claim

- Frame the two providers as Parallel Search (Free) vs paid Parallel Search;
  remove internal 'v1 REST API' wording.
- Remove conversational/overstated phrasing ('out of the box for everyone').
- Remove the 'labeled Parallel Web Search in the UI' claim (only renders in the
  Control UI, not the TUI). Scope the searchTransport code comment accordingly.

* revert(parallel): drop the "Parallel Web Search" tool-call branding

The label only rendered in the Control UI, never the TUI (a separate renderer
via src/agents/tool-display.ts). Extending it would put provider-specific
labeling into a shared/core display path, against the plugin-agnostic-core rule.

Reverts the Control-UI labelOverride wiring and removes the now-orphaned
searchTransport marker from the free provider's result. The result still carries
provider: "parallel-free".

* fix(parallel): cap free Search MCP session_id at its 100-char tools/list contract

The free parallel-free provider reused the paid ParallelSearchSchema, whose
session_id allows 1000 chars, but the live Search MCP tools/list schema caps
session_id at 100. Parameterize normalizeParallelSessionId(value, maxLength);
the free path passes 100 (paid keeps 1000) and advertises the tighter bound in
its own ParallelFreeSearchSchema. An over-limit caller id is dropped and a
fresh in-contract id is minted. Updates tests and docs accordingly.
2026-06-06 17:36:28 -07:00
Vincent Koc
8516f37563 fix(scripts): reject loose run env kill delay 2026-06-07 02:35:08 +02:00
Vincent Koc
e28dd6dd6e fix(test): reject loose startup memory limits 2026-06-07 02:33:15 +02:00
Vincent Koc
b32d769069 fix(test): require live shard file evidence 2026-06-07 02:30:18 +02:00
Vincent Koc
824d5d44cd fix(test): require advisory cli provider skips 2026-06-07 02:23:27 +02:00
Vincent Koc
e12136a7bb fix(test): require strict codex models proof 2026-06-07 02:21:23 +02:00
Vincent Koc
1ce84627e8 fix(test): reject loose release journey limits 2026-06-07 02:11:49 +02:00
Vincent Koc
4dae3b3071 fix(ci): require kova partial evidence 2026-06-07 02:10:23 +02:00
Vincent Koc
5378cf527e fix(test): require live model successes 2026-06-07 02:08:40 +02:00
Vincent Koc
ba46d00589 fix(test): fail secret proof signal exits 2026-06-07 02:06:21 +02:00
Vincent Koc
b6cbb4b861 fix(test): fail acp bind auth errors 2026-06-07 02:04:18 +02:00
Vincent Koc
cb5c513d58 fix(test): fail acp bind missing transcript 2026-06-07 02:02:12 +02:00
Vincent Koc
029e8f0153 fix(test): retry cli backend codex timeouts 2026-06-07 02:00:12 +02:00
Peter Steinberger
05c3325b0a fix: store acpx process state in sqlite
Move ACPX gateway identity and live process leases into SQLite-backed plugin state. Add doctor migration for legacy runtime state and preserve process cleanup identity checks across the storage move.
2026-06-06 16:49:47 -07:00
Vincent Koc
157da3621a fix(gateway): close slow direct response consumers 2026-06-07 01:45:47 +02:00
Vincent Koc
ee7cafafeb fix(test): retry codex harness live timeouts 2026-06-07 01:40:44 +02:00
Vincent Koc
172c3f6064 fix(gateway): classify mcp json-rpc failures 2026-06-07 01:35:40 +02:00
Vincent Koc
46e12e7aff fix(gateway): cap mcp loopback tool cache 2026-06-07 01:27:55 +02:00
Vincent Koc
5f7cfd6451 fix(test): require perf budget source 2026-06-07 01:24:10 +02:00
Vincent Koc
bb0384e884 fix(test): require gateway smoke history evidence 2026-06-07 01:21:49 +02:00
Vincent Koc
e74d98bd65 fix(ci): fail release qa verifier closed 2026-06-07 01:17:58 +02:00
Vincent Koc
2accfeedc7 fix(test): require resource evidence in perf scripts 2026-06-07 01:11:24 +02:00
Peter Steinberger
ba447d5afc fix: store device-pair notify state in sqlite
Move Device Pair notify subscribers and delivery dedupe state into SQLite-backed plugin state. Add doctor migration for legacy notify subscribers; request-id delivery state is cache-only and rebuilt.
2026-06-06 16:10:16 -07:00
Vincent Koc
76435679f5 fix(test): require live runtime parity evidence 2026-06-07 01:05:44 +02:00
Vincent Koc
f5c345b3fe fix(test): require native live shard proof 2026-06-07 00:57:51 +02:00
Vincent Koc
369793d9ab fix(test): require telegram rtt samples 2026-06-07 00:54:13 +02:00
Vincent Koc
a9706ddef2 fix(ci): require live kova evidence 2026-06-07 00:48:54 +02:00
Vincent Koc
9564ee25b2 fix(test): reject failed agent reply markers 2026-06-07 00:45:00 +02:00
Vincent Koc
c3dca12274 fix(test): ignore error reply payload markers 2026-06-07 00:36:02 +02:00
Vincent Koc
441a73c492 fix(ci): require docker e2e summaries 2026-06-07 00:35:03 +02:00
Vincent Koc
c575b9782e fix(test): require group report evidence 2026-06-07 00:31:52 +02:00
Vincent Koc
8ac2ffde2a fix(ci): require source performance artifacts 2026-06-07 00:27:48 +02:00
Vincent Koc
d5ef040e65 fix(test): require live media providers 2026-06-07 00:25:05 +02:00
Vincent Koc
84bcae95a0 fix(ci): fail closed on partial kova reports 2026-06-07 00:17:20 +02:00
Vincent Koc
84275d6608 fix(test): forward kitchen rpc env knobs 2026-06-07 00:13:02 +02:00
Vincent Koc
2876906da5 fix(ci): require kova resource metrics 2026-06-07 00:10:37 +02:00
Vincent Koc
368f687735 fix(test): require tool search execution proof 2026-06-07 00:07:07 +02:00
Vincent Koc
1b6bc2ef7d fix(test): require native web search proof 2026-06-07 00:02:29 +02:00
Vincent Koc
2102166f86 fix(test): normalize docker stats heartbeat 2026-06-06 23:59:33 +02:00
Vincent Koc
f7b6ee48fd fix(test): require chat tool-call-only proof 2026-06-06 23:53:08 +02:00
Vincent Koc
a5fcab9ff8 fix(test): reject malformed plugin uninstall config 2026-06-06 23:50:30 +02:00
Vincent Koc
39e27c8276 fix(test): require lifecycle uninstall config proof 2026-06-06 23:46:46 +02:00
Vincent Koc
a78234a5d3 fix(test): validate kitchen sink rpc outputs 2026-06-06 23:45:00 +02:00
Vincent Koc
1f6eabb09f fix(test): require live tool transcript evidence 2026-06-06 23:41:52 +02:00
Shakker
caae4c9109 test: manage update startup env 2026-06-06 22:10:40 +01:00
Shakker
c6bbb55fb5 fix: scope llm api key env 2026-06-06 22:10:01 +01:00
Vincent Koc
7f4ddf62ea fix(test): validate auth profile env refs 2026-06-06 23:09:43 +02:00
Shakker
f6b6cf6d6c test: manage chat reply media state 2026-06-06 22:08:30 +01:00
joshp123
5d5bc5c84d Revert "Fix talk config secret resolution"
This reverts commit 4500f02fe6.
2026-06-06 23:07:03 +02:00
Shakker
de4ef48323 fix: manage skill autocapture state 2026-06-06 22:04:17 +01:00
Shakker
133585d97f test: wrap install download state fixture 2026-06-06 22:03:49 +01:00
Vincent Koc
a33077d9c6 fix(test): reject missing numeric flag values 2026-06-06 23:01:21 +02:00
Shakker
78f67fa85f fix: manage diagnostic session state 2026-06-06 21:59:54 +01:00
Shakker
27406dc6fb test: scope logging config env 2026-06-06 21:59:24 +01:00
joshp123
4500f02fe6 Fix talk config secret resolution 2026-06-06 22:58:55 +02:00
Shakker
86792c0319 fix: manage gateway skills state fixtures 2026-06-06 21:58:04 +01:00
Vincent Koc
a3e969101c fix(perf): reject missing cpu scenario values 2026-06-06 22:57:19 +02:00
Vincent Koc
5b88ddfb99 fix(perf): reject ambiguous changed bench args 2026-06-06 22:55:56 +02:00
Vincent Koc
c5f40275f5 fix(rpc): reject missing rtt option values 2026-06-06 22:54:07 +02:00
Shakker
4a46da7499 test: scope subagent resume state env 2026-06-06 21:53:29 +01:00
Vincent Koc
ba5fa16907 fix(perf): require group report numeric values 2026-06-06 22:52:29 +02:00
Shakker
ca40b3cdc6 test: manage workshop state fixtures 2026-06-06 21:51:01 +01:00
Vincent Koc
5ecfee04f8 fix(ci): reject ambiguous run timing args 2026-06-06 22:50:49 +02:00
Vincent Koc
125b0fc279 fix(ci): reject unknown kova summary flags 2026-06-06 22:49:29 +02:00
Shakker
aa9c5209fc test: restore ssh sandbox env snapshots 2026-06-06 21:49:10 +01:00
Vincent Koc
a470daad12 fix(test): require docker timing limit values 2026-06-06 22:48:19 +02:00
Shakker
f86dd6c0af test: scope session read media env 2026-06-06 21:47:39 +01:00
Vincent Koc
fbcd27e258 fix(perf): require source summary path values 2026-06-06 22:47:00 +02:00
Vincent Koc
8ff0a20744 fix(docs): require sync provenance values 2026-06-06 22:43:04 +02:00
Vincent Koc
bdc317f4a6 fix(release): require validation dispatch values 2026-06-06 22:39:58 +02:00
Vincent Koc
1c0d7c8a57 fix(package): require docker package option values 2026-06-06 22:36:47 +02:00
Vincent Koc
b6b50d893c fix(security): require docker attestation platform values 2026-06-06 22:33:56 +02:00
Vincent Koc
b5b73bd362 fix(plugin): require package manifest run target 2026-06-06 22:31:42 +02:00
Vincent Koc
3f3b757e50 fix(plugin): require runtime build package targets 2026-06-06 22:29:12 +02:00
Vincent Koc
bd7f65d445 fix(package): require package root values 2026-06-06 22:26:53 +02:00
Vincent Koc
b8f5950fe3 fix(ci): reject missing merge diff refs 2026-06-06 22:25:06 +02:00
Vincent Koc
f47f32db46 fix(docs): reject missing glossary diff refs 2026-06-06 22:23:09 +02:00
Vincent Koc
1549172816 fix(ci): reject missing changed scope refs 2026-06-06 22:21:14 +02:00
Vincent Koc
9f5fc45593 fix(security): require audit severity values 2026-06-06 22:19:18 +02:00
Vincent Koc
bedb3e61c6 fix(report): require ownership markdown path 2026-06-06 22:18:03 +02:00
Vincent Koc
c2af0475fe fix(test): require vitest profile output dir 2026-06-06 22:16:53 +02:00
Vincent Koc
66a1cfb7be fix(ci): reject missing release metadata refs 2026-06-06 22:15:34 +02:00
Vincent Koc
457c76964d fix(test): reject missing report artifact paths 2026-06-06 22:13:37 +02:00
Vincent Koc
4b19f820e1 fix(test): reject missing dependency evidence values 2026-06-06 22:12:08 +02:00
Vincent Koc
4bce355318 fix(test): reject missing group report values 2026-06-06 22:09:33 +02:00
Vincent Koc
53044e8717 fix(report): reject missing dependency report paths 2026-06-06 22:07:31 +02:00
Vincent Koc
98c45aa8b5 fix(test): reject missing env mutation roots 2026-06-06 22:05:23 +02:00
Vincent Koc
71f8b9c41e fix(rtt): reject missing path option values 2026-06-06 22:03:50 +02:00
Vincent Koc
59a8137b04 fix(docs): reject missing mdx report paths 2026-06-06 22:02:20 +02:00
Vincent Koc
a791636160 fix(test): reject missing startup memory paths 2026-06-06 22:00:38 +02:00
Vincent Koc
e3d402427c fix(test): reject zero startup RSS samples 2026-06-06 21:59:09 +02:00
Vincent Koc
f5a7f613ee fix(release): use monthly patch versions
Switch release train handling to YYYY.M.PATCH monthly patch numbering, preserve pre-transition compatibility, and pin the June 2026 stable/beta floor at 2026.6.5 after the published beta.

Verification:
- node scripts/run-vitest.mjs run test/appcast.test.ts test/release-check.test.ts test/scripts/package-mac-app.test.ts test/scripts/package-mac-dist.test.ts test/openclaw-npm-release-check.test.ts test/npm-publish-plan.test.ts src/infra/npm-registry-spec.test.ts src/infra/clawhub.test.ts src/plugins/clawhub.test.ts test/plugin-npm-release.test.ts test/scripts/ios-version.test.ts test/scripts/ios-pin-version.test.ts
- node --import tsx scripts/plugin-npm-release-check.ts --base-ref origin/main --head-ref HEAD
- node --import tsx scripts/plugin-clawhub-release-check.ts --base-ref origin/main --head-ref HEAD
- git diff --check origin/main...HEAD
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --no-web-search
2026-06-06 12:26:32 -07:00
Vincent Koc
15cb26e6cb fix(ci): reject empty timing job payloads 2026-06-06 21:22:55 +02:00
Vincent Koc
6ce5182522 fix(perf): reject empty cpu profiles 2026-06-06 21:21:17 +02:00
Vincent Koc
3f18c71ac4 fix(test): require restart bench resource evidence 2026-06-06 21:18:58 +02:00
Vincent Koc
cc2ed8dbf6 fix(test): require passing Telegram evidence reports 2026-06-06 21:14:51 +02:00
Vincent Koc
dfd52e72b3 fix(test): reject empty Kova summaries 2026-06-06 21:13:08 +02:00
Vincent Koc
e1e203ce11 fix(test): require perf budget report evidence 2026-06-06 21:11:05 +02:00
Vincent Koc
8086c44043 fix(mac): fail release packaging without Swift compat lib 2026-06-06 21:06:32 +02:00
Vincent Koc
e974d98811 fix(test): reject missing gateway bench flag values 2026-06-06 21:03:45 +02:00
Vincent Koc
c6ee13529f fix(test): avoid zero rpc rtt artifacts 2026-06-06 21:01:44 +02:00
Vincent Koc
7cd7a4f438 fix(test): preserve kitchen sink log scans 2026-06-06 20:59:58 +02:00
Vincent Koc
f2677d55ec fix(test): require startup bench report overlap 2026-06-06 20:53:08 +02:00
Omar Shahine
cd806101cd fix(imessage): send TTS audio as voice messages (#90853)
Merged via squash.

Prepared head SHA: 258d2d73f3

Reviewed-by: @omarshahine
2026-06-06 11:49:50 -07:00
Vincent Koc
c4b64de017 fix(test): reject zero RSS resource samples 2026-06-06 20:47:51 +02:00
Vincent Koc
bf5e0e9f10 fix(test): parse RPC RTT iteration counts strictly 2026-06-06 20:45:20 +02:00
Vincent Koc
4b2e3656af fix(test): require Vitest JSON reports 2026-06-06 20:43:28 +02:00
Vincent Koc
d5f5cb2430 fix(test): require gateway CPU startup reports 2026-06-06 20:41:31 +02:00
Vincent Koc
77f8b16716 fix(test): fail startup bench missing RSS samples 2026-06-06 20:39:07 +02:00
Vincent Koc
a1ffaafc12 fix(test): reject timed-out startup bench reports 2026-06-06 20:37:29 +02:00
Vincent Koc
46c000a34f fix(test): fail extension memory import failures 2026-06-06 20:35:27 +02:00
Marvinthebored
1af55bc665 fix(agents): stabilize user-turn serialization across turns to preserve prompt cache (#90811)
Merged via squash.

Prepared head SHA: 3572122df7
Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-06 11:17:33 -07:00
Vincent Koc
59365959d3 fix(test): fail startup bench timed-out samples 2026-06-06 20:03:47 +02:00
Vincent Koc
198002b579 fix(test): report mac changed-bench RSS correctly 2026-06-06 20:02:30 +02:00
Vincent Koc
559d723a13 fix(test): anchor startup memory checks to repo root 2026-06-06 20:00:41 +02:00
Vincent Koc
44fbe63bcf fix(mac): derive app build metadata from repo root 2026-06-06 19:57:52 +02:00
Vincent Koc
7056222288 fix(mac): fail closed on invalid release build floors 2026-06-06 19:55:22 +02:00
Vincent Koc
c13cf91787 fix(mac): fail closed when packaging app shutdown stalls 2026-06-06 19:39:37 +02:00
Vincent Koc
b8956b6a56 fix(mac): fail closed when restart cleanup stalls 2026-06-06 19:37:45 +02:00
Vincent Koc
4d142b185e fix(mac): scope restart process cleanup 2026-06-06 19:31:19 +02:00
Vincent Koc
96c5d33d2b fix(agents): read inbound media refs 2026-06-06 10:28:46 -07:00
Vincent Koc
a9ef3adeb3 fix(test): remove stale deadcode allowlist entries 2026-06-06 19:25:22 +02:00
Vincent Koc
e1d18e5d02 fix(ci): surface advisory release QA failures 2026-06-06 19:20:29 +02:00
Vincent Koc
c682919808 fix(gateway): notify session changes from goal commands 2026-06-06 10:11:04 -07:00
Vincent Koc
6324abbe53 fix(test): reject malformed group reports 2026-06-06 19:03:09 +02:00
Vincent Koc
eac192c170 fix(test): fail unready gateway watch runs 2026-06-06 18:54:01 +02:00
Vincent Koc
6f2cb53fc4 fix(test): fail stale Crabbox broker auth 2026-06-06 18:46:28 +02:00
Joseph Krug
daab68efc8 fix(plugins): load memory embedding provider owners at startup
Gateway startup now includes plugin owners for explicit memorySearch.provider and memorySearch.fallback values, including custom models.providers API owners and generic embedding provider contracts.

Sentinel and disabled paths keep existing startup behavior for auto, local, none, disabled memory search, and disabled memory slots.

Adds post-runtime-load diagnostics for configured memory embedding providers that remain unregistered.

Closes #89651

Co-authored-by: Joseph Krug <5925937+joeykrug@users.noreply.github.com>
2026-06-07 00:44:06 +08:00
Vincent Koc
0b591acd77 fix(test): fail skipped explicit live media suites 2026-06-06 18:41:08 +02:00
the sun gif man
47cfacbb87 docs: improve plugin inventory layout (#90922)
Summary:
- The branch changes plugin inventory generation from wide Markdown tables to per-plugin list entries, shorten ... nerated plugin reference landing page, routes Parallel to its setup page, and updates zh-CN glossary terms.
- PR surface: Docs +9, Other +20. Total +29 across 4 files.
- Reproducibility: not applicable. this is a docs layout PR rather than a reproducible runtime bug. Current ma ... and the PR body plus prior review discussion documents before/after screenshot proof for the layout change.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: improve plugin inventory layout

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

Prepared head SHA: c94b7a4bbc
Review: https://github.com/openclaw/openclaw/pull/90922#issuecomment-4638524853

Co-authored-by: joshp123 <joshp123@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: joshp123
Co-authored-by: joshp123 <1497361+joshp123@users.noreply.github.com>
2026-06-06 16:37:39 +00:00
Vincent Koc
9ac685b5fe fix(test): fail filtered live media no-op runs 2026-06-06 18:34:54 +02:00
Vincent Koc
cf152fe76e fix(ci): require valid Kova partial reports 2026-06-06 18:28:22 +02:00
Vincent Koc
52adf91b6f test(e2e): share mcp code mode validation 2026-06-06 18:16:57 +02:00
zenglingbiao
09d6479681 fix(build): ship export session html assets
Align the export-session template asset copy step and build-all cache output with the runtime lookup path so published packages include the HTML export assets at `dist/export-html`.

Adds a focused build-all regression assertion for the output path contract.

Fixes #90843
2026-06-06 09:14:12 -07:00
Vincent Koc
98498f2579 test(e2e): exercise gateway network health client 2026-06-06 18:13:00 +02:00
Vincent Koc
539f745d12 test(gateway): cover smoke health path 2026-06-06 18:09:18 +02:00
Vincent Koc
bc996d3dfa test(e2e): cover mcp code mode validation 2026-06-06 18:06:59 +02:00
nas
63fba5d2fe fix(cron): require HTTP context for server_error retries
Narrow cron server_error retry classification so incidental 500-599 numbers in failure text no longer trigger retryable server_error. Genuine HTTP/status 5xx strings, canonical 5xx phrases, 5xx, and standalone terse codes still retry.

Maintainer proof: focused cron retry tests, formatter/lint/diff checks, clean autoreview, Testbox-through-Crabbox check:changed tbx_01kteteqqrppbzgh560sybe0nk / Actions run 27066938422, and green PR CI on 6124f14850.

Fixes #90947.
2026-06-06 09:06:31 -07:00
Vincent Koc
e099c01a8c fix(e2e): require full kitchen sink tool surface 2026-06-06 18:05:13 +02:00
Vincent Koc
125329cde7 fix(e2e): assert kitchen sink rpc status payloads 2026-06-06 18:03:11 +02:00
Vincent Koc
ec4c79cb38 fix(e2e): require kitchen sink ready body 2026-06-06 18:00:47 +02:00
Vincent Koc
9c2d243803 fix(e2e): require shared gateway readyz proof 2026-06-06 17:56:38 +02:00
Vincent Koc
ac9d4ff2f0 test(gateway): include aborted chat run state in mock 2026-06-06 08:53:01 -07:00
Vincent Koc
b77ef4d6df fix(doctor): stop repeating talk normalization 2026-06-06 08:46:39 -07:00
Vincent Koc
f00e7af3e3 fix(scripts): require rtt readyz readiness 2026-06-06 17:42:19 +02:00
Vincent Koc
69a406118c fix(e2e): preserve docker cleanup failure artifacts 2026-06-06 17:37:12 +02:00
Vincent Koc
ffea7fa647 fix(memory-wiki): accept wiki apply op aliases 2026-06-06 08:35:33 -07:00
Vincent Koc
16921dba7d fix(ui): allow short tweakcn theme ids 2026-06-06 08:29:50 -07:00
Vincent Koc
97758910fa fix(e2e): reject invalid bundled runtime limits 2026-06-06 17:26:19 +02:00
Vincent Koc
d90a94ad16 fix(channels): strip dangling progress italics 2026-06-06 08:23:55 -07:00
Vincent Koc
7b4b238566 fix(tui): show models loading feedback 2026-06-06 08:16:46 -07:00
Vincent Koc
7a62cd5efc fix(gateway): count slugged daily memory status 2026-06-06 08:08:55 -07:00
Vincent Koc
6ace7a6ca8 fix(gateway): close chat abort send race 2026-06-06 08:01:39 -07:00
Vincent Koc
cee432f0f0 fix(auth): prefer agent-local auth profiles
Ensure selected-agent auth profiles are tried before inherited main-agent profiles for the same provider while preserving explicit agent auth order as a hard filter.

Fixes #64274
2026-06-06 07:48:11 -07:00
Vincent Koc
779fb9efe3 fix(e2e): require gateway network TCP readiness 2026-06-06 16:46:57 +02:00
Vincent Koc
da98896f0c test(agents): keep shell env mock current 2026-06-06 07:44:42 -07:00
Nimrod Gutman
59ed6413d9 [codex] Add iOS Apple Review demo mode (#90919)
Merged via squash.

Prepared head SHA: e7f7db3cb5
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-06-06 17:43:48 +03:00
Vincent Koc
bb056dca84 fix(e2e): require TCP gateway readiness 2026-06-06 16:40:59 +02:00
Vincent Koc
205d00bf82 fix(scripts): fail missing test group reports 2026-06-06 16:38:25 +02:00
Vincent Koc
4ff0aa9969 fix(scripts): fail gateway CPU QA summary failures 2026-06-06 16:34:59 +02:00
Vincent Koc
f02750d162 fix(docker): allow spaces in setup mount paths
Allow ordinary spaces in Docker setup host persistence paths while preserving control-character and comma mount guards. Quote generated and base Compose volume scalars so OPENCLAW_CONFIG_DIR, OPENCLAW_WORKSPACE_DIR, and auth-profile secret paths can be parsed when host directories contain spaces.
2026-06-06 07:32:01 -07:00
Vincent Koc
2a48a2655b fix(e2e): reject empty Docker lane plans 2026-06-06 16:28:24 +02:00
Vincent Koc
e0e3012c84 fix(e2e): reject invalid kitchen sink RPC guardrails 2026-06-06 16:26:11 +02:00
Vincent Koc
fc2a7be0bc fix(update): hand off supervised auto-updates 2026-06-06 07:19:46 -07:00
Vincent Koc
10fb3e110d test(live): require CLI backend provider proof 2026-06-06 16:18:20 +02:00
Vincent Koc
a210a53c19 test(live): require Android node core commands 2026-06-06 16:12:56 +02:00
Vincent Koc
51488bf914 test(gateway): add small model live profile
Add the small-model selector to the gateway live-model profile harness and document the OPENCLAW_LIVE_GATEWAY_MODELS=small recipe.\n\nVerification: node scripts/run-vitest.mjs run --config test/vitest/vitest.live.config.ts src/gateway/gateway-models.profiles.live.test.ts; GitHub Actions CI run 27064309683; CodeQL run 27064309687; OpenGrep PR Diff run 27064309689.
2026-06-06 07:10:26 -07:00
Vincent Koc
4af444ab30 fix(agents): count streamed model deltas incrementally
Count streamed text/thinking/tool-call deltas incrementally in model diagnostics instead of repeatedly estimating full event payloads. Updates diagnostics docs and OTEL wording for the new response byte baseline.\n\nVerification: node scripts/run-vitest.mjs run src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts; GitHub Actions CI run 27064304709; CodeQL run 27064304710; OpenGrep PR Diff run 27064304716.
2026-06-06 07:09:49 -07:00
Vincent Koc
5b84ebfc56 test(agents): keep camera media service mock complete 2026-06-06 16:00:30 +02:00
Vincent Koc
4ee50ce18e fix(agents): stream phased text deltas incrementally
Stream same-item phased final-answer deltas incrementally without rereading full partial assistant text on every token. Preserves sanitizer context for split hidden tool-call payloads and keeps full partial reads for item boundaries and text_end finalization.\n\nRefs https://github.com/openclaw/openclaw/issues/86599.
2026-06-06 06:38:28 -07:00
Vincent Koc
31c3e0c3f3 test(live): keep voice-note preflight inside plugins 2026-06-06 06:24:29 -07:00
Vincent Koc
f5eddc2b6d fix(gateway): dedupe delivery mirror history rows 2026-06-06 06:19:35 -07:00
Nimrod Gutman
a547010a95 fix(talk): resolve realtime provider secret refs (#90914)
Merged via squash.

Prepared head SHA: c5a52049be
Reviewed-by: @ngutman
2026-06-06 15:47:13 +03:00
Onur Solmaz
0aea58ab66 fix(memory): fail fast when embeddings provider is unavailable
Fixes #89691.

Memory search now treats explicitly configured non-local embedding providers as required. When that provider is unavailable, search and sync surface an unavailable memory-search result instead of silently returning FTS-only recall.

Unset/default/local/none-style paths keep FTS fallback so existing workflows do not lose keyword recall entirely. The fallback state is now surfaced in diagnostics/status instead of being hidden.

Maintainer merge note: current CI still has unrelated baseline boundary failures in extensions/google/google.live.test.ts and extensions/minimax/minimax.live.test.ts. This PR does not touch those files; the PR-specific memory, docs, lint, type, security, and ClawSweeper checks were reviewed before merge.
2026-06-06 20:39:38 +08:00
Vincent Koc
6b2af6c1ee fix(agents): keep safe tool images without native backend 2026-06-06 05:11:55 -07:00
Vincent Koc
0a08625d79 fix(agents): emit terminal abort lifecycle metadata
Carry terminal abort state into embedded agent lifecycle events before agent_end emits, and include terminal stopReason from the last assistant message when runner metadata is not available yet.

Fixes #66534
2026-06-06 04:54:11 -07:00
Vincent Koc
74331f632b test(live): tolerate ARM provider drift 2026-06-06 03:47:24 -07:00
joshavant
f4a5e5762e feat(android): brand onboarding welcome screen 2026-06-06 05:03:37 -05:00
joshavant
1098063783 fix(android): clarify nearby gateway discovery state 2026-06-06 05:03:37 -05:00
joshavant
b80893f30d chore(android): simplify onboarding entry actions 2026-06-06 05:03:37 -05:00
joshavant
72b387ad48 fix(android): show configured provider readiness 2026-06-06 05:03:37 -05:00
joshavant
44a72cde58 chore(android): remove provider setup footer 2026-06-06 05:03:37 -05:00
joshavant
81312e7aa3 chore(android): remove model catalog section 2026-06-06 05:03:37 -05:00
joshavant
53e50ec127 fix(android): reconnect saved gateway after disconnect 2026-06-06 05:03:37 -05:00
joshavant
485446af8c fix(android): keep sent chat messages in history 2026-06-06 05:03:37 -05:00
joshavant
81f4fe6c11 fix(android): pause gateway pairing retries 2026-06-06 05:03:37 -05:00
joshavant
a2455fcc09 fix(android): keep gateway pairing off main thread 2026-06-06 05:03:37 -05:00
joshavant
e4583b4f57 fix(android): show flavor channel in about 2026-06-06 05:03:37 -05:00
joshavant
9413a5aba5 fix(android): defer runtime startup after first draw 2026-06-06 05:03:36 -05:00
joshavant
b7cafb56fa fix(android): surface voice provider attention 2026-06-06 05:03:36 -05:00
joshavant
efea9ca0f5 chore(android): fix ktlint formatting 2026-06-06 05:03:36 -05:00
Vincent Koc
98f52dcc00 test(live): skip DeepInfra V4 Flash tool sentinel drift 2026-06-06 03:00:12 -07:00
joshavant
32b0b58868 style(ios): use app logo on onboarding intro 2026-06-06 04:41:33 -05:00
joshavant
9942428df0 fix(ios): disable chat composer while offline 2026-06-06 04:41:33 -05:00
joshavant
f40680c826 style(ios): align command section header padding 2026-06-06 04:41:33 -05:00
joshavant
a6582f787c fix(ios): remove extra root tab bottom insets 2026-06-06 04:41:33 -05:00
joshavant
a9a2c34293 fix(ios): stop marking scheduled agents busy 2026-06-06 04:41:33 -05:00
joshavant
2ef0d274fa fix(ios): hide agent sessions from recent sessions 2026-06-06 04:41:33 -05:00
joshavant
dc5c24fbe6 fix(ios): keep chat messages above composer 2026-06-06 04:41:33 -05:00
joshavant
0b87990328 fix(ios): remove command live activity section 2026-06-06 04:41:33 -05:00
joshavant
14f018e794 fix(ios): move approvals to settings 2026-06-06 04:41:33 -05:00
joshavant
81d099f0e9 fix(ios): remove command start work button 2026-06-06 04:41:33 -05:00
joshavant
e8c0d92015 fix(ios): clarify agent chat session 2026-06-06 04:41:32 -05:00
joshavant
67dc71983c fix(ios): show focused session agent 2026-06-06 04:41:32 -05:00
joshavant
be537060ce fix(ios): show recent sessions preview 2026-06-06 04:41:32 -05:00
joshavant
ea7e214bd4 Fix chat history races across agent switches 2026-06-06 04:41:32 -05:00
joshavant
7478e6e485 Fix chat session sync ownership 2026-06-06 04:41:32 -05:00
joshavant
83a6bce835 Fix iOS chat background presentation 2026-06-06 04:41:32 -05:00
joshavant
5c07f7ccf0 Fix iOS selected agent chat routing 2026-06-06 04:41:32 -05:00
joshavant
af50a5959d fix ios onboarding success screen 2026-06-06 04:41:32 -05:00
joshavant
472a30bd3f fix ios skill editor toggle hit target 2026-06-06 04:41:32 -05:00
joshavant
8f6f18b6e7 fix ios operator recovery live activity 2026-06-06 04:41:32 -05:00
joshavant
1746319db5 fix ios operator scope upgrade state 2026-06-06 04:41:32 -05:00
joshavant
19e827c969 fix ios operator admin scope requests 2026-06-06 04:41:32 -05:00
joshavant
f1cf898460 fix ios onboarding tls toggle hit targets 2026-06-06 04:41:32 -05:00
joshavant
7e6134cb12 fix ios onboarding developer toggle hit target 2026-06-06 04:41:32 -05:00
joshavant
2fb5ff3034 fix ios settings bottom scroll inset 2026-06-06 04:41:32 -05:00
joshavant
fbaa5a6f0a fix ios gateway settings control hit targets 2026-06-06 04:41:32 -05:00
joshavant
33cb1c18ac fix ios diagnostics toggle hit targets 2026-06-06 04:41:31 -05:00
joshavant
0ee7cf970c fix ios quick setup suppression toggle 2026-06-06 04:41:31 -05:00
joshavant
762540aa04 fix ios talk controls hit targets 2026-06-06 04:41:31 -05:00
joshavant
73f056a0a4 fix ios chat error banner overlap 2026-06-06 04:41:31 -05:00
joshavant
88f6857c2e fix ios onboarding mode row hit targets 2026-06-06 04:41:31 -05:00
joshavant
c29cc7f82f fix(ios): use safe area inset for settings scroll 2026-06-06 04:41:31 -05:00
Vincent Koc
d4b4a65809 fix(plugins): preserve core embedding providers 2026-06-06 00:30:48 -07:00
Vincent Koc
f94e4f85f0 test(pairing): isolate store state tests 2026-06-05 23:46:11 -07:00
Vincent Koc
c72c82726f fix(installer): print npm debug logs on Windows install failure 2026-06-05 23:16:39 -07:00
Vincent Koc
92242f4f68 fix(test): route extension tests through scoped paths 2026-06-05 22:59:54 -07:00
xydigit-sj
743051d400 fix(uninstall): refuse to remove current working directory during cleanup (#90813)
* fix(uninstall): refuse to remove current working directory during cleanup

* fix(uninstall): guard cleanup ancestors of cwd

---------

Co-authored-by: sallyom <somalley@redhat.com>
2026-06-06 01:51:16 -04:00
Vincent Koc
153a2badb0 fix(release): extend live Docker image pull timeout 2026-06-05 22:34:22 -07:00
Omar Shahine
37aaa5cc2b fix(imessage): frame rpc stdout on LF only (#90845)
Merged via squash.

Prepared head SHA: c62a2dcbf1
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-06-05 22:31:50 -07:00
Kevin Lin
ab7c922825 fix(codex): report completion timeout diagnostics
Surface Codex-specific completion-timeout outcomes and structural diagnostics while preserving the existing replay-safe retry behavior.\n\nVerified with focused Vitest coverage, live forced-timeout Showboat proof, and green PR CI.
2026-06-05 22:00:38 -07:00
Vincent Koc
2fc4511eeb fix(release): retry provider-throttled cross-os agent turns 2026-06-05 21:58:46 -07:00
Vincent Koc
9313471fa5 fix(plugins): strengthen registry root memo fingerprint 2026-06-05 21:23:55 -07:00
brokemac79
2f46a27b40 fix(codex): preserve completed replies after client close (#90790)
Merged via squash.

Prepared head SHA: d948b3543c
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-05 21:22:10 -07:00
Vincent Koc
8e9c377971 test(gateway): stabilize channel restart fake timers 2026-06-05 21:16:41 -07:00
Glucksberg
6f909f6454 Fix OpenAI audio auth to use API keys (#90793)
* fix(media): require api key auth for OpenAI audio

* fix(media): narrow OpenAI audio auth API scope

* fix(media): align OpenAI audio auth selection

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

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-06 00:11:05 -04:00
Vincent Koc
092075534e docs(changelog): complete 2026.6.5 release refs 2026-06-05 20:36:28 -07:00
Vincent Koc
04ecc1aae9 docs(changelog): complete 2026.6.5 notes 2026-06-05 20:23:13 -07:00
Vincent Koc
af4ba6221b docs(changelog): refresh 2026.6.5 notes 2026-06-05 20:04:04 -07:00
clawsweeper[bot]
9cbf18293b fix #90668: [Bug]: macOS node mode can silently self-reconnect in a healthy direct gateway session (#90815)
Summary:
- Adds a macOS node-mode TLS session cache keyed by gateway URL and TLS pin parameters, with Swift tests for reuse and rebuild behavior.
- PR surface: Other +78. Total +78 across 2 files.
- Reproducibility: yes. The source path is clear: current main supplies a fresh TLS session identity into `Gat ... inked macOS WSS proof demonstrates repeated connected callbacks before the cache and one callback after it.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(macos): make TLS session cache lint-safe
- PR branch already contained follow-up commit before automerge: fix #90668: [Bug]: macOS node mode can silently self-reconnect in a h…

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

Prepared head SHA: 1496eac8c1
Review: https://github.com/openclaw/openclaw/pull/90815#issuecomment-4637057530

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.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-06-06 02:44:59 +00:00
clawsweeper[bot]
50aaf1f9b6 fix(memory): resolve adapter default model in plain status identity check (#90816)
Summary:
- This PR updates memory-core index identity resolution to treat an empty configured model as the embedding adapter default and adds a regression test for plain memory status.
- PR surface: Source +5, Tests +33. Total +38 across 2 files.
- Reproducibility: yes. from source and inherited proof: current main compares identity against an unresolved empty model in the plain status path, and the source PR shows the before/after CLI behavior on the same index.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory): resolve adapter default model in plain status identity c…

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

Prepared head SHA: 9741437564
Review: https://github.com/openclaw/openclaw/pull/90816#issuecomment-4637058847

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-06 02:34:29 +00:00
Chunyue Wang
aa8070a76f fix(llm): defer Anthropic stream start event until after message_start (#90697)
Summary:
- The branch moves Anthropic `start` emission into `message_start` handling for the provider and transport stream paths and adds focused ordering/error tests.
- PR surface: Source +5, Tests +149. Total +154 across 4 files.
- Reproducibility: Do we have a high-confidence way to reproduce the issue? Yes from source: current main emit ... ecovery intentionally refuses to retry after any non-error output; no live expired-cache run was performed.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): defer Anthropic transport stream start event until after…

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

Prepared head SHA: 399a243c64
Review: https://github.com/openclaw/openclaw/pull/90697#issuecomment-4632866448

Co-authored-by: openperf <16864032@qq.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-06-06 02:17:54 +00:00
Yzx
b1e4b6b65e fix(agents): coerce non-text/image MCP tool-result blocks to text (fixes #90710) (#90728)
Summary:
- The PR converts wider MCP CallToolResult content blocks into text/image AgentToolResult blocks at the bundle-MCP materialization boundary and adds regression tests.
- PR surface: Source +36, Tests +66. Total +102 across 2 files.
- Reproducibility: yes. Source inspection shows current main lets MCP resource/audio blocks cross into a text/ ...  a spawned stdio MCP server; I did not run a live hosted Anthropic API round trip in this read-only review.

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

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

Prepared head SHA: f70dccf33e
Review: https://github.com/openclaw/openclaw/pull/90728#issuecomment-4634126025

Co-authored-by: 宇宙熊Yzx <53250620+849261680@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-06-06 02:16:10 +00:00
Sahibzada
9e29375cec fix(voice-call): track Twilio streams after connect (#90607)
Summary:
- The PR moves Twilio inbound active-stream tracking from TwiML generation to `registerCallStream` and updates provider tests for connected-stream and no-stream cases.
- PR surface: Source -3, Tests +23. Total +20 across 2 files.
- Reproducibility: yes. from source inspection and supplied before/after output: on current main, one inbound  ... nd inbound parse queues even when no media stream registered. I did not run tests in this read-only review.

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

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

Prepared head SHA: 22575a9f27
Review: https://github.com/openclaw/openclaw/pull/90607#issuecomment-4630012870

Co-authored-by: Sahibzada Allahyar <sahibzada@fastino.ai>
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-06-06 02:15:00 +00:00
keshavbotagent
3a2f54e6a8 fix(telegram): suppress post-final tool error noise
Suppress non-actionable text-only tool/progress noise after Telegram final delivery while preserving terminal final warnings, media payloads, and exec approval prompts.

Use the core nonTerminalToolErrorWarning marker for recovered final tool warnings, and cover suppression plus preservation cases with regression tests.
2026-06-05 18:24:09 -07:00
Harjoth Khara
e5d1fadea7 test(codex): cover thread abandonment after completion-idle timeout (#90027)
Regression coverage for #89974. Confirms that after a
turn_completion_idle_timeout, OpenClaw clears the timed-out Codex
app-server thread binding and the next turn starts a fresh thread instead
of resuming the thread that may hold Codex's generic <turn_aborted> /
user-interrupted marker. No runtime behavior changes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:41:52 -07:00
Josh Lehman
bbfe8ccaf6 fix: refresh prompt fence after compaction writes
Fix embedded attempts falsely reporting session takeover after OpenClaw-owned auto-compaction writes a compaction entry while the prompt fence is released.

The compaction append path now publishes an owned session-file fence only when the guarded SessionManager append produced the expected compaction entry. External or interleaved session-file edits remain takeover errors.

Closes #90729
2026-06-05 17:05:35 -07:00
Yzx
a4f7e4cbb9 fix(google): preserve Vertex ADC catalog auth (#90609)
* fix: preserve Google Vertex ADC catalog auth

* fix: register Google Vertex ADC config marker

* fix: fill Vertex ADC static catalog auth
2026-06-05 18:16:34 -04:00
Yzx
6da3b1f6a3 fix(agents): re-probe single-provider primary during cooldown (#90717)
Fixes #90702.

Allow a single-provider primary to periodically probe through the existing cooldown throttle even when no fallback chain is configured. This lets WHAM/subscription-limit cooldown state recover without waiting for a far-future provider reset timestamp.

Verified:
- node scripts/run-vitest.mjs src/agents/model-fallback.probe.test.ts
- git diff --check
- cherry-pick onto current origin/main and rerun focused regression
2026-06-05 14:20:57 -07:00
dependabot[bot]
2ab4eaa2b1 build(deps): bump docker/login-action from 3.6.0 to 4.1.0 (#74980)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.6.0...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:12:53 -07:00
dependabot[bot]
c965141d67 chore(deps): bump the android-deps group across 1 directory with 9 updates (#86481)
Bumps the android-deps group with 9 updates in the /apps/android directory:

| Package | From | To |
| --- | --- | --- |
| [gradle-wrapper](https://github.com/gradle/gradle) | `9.4.1` | `9.5.1` |
| androidx.compose:compose-bom | `2026.04.01` | `2026.05.01` |
| [dnsjava:dnsjava](https://github.com/dnsjava/dnsjava) | `3.6.4` | `3.6.5` |
| [org.junit.vintage:junit-vintage-engine](https://github.com/junit-team/junit-framework) | `6.0.3` | `6.1.0` |
| [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://github.com/Kotlin/kotlinx.coroutines) | `1.10.2` | `1.11.0` |
| [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) | `1.10.2` | `1.11.0` |
| [com.google.android.material:material](https://github.com/material-components/material-components-android) | `1.13.0` | `1.14.0` |
| com.android.application | `9.2.0` | `9.2.1` |
| com.android.test | `9.2.0` | `9.2.1` |



Updates `gradle-wrapper` from 9.4.1 to 9.5.1
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v9.4.1...v9.5.1)

Updates `androidx.compose:compose-bom` from 2026.04.01 to 2026.05.01

Updates `dnsjava:dnsjava` from 3.6.4 to 3.6.5
- [Release notes](https://github.com/dnsjava/dnsjava/releases)
- [Changelog](https://github.com/dnsjava/dnsjava/blob/master/Changelog)
- [Commits](https://github.com/dnsjava/dnsjava/commits)

Updates `org.junit.vintage:junit-vintage-engine` from 6.0.3 to 6.1.0
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r6.0.3...r6.1.0)

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-android` from 1.10.2 to 1.11.0
- [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md)
- [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.10.2...1.11.0)

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
- [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md)
- [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.10.2...1.11.0)

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
- [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases)
- [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md)
- [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.10.2...1.11.0)

Updates `com.google.android.material:material` from 1.13.0 to 1.14.0
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.13.0...1.14.0)

Updates `com.android.application` from 9.2.0 to 9.2.1

Updates `com.android.test` from 9.2.0 to 9.2.1

Updates `com.android.test` from 9.2.0 to 9.2.1

---
updated-dependencies:
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2026.05.01
  dependency-type: direct:production
  dependency-group: android-deps
- dependency-name: com.android.application
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: android-deps
- dependency-name: com.android.test
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: android-deps
- dependency-name: com.android.test
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: android-deps
- dependency-name: com.google.android.material:material
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: dnsjava:dnsjava
  dependency-version: 3.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: android-deps
- dependency-name: gradle-wrapper
  dependency-version: 9.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
- dependency-name: org.junit.vintage:junit-vintage-engine
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: android-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:07:08 -07:00
dependabot[bot]
c6972a0664 chore(deps): bump github.com/apple/swift-testing (#81757)
Bumps the swift-deps group with 1 update in the /apps/swabble directory: [github.com/apple/swift-testing](https://github.com/apple/swift-testing).


Updates `github.com/apple/swift-testing` from 6.3.1 to 6.3.2
- [Release notes](https://github.com/apple/swift-testing/releases)
- [Commits](https://github.com/apple/swift-testing/compare/6.3.1...6.3.2)

---
updated-dependencies:
- dependency-name: github.com/apple/swift-testing
  dependency-version: 6.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swift-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:05:15 -07:00
dependabot[bot]
662d366f01 chore(deps): bump the actions group across 1 directory with 4 updates (#90601)
Bumps the actions group with 4 updates in the / directory: [github/codeql-action](https://github.com/github/codeql-action), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/build-push-action](https://github.com/docker/build-push-action) and [openai/codex-action](https://github.com/openai/codex-action).


Updates `github/codeql-action` from 4 to 4.36.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v4...v4.36.1)

Updates `docker/setup-buildx-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](4d04d5d948...d7f5e7f509)

Updates `docker/build-push-action` from 7.1.0 to 7.2.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](bcafcacb16...f9f3042f7e)

Updates `openai/codex-action` from 1.7 to 1.8
- [Changelog](https://github.com/openai/codex-action/blob/main/CHANGELOG.md)
- [Commits](5c3f4ccdb2...e0fdf01220)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: openai/codex-action
  dependency-version: '1.8'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 13:37:40 -07:00
dependabot[bot]
aee45f5f73 chore(deps): bump the swift-deps group across 1 directory with 3 updates (#86483)
Bumps the swift-deps group with 3 updates in the /apps/macos directory: [github.com/apple/swift-log](https://github.com/apple/swift-log), [github.com/sparkle-project/sparkle](https://github.com/sparkle-project/Sparkle) and [github.com/steipete/peekaboo](https://github.com/steipete/Peekaboo).


Updates `github.com/apple/swift-log` from 1.12.0 to 1.13.1
- [Release notes](https://github.com/apple/swift-log/releases)
- [Commits](https://github.com/apple/swift-log/compare/1.12.0...1.13.1)

Updates `github.com/sparkle-project/sparkle` from 2.9.1 to 2.9.2
- [Release notes](https://github.com/sparkle-project/Sparkle/releases)
- [Commits](https://github.com/sparkle-project/Sparkle/compare/2.9.1...2.9.2)

Updates `github.com/steipete/peekaboo` from 3.2.1 to 3.3.0
- [Release notes](https://github.com/steipete/Peekaboo/releases)
- [Commits](https://github.com/steipete/Peekaboo/compare/v3.2.1...v3.3.0)

---
updated-dependencies:
- dependency-name: github.com/apple/swift-log
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swift-deps
- dependency-name: github.com/sparkle-project/sparkle
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swift-deps
- dependency-name: github.com/steipete/peekaboo
  dependency-version: 3.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swift-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 13:10:45 -07:00
Dallin Romney
ac9a219692 fix(tui): stabilize optimistic user messages across history reloads, runId reassignment, and abort (#86205)
* fix(tui): preserve optimistic user messages

* refactor(tui): drop unused pending-user chat-log helpers

* fix(tui): reconcile optimistic user row across runId reassignment and abort

* refactor(tui): reuse asDateTimestampMs for history timestamp coercion

* test(tui): fix event-handler chatLog render mock arity
2026-06-05 12:09:24 -07:00
Matt H
db7d70ae4d feat(parallel): add Parallel as a bundled web_search provider (#85158)
- New extensions/parallel package modeled on extensions/exa
- Wires Parallel's POST /v1/search through the generic web_search contract,
  exposing Parallel's recommended {objective, search_queries} shape (plus
  optional count, session_id, client_model) so the model can supply both the
  natural-language goal and 2-3 short keyword queries as Parallel docs advise
- client_model lets the model report its own slug so Parallel can tailor
  optimizations for the consuming model's capabilities; partitions the cache
  by client_model so different models do not silently share ranked excerpts
- Honors top-level tools.web.search.{maxResults,timeoutSeconds,cacheTtlMinutes}
  via the shared SDK helpers (mergeScopedSearchConfig, withTrustedWebSearchEndpoint,
  buildSearchCacheKey, read/writeCachedSearchPayload)
- Auto-detect order 75; auth via PARALLEL_API_KEY or
  plugins.entries.parallel.config.webSearch.apiKey
- Optional baseUrl override for proxies (e.g. Cloudflare AI Gateway)
- Threads caller-supplied session_id through follow-up calls; strips
  auto-generated session_id from the shared cache to avoid cross-task leaks
- Always sends advanced_settings.max_results so result volume matches the
  OpenClaw web_search default (5) instead of Parallel's default (10)
- Identifies the plugin via User-Agent header built from package version
- Runtime accepts the generic `query` arg as a fallback so the operator
  CLI (openclaw capability web.search) keeps working when Parallel is the
  active provider: it is promoted into the lone `search_queries` entry.
  `objective` stays optional and is never synthesized from a keyword
  query (Parallel documents it as natural-language intent). Agent callers
  using the native objective+search_queries shape take precedence; the
  schema still advertises only the native keys
- Updates the agent tool-display extractor (src/agents/tool-display-common.ts)
  to recognize Parallel's objective+search_queries shape so calls render with
  query context in CLI progress and Codex activity metadata
- Adds /tools/parallel-search docs page, web.md provider listing, docs nav,
  labeler entry, per-plugin registration contract test, and minimal core
  touch-points (legacy migrate, registration cases, providers contract list,
  runtime bundled list, vitest extension paths)
2026-06-05 12:01:58 -07:00
Jason (Json)
36d9241cf7 docs: prefer web_fetch in weather skill (#90250)
* docs: prefer web_fetch in weather skill

* docs: use compact wttr json in weather skill
2026-06-05 14:35:55 -04:00
zenglingbiao
d896a4c7a3 fix(context-engine): forward isHeartbeat to afterTurn (fixes #89302) (#90632)
Merged via squash.

Prepared head SHA: 2f6da84c4b
Co-authored-by: zenglingbiao <290951975+zenglingbiao@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-05 11:27:37 -07:00
Gio Della-Libera
b3eba2ff38 fix(gateway): dedupe probe warnings by gateway identity (#85791)
Merged via squash.

Prepared head SHA: 13e3c00f56
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl_microsoft <115749436+giodl_microsoft@users.noreply.github.com>
Reviewed-by: @giodl_microsoft
2026-06-05 10:23:12 -07:00
Ted Li
21aa297434 fix(cron): auto-migrate legacy cron store (#90208)
Merged via squash.

Prepared head SHA: f5aa1b6759
Co-authored-by: MonkeyLeeT <6754057+MonkeyLeeT@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-05 10:22:02 -07:00
Shakker
4752e9a67d test: bracket provider worker env 2026-06-05 17:09:55 +01:00
Shakker
ec91dce0b8 test: scope internal transcript state env 2026-06-05 17:08:30 +01:00
Shakker
fbbb88925a test: scope openrouter capability env 2026-06-05 17:06:09 +01:00
Shakker
9235c25d33 test: clean model cache state env 2026-06-05 17:04:19 +01:00
Shakker
6ce71737e5 test: manage workspace state fixture 2026-06-05 17:02:10 +01:00
Shakker
935c80d6e1 test: use managed skill workshop state 2026-06-05 17:01:20 +01:00
Vincent Koc
286772e930 test: shorten vitest no-output heartbeat 2026-06-05 09:00:02 -07:00
Shakker
b19904931e test: contain sessions tool state env 2026-06-05 16:58:37 +01:00
Shakker
415272d17e test: isolate pdf media state env 2026-06-05 16:56:05 +01:00
Shakker
002aa1061b test: narrow media tool state env 2026-06-05 16:54:58 +01:00
Shakker
8a83c13389 test: bound sandbox media state env 2026-06-05 16:53:44 +01:00
Shakker
a16b6c02ce test: pair cron task state env 2026-06-05 16:52:47 +01:00
Peter Steinberger
2514980118 feat(matrix): handle voice preflight and threads (#90415)
* feat(matrix): handle voice preflight and threads

Co-authored-by: Frank Dierolf <frank_dierolf@web.de>
Co-authored-by: marc.wilson <marcwilson@gazasrv15i5.globaladvisors.biz>

* test(matrix): satisfy ci guards

* fix(matrix): preserve thread relations on edits

* chore: annotate deprecated compatibility aliases

* fix(matrix): include poll thread roots in reads

* test(matrix): enable audio preflight qa config

* test(matrix): make voice preflight QA mention deterministic

---------

Co-authored-by: Frank Dierolf <frank_dierolf@web.de>
Co-authored-by: marc.wilson <marcwilson@gazasrv15i5.globaladvisors.biz>
2026-06-05 08:49:35 -07:00
Shakker
c85b0ee3db test: scope subagent sqlite state env 2026-06-05 16:48:32 +01:00
Shakker
1e683ff245 test: scope auth path state env 2026-06-05 16:46:55 +01:00
Shakker
fc0b141445 test: contain launch restart home env 2026-06-05 16:45:08 +01:00
Shakker
a0840cad8f test: scope restart sentinel state env 2026-06-05 16:43:32 +01:00
Shakker
03b35b53e3 test: delegate media redirect state env 2026-06-05 16:43:00 +01:00
Peter Steinberger
797bcd5bdb fix: propagate ClickClack toolsAllow through replies
Propagate ClickClack account-level runtime tool allowlists through inbound reply dispatch so restricted ClickClack accounts keep their tool policy when model/agent replies are generated.

This threads `toolsAllow` through shared dispatch, provider wrappers, embedded agent execution, and ACP hook events. ACP-bound sessions now fail closed for restrictive runtime allowlists because ACPX cannot enforce per-turn tool allowlists on reused persistent sessions.

Verification:
- Live ClickClack E2E on Crabbox AWS `run_6a0472ed7e71`, provider `aws`, id `cbx_dace25addcaa`.
- `node scripts/run-vitest.mjs run src/auto-reply/reply/dispatch-acp.test.ts src/plugin-sdk/acp-runtime.test.ts src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts src/auto-reply/dispatch.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/provider-dispatcher.test.ts extensions/clickclack/src/inbound.test.ts --reporter=verbose`
- Crabbox changed gate `run_d32af37fb265`, provider `aws`, id `cbx_8236876017c9`: `corepack pnpm check:changed`
- Autoreview clean: `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`

Supersedes #89500.

Co-authored-by: Michael Appel <mappel@nvidia.com>
2026-06-05 08:40:35 -07:00
Shakker
5a0f9cb03c test: scope logging config path env 2026-06-05 16:39:50 +01:00
Shakker
e4de53a460 test: snapshot flows state env 2026-06-05 16:38:23 +01:00
Shakker
d1fe0184b9 test: preserve secrets state env snapshot 2026-06-05 16:37:09 +01:00
Vincent Koc
da88940c6c fix(android): skip gradle resource tasks on linux arm 2026-06-05 08:14:42 -07:00
Ayaan Zaidi
520992a1de test(gateway): avoid future session fixture timestamps 2026-06-05 18:19:19 +05:30
Ayaan Zaidi
00d21a4720 test(telegram): align transcript append mock 2026-06-05 18:19:19 +05:30
Ayaan Zaidi
3d68f7e5f7 test(gateway): stabilize live session metadata fixture 2026-06-05 18:19:19 +05:30
Ayaan Zaidi
ceee4c6b01 fix(sessions): mark transcript rewrites in registry 2026-06-05 18:19:19 +05:30
Fermin Quant
e22e857ddd fix(sessions): keep transcript append result discriminant 2026-06-05 18:19:19 +05:30
Fermin Quant
57bed6ae0c fix(sessions): cover terminal transcript markers 2026-06-05 18:19:19 +05:30
Fermin Quant
0c9ac48d2c fix(sessions): reconcile stale terminal main transcripts 2026-06-05 18:19:19 +05:30
Ayaan Zaidi
afa04d6454 fix(gateway): share codex model visibility 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
85343ea546 fix(gateway): fail closed for unknown model auth 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
d6dbcb2f4b fix(android): surface expiring providers in palette 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
61d121f1ca fix(android): show unavailable model rows as attention 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
21512a696f fix(gateway): preserve codex alias model availability 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
ea1ef72394 fix(gateway): keep unresolved profile refs unknown 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
7c885528ba fix(gateway): recognize env profile refs in model availability 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
cec5e36a39 fix(gateway): avoid resolving auth during models list 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
e404ce98f5 fix(gateway): require resolved auth for model availability 2026-06-05 17:14:34 +05:30
Ayaan Zaidi
30160933f0 refactor(android): distill provider availability cleanup 2026-06-05 17:14:34 +05:30
Tosko4
8b66003a0b fix(android): clarify provider attention state 2026-06-05 17:14:34 +05:30
Chunyue Wang
12a569109b fix(agents): detect unsigned thinking-only stall when reasoning payload inflates payloadCount (#89874)
Summary:
- Merged fix(agents): detect unsigned thinking-only stall when reasoning payload inflates payloadCount after ClawSweeper review.

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

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

Prepared head SHA: c613c3884f
Review: https://github.com/openclaw/openclaw/pull/89874#issuecomment-4630564594

Co-authored-by: openperf <16864032@qq.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-06-05 10:29:18 +00:00
Chunyue Wang
1a3ce7c2a8 fix(qqbot): sanitize outbound text to strip reasoning/thinking content (#90132)
Summary:
- Adds QQBot outbound `sanitizeText` wired to `sanitizeAssistantVisibleText` plus a regression test for stripping `<thinking>` and `<think>` blocks.
- PR surface: Source +2, Tests +19. Total +21 across 2 files.
- Reproducibility: yes. source-reproducible: current main QQBot outbound lacks `sanitizeText`, and shared deli ... nnel text sanitization when that hook exists. I did not run a live Tencent QQBot plus MiniMax reproduction.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(qqbot): add curly braces for eslint(curly) compliance

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

Prepared head SHA: 17cf140183
Review: https://github.com/openclaw/openclaw/pull/90132#issuecomment-4618527026

Co-authored-by: openperf <16864032@qq.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-06-05 06:57:16 +00:00
ooiuuii
560b77a4af test: add Codex session route migration coverage (#90319)
* Add Codex session route migration coverage

* Use synthetic Telegram session id in Codex test
2026-06-04 23:28:08 -07:00
ooiuuii
cfd5f1ad13 Add Codex multi-agent migration coverage (#90317) 2026-06-04 23:27:34 -07:00
Kevin Lin
d7759c6a35 feat(googlechat): add native approval cards
## Summary

- Adds native Google Chat approval cards for exec and plugin approval requests that originate from Google Chat spaces or threads.
- Uses opaque server-side action tokens for Google Chat `cardsV2` button callbacks and updates delivered approval messages after resolution or expiry.
- Preserves the shipped Google Chat typing-message default while keeping approval cards on the channel-local native path.
- Suppresses duplicate manual `/approve ...` follow-up delivery inside `extensions/googlechat/` when the native card path owns the approval prompt.
- Documents Google Chat native approval behavior and the `typingIndicator: "message"` default.

## Linked context

Which issue does this close?

Closes #

Which issues, PRs, or discussions are related?

Related Spec 24.8: Google Chat native approval cards.

Was this requested by a maintainer or owner?

Requested by maintainer in the Codex task thread.

## Real behavior proof (required for external PRs)

- Behavior addressed: Google Chat exec and plugin approvals render as native cards and resolve through Google Chat button clicks. The latest change verifies an exec approval card is not accompanied by a duplicate manual `/approve` instruction bubble.
- Real environment tested: OpenClaw dev profile with a real Google Chat DM to the OpenClaw app, local gateway behind a temporary Cloudflare quick tunnel, and Arc/Computer Use against the signed-in Google Chat session.
- Exact steps or command run after this patch: Rebuilt the gateway runtime, started the dev-profile gateway with the Google Chat webhook routed through the tunnel, sent a fresh exec request from Google Chat, verified only the native approval card appeared, clicked `Allow Once` in Google Chat, and checked the command output reply plus marker file.
- Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): Latest proof used nonce `GCHAT_NODOUBLE_LIVE_20260604070730`, approval id `949bc08c-9e57-47c0-b045-137603782292`, and proof directory `.mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race/`. `raw/google-chat-gchat-nodouble-request-card-only-clean.png` shows the fresh user message followed by a single native `Exec Approval Required` card with `Allow Once`/`Deny` and no manual `/approve` follow-up bubble. `raw/google-chat-gchat-nodouble-resolved-clean.png` shows the card edited to `Exec Approval: Allowed once` and the final successful command reply. `raw/gchat-nodouble-live-filtered-log.txt` contains `googlechat approval resolved id=949bc08c-9e57-47c0-b045-137603782292 decision=allow-once`. `raw/marker-file-check.txt` records `/tmp/openclaw-gchat-no-double-GCHAT_NODOUBLE_LIVE_20260604070730` as created.
- Observed result after fix: The approval prompt posted as a native Google Chat card only. No duplicate manual approval-instruction bubble was sent. Clicking `Allow Once` resolved the approval through the gateway and OpenClaw replied with the successful exec output in the same Google Chat DM.
- What was not tested: A persistent production Google Chat app URL; live proof used a temporary Cloudflare tunnel for the local dev callback.
- Proof limitations or environment constraints: Video was not captured for the final resumed manual UI run; still screenshots, gateway/proxy logs, a marker-file artifact, and Showboat verification were captured.
- Before evidence (optional but encouraged): Before the final channel-local suppression path, Google Chat could show both the native approval card and a separate manual `/approve` instruction bubble.

## Tests and validation

Which commands did you run?

- `node scripts/build-all.mjs gatewayWatch`
- `node scripts/run-vitest.mjs extensions/googlechat/src/monitor-webhook.test.ts extensions/googlechat/src/monitor.test.ts extensions/googlechat/src/monitor.reply-delivery.test.ts extensions/googlechat/src/monitor-durable.test.ts extensions/googlechat/src/approval-card-actions.test.ts extensions/googlechat/src/approval-handler.runtime.test.ts extensions/googlechat/src/approval-native.test.ts extensions/googlechat/src/approval-card-click.test.ts extensions/googlechat/src/channel-config.test.ts extensions/googlechat/src/targets.test.ts`
- `git diff --check`
- `pnpm docs:list`
- `uvx showboat --workdir .mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race verify .mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race/raw/showboat-summary.md`
- Live dev-profile Google Chat proof described above.

What regression coverage was added or updated?

- Added Google Chat native approval capability, runtime delivery, card token, and card-click resolver tests.
- Added in-flight native card send suppression coverage so manual follow-up text is suppressed while native card delivery is pending.
- Added cleanup coverage so manual follow-ups are restored if native card send fails.
- Updated webhook ACK coverage for card-click events and default typing-indicator behavior coverage.

What failed before this fix, if known?

Google Chat could deliver the native approval card and still allow a model/message-tool manual `/approve` follow-up to appear as a second visible bubble.

If no test was added, why not?

Tests were added for the changed runtime and webhook behavior.

## Risk checklist

Did user-visible behavior change? (`Yes/No`)

Yes.

Did config, environment, or migration behavior change? (`Yes/No`)

No migration. The shipped Google Chat `typingIndicator: "message"` default is preserved.

Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)

Yes.

What is the highest-risk area?

Approval authorization and callback token handling for native Google Chat card actions.

How is that risk mitigated?

Callbacks carry opaque action tokens only, token bindings check account, space, message, expiry, allowed decision, and in-flight state, and actor authorization reuses the existing Google Chat approver allowlist adapter based on stable `users/<id>` principals.

## Current review state

What is the next action?

Merge after current-head CI for `5923f2af46`.

What is still waiting on author, maintainer, CI, or external proof?

Current-head CI is green for `5923f2af46`; live dev-profile proof is complete.

Which bot or reviewer comments were addressed?

Addressed duplicate approval delivery by keeping the final suppression path inside `extensions/googlechat/`, preserving default typing-message behavior, and proving the current Google Chat surface sends only the native approval card.
2026-06-04 23:05:06 -07:00
Vincent Koc
e0018382eb fix(agents): reject empty completion handoffs 2026-06-04 21:33:42 -07:00
clawsweeper[bot]
69d1d78649 fix(mattermost): anchor slash state on globalThis (#68113) (#90534)
Summary:
- The branch stores Mattermost slash-command account state in a process-wide Symbol.for/globalThis Map and adds module-reload regression coverage.
- PR surface: Source +21, Tests +43. Total +64 across 2 files.
- Reproducibility: yes. at source level: current main's route handler returns 503 when its module-local accoun ... pulate state through a separate loader path. I did not run a live Mattermost POST in this read-only review.

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

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

Prepared head SHA: 3cf28a1f96
Review: https://github.com/openclaw/openclaw/pull/90534#issuecomment-4627897262

Co-authored-by: ben.li <ly85206559@163.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-06-05 04:10:43 +00:00
Peter Steinberger
cb5bb9b936 docs: document e2e helpers 2026-06-05 00:04:03 -04:00
Peter Steinberger
bafe17e60b docs: document vitest routing maps 2026-06-04 23:59:11 -04:00
Peter Steinberger
613a2835cb docs: document scoped script helpers 2026-06-04 23:57:22 -04:00
Peter Steinberger
a59eba3ee1 docs: document test project scripts 2026-06-04 23:55:54 -04:00
Peter Steinberger
9b1a01e4f9 docs: document test wrapper scripts 2026-06-04 23:54:19 -04:00
Peter Steinberger
29746cf7a9 docs: document smoke test scripts 2026-06-04 23:53:10 -04:00
joshavant
17ab517047 fix(ios): use dynamic settings bottom margin 2026-06-04 22:52:52 -05:00
joshavant
697eeb8bab fix(ios): keep diagnostics action reachable 2026-06-04 22:52:52 -05:00
joshavant
853f1c0d9e fix(ios): keep gateway row grouped and tappable 2026-06-04 22:52:52 -05:00
joshavant
1447a4507a fix(ios): keep talk unavailable without config 2026-06-04 22:52:52 -05:00
joshavant
748881e0a8 fix(ios): label chat attachment button 2026-06-04 22:52:52 -05:00
Peter Steinberger
ff83d4d164 docs: document runner scripts 2026-06-04 23:52:06 -04:00
Vincent Koc
13078d24ab chore(release): refresh plugin sdk api baseline 2026-06-04 20:50:17 -07:00
Vincent Koc
48c19590eb fix(test): install playwright deps after host validation failure 2026-06-04 20:50:17 -07:00
Peter Steinberger
72547a1ac6 docs: document release audit scripts 2026-06-04 23:49:34 -04:00
Peter Steinberger
26bc069308 docs: document profiling scripts 2026-06-04 23:48:20 -04:00
Peter Steinberger
57f8d71c50 docs: document release runner scripts 2026-06-04 23:46:55 -04:00
Peter Steinberger
980c91d293 docs: document ci dependency docs scripts 2026-06-04 23:42:32 -04:00
Peter Steinberger
6b0ffa2106 docs: document package boundary scripts 2026-06-04 23:37:42 -04:00
Peter Steinberger
056421f4f8 docs: document root runtime guard scripts 2026-06-04 23:34:16 -04:00
Dallin Romney
fb750e6eed Fix main CI guard drift (#90532) 2026-06-04 20:31:41 -07:00
Peter Steinberger
978fdd7d2a docs: document root guard scripts 2026-06-04 23:30:59 -04:00
Peter Steinberger
74f3baebb7 docs: document root build check scripts 2026-06-04 23:28:04 -04:00
Peter Steinberger
deff9ea180 docs: document cjs bridge headers 2026-06-04 23:26:24 -04:00
Peter Steinberger
9fd5f9ee7c docs: document source bridge files 2026-06-04 23:25:42 -04:00
Vincent Koc
4dd7bc6d88 fix(test): stage live docker home credentials 2026-06-04 20:22:35 -07:00
Onur Solmaz
0dbf17471b feat(memory): support qmd query rerank toggle
Add memory.qmd.rerank as an opt-out for QMD query reranking when searchMode is query.

When set to false, direct QMD query calls pass --no-rerank and the mcporter unified query tool receives rerank:false. Search and vsearch modes keep their existing behavior.

Refs #61834.
2026-06-05 11:18:57 +08:00
Peter Steinberger
f3abe61b78 docs: document script lib test helpers 2026-06-04 23:08:26 -04:00
Peter Steinberger
92cdcae500 docs: document script lib report helpers 2026-06-04 23:07:12 -04:00
Peter Steinberger
3cf1bd22f9 docs: document script lib runtime package helpers 2026-06-04 23:05:22 -04:00
Peter Steinberger
44cd0ec13f docs: document script lib plugin helpers 2026-06-04 23:03:25 -04:00
Peter Steinberger
d77bac8911 docs: document script lib package helpers 2026-06-04 23:01:02 -04:00
Peter Steinberger
1da49dcfd0 docs: document script lib process helpers 2026-06-04 22:59:33 -04:00
Peter Steinberger
ee74fff7ad docs: document script lib inventory helpers 2026-06-04 22:57:30 -04:00
Peter Steinberger
1de46bb425 docs: document script lib extension helpers 2026-06-04 22:56:09 -04:00
Peter Steinberger
e662435067 docs: document script lib guard helpers 2026-06-04 22:54:18 -04:00
Peter Steinberger
62a6fd8139 docs: document script lib scan helpers 2026-06-04 22:52:34 -04:00
Peter Steinberger
88158525a7 docs: document script lib helper contracts 2026-06-04 22:51:08 -04:00
Peter Steinberger
c8bb7330b5 docs: add headers to build check scripts 2026-06-04 22:49:21 -04:00
Peter Steinberger
8732ef2f28 docs: document channel sdk core contracts 2026-06-04 22:46:51 -04:00
Peter Steinberger
8f6e71087b docs: document agent harness sdk contracts 2026-06-04 22:45:30 -04:00
Peter Steinberger
9448f91e6f docs: document memory runtime contracts 2026-06-04 22:44:01 -04:00
Peter Steinberger
5613a0fb6e docs: document discord sdk facade contracts 2026-06-04 22:42:23 -04:00
Peter Steinberger
82710b4f1f docs: document lmstudio runtime contracts 2026-06-04 22:41:26 -04:00
Peter Steinberger
d23558e691 docs: document qa runtime facade contracts 2026-06-04 22:40:12 -04:00
Peter Steinberger
2f00fbf28e docs: document tts runtime contracts 2026-06-04 22:39:02 -04:00
Peter Steinberger
86872e0880 docs: document channel approval ingress contracts 2026-06-04 22:38:03 -04:00
Peter Steinberger
506c2ee181 docs: document qa video gateway sdk contracts 2026-06-04 22:34:53 -04:00
Peter Steinberger
1e6fb5089b docs: document approval reaction reply contracts 2026-06-04 22:32:37 -04:00
Peter Steinberger
14690904f0 docs: document browser session oauth sdk contracts 2026-06-04 22:31:20 -04:00
Peter Steinberger
99bb94589b docs: document sdk facade loader contracts 2026-06-04 22:29:06 -04:00
Peter Steinberger
de4571da4b docs: document sdk dedupe and group contracts 2026-06-04 22:27:50 -04:00
Peter Steinberger
a4087c54b5 docs: document provider facade constants 2026-06-04 22:26:17 -04:00
Peter Steinberger
4756d6a42a docs: document sdk migration and approval contracts 2026-06-04 22:24:26 -04:00
Peter Steinberger
9e22b8560c docs: document sdk facade contracts 2026-06-04 22:22:21 -04:00
Peter Steinberger
c1b49bb1d0 docs: document sdk payload and fetch contracts 2026-06-04 22:20:19 -04:00
Peter Steinberger
d6c0f9ccb8 docs: document sdk utility contracts 2026-06-04 22:18:33 -04:00
Peter Steinberger
5d350e785a docs: document sdk single-export contracts 2026-06-04 22:16:29 -04:00
Peter Steinberger
de68623ffe docs: document sdk runtime helper contracts 2026-06-04 22:14:54 -04:00
Peter Steinberger
848f39e70d docs: document public sdk contract helpers 2026-06-04 22:13:13 -04:00
Peter Steinberger
b311fd607f docs: document generated locale bundles 2026-06-04 22:11:11 -04:00
2448 changed files with 160528 additions and 24050 deletions

8
.github/labeler.yml vendored
View File

@@ -293,6 +293,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/lobster/**"
"extensions: llama-cpp":
- changed-files:
- any-glob-to-any-file:
- "extensions/llama-cpp/**"
"extensions: memory-core":
- changed-files:
- any-glob-to-any-file:
@@ -574,6 +578,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/openshell/**"
"extensions: parallel":
- changed-files:
- any-glob-to-any-file:
- "extensions/parallel/**"
"extensions: perplexity":
- changed-files:
- any-glob-to-any-file:

View File

@@ -2,19 +2,14 @@
What problem does this PR solve?
Why does this matter now?
What is the intended outcome?
What is intentionally out of scope?
What does success look like?
What should reviewers focus on?
<details>
@@ -75,13 +70,10 @@ Be mindful of private information like IP addresses, API keys, phone numbers, no
Which commands did you run?
What regression coverage was added or updated?
What failed before this fix, if known?
If no test was added, why not?
<details>
@@ -95,16 +87,12 @@ List focused commands, not every incidental check. CI is useful support, but ext
Did user-visible behavior change? (`Yes/No`)
Did config, environment, or migration behavior change? (`Yes/No`)
Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)
What is the highest-risk area?
How is that risk mitigated?
<details>
@@ -118,10 +106,8 @@ Use this for author judgment that is not obvious from the diff. ClawSweeper can
What is the next action?
What is still waiting on author, maintainer, CI, or external proof?
Which bot or reviewer comments were addressed?
<details>

View File

@@ -722,7 +722,7 @@ jobs:
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
start_check "gateway-watch" \
node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000
node scripts/check-gateway-watch-regression.mjs --skip-build
fi
for index in "${!pids[@]}"; do
@@ -2093,7 +2093,7 @@ jobs:
uses: actions/cache@v5
with:
path: ~/.android-sdk
key: ${{ runner.os }}-android-sdk-v1-cmdline-12266719-platform-36-build-tools-36.0.0
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-37.0-build-tools-36.0.0
restore-keys: |
${{ runner.os }}-android-sdk-v1-
@@ -2101,7 +2101,7 @@ jobs:
run: |
set -euo pipefail
ANDROID_SDK_ROOT="$HOME/.android-sdk"
CMDLINE_TOOLS_VERSION="12266719"
CMDLINE_TOOLS_VERSION="14742923"
ARCHIVE="commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
URL="https://dl.google.com/android/repository/${ARCHIVE}"
@@ -2123,7 +2123,7 @@ jobs:
yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null
sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \
"platform-tools" \
"platforms;android-36" \
"platforms;android-37.0" \
"build-tools;36.0.0"
- name: Run Android ${{ matrix.task }}

View File

@@ -35,7 +35,7 @@ jobs:
java-version: "21"
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: java-kotlin
build-mode: manual
@@ -46,6 +46,6 @@ jobs:
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-security/android"

View File

@@ -342,13 +342,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-core-auth-secrets-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/core-auth-secrets"
@@ -365,13 +365,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-config-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/config-boundary"
@@ -388,13 +388,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/gateway-runtime-boundary"
@@ -411,13 +411,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-channel-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/channel-runtime-boundary"
@@ -460,7 +460,7 @@ jobs:
- name: Initialize CodeQL
if: ${{ github.event_name != 'pull_request' }}
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
@@ -468,7 +468,7 @@ jobs:
- name: Analyze
id: analyze
if: ${{ github.event_name != 'pull_request' }}
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
output: sarif-results
category: "/codeql-critical-quality/network-runtime-boundary"
@@ -518,13 +518,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/agent-runtime-boundary"
@@ -541,13 +541,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-mcp-process-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/mcp-process-runtime-boundary"
@@ -564,13 +564,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-memory-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/memory-runtime-boundary"
@@ -587,13 +587,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-session-diagnostics-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/session-diagnostics-boundary"
@@ -610,13 +610,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-reply-runtime-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/plugin-sdk-reply-runtime"
@@ -633,13 +633,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-provider-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/provider-runtime-boundary"
@@ -655,13 +655,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-ui-control-plane-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/ui-control-plane"
@@ -677,13 +677,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-web-media-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/web-media-runtime-boundary"
@@ -700,13 +700,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/plugin-boundary"
@@ -723,12 +723,12 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-package-contract-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-critical-quality/plugin-sdk-package-contract"

View File

@@ -35,7 +35,7 @@ jobs:
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: swift
build-mode: manual
@@ -46,7 +46,7 @@ jobs:
- name: Analyze
id: analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
output: sarif-results
upload: failure-only
@@ -83,7 +83,7 @@ jobs:
done
- name: Upload filtered SARIF
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
sarif_file: sarif-results-filtered
category: "/codeql-critical-security/macos"

View File

@@ -101,12 +101,12 @@ jobs:
.github/codeql
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: ${{ matrix.language }}
config-file: ${{ matrix.config_file }}
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/codeql-security-high/${{ matrix.category }}"

View File

@@ -88,11 +88,30 @@ jobs:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Pre-pull BuildKit image
shell: bash
env:
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
run: |
set -euo pipefail
for attempt in 1 2 3 4; do
if docker pull "${BUILDKIT_IMAGE}"; then
exit 0
fi
if [[ "${attempt}" == "4" ]]; then
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
exit 1
fi
sleep_seconds=$((attempt * 10))
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
sleep "${sleep_seconds}"
done
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
@@ -161,7 +180,7 @@ jobs:
- name: Build and push amd64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64
@@ -179,7 +198,7 @@ jobs:
id: build-browser
if: steps.tags.outputs.browser != ''
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64
@@ -279,11 +298,30 @@ jobs:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Pre-pull BuildKit image
shell: bash
env:
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
run: |
set -euo pipefail
for attempt in 1 2 3 4; do
if docker pull "${BUILDKIT_IMAGE}"; then
exit 0
fi
if [[ "${attempt}" == "4" ]]; then
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
exit 1
fi
sleep_seconds=$((attempt * 10))
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
sleep "${sleep_seconds}"
done
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
@@ -352,7 +390,7 @@ jobs:
- name: Build and push arm64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/arm64
@@ -370,7 +408,7 @@ jobs:
id: build-browser
if: steps.tags.outputs.browser != ''
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/arm64
@@ -468,7 +506,7 @@ jobs:
fetch-depth: 0
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
@@ -561,11 +599,30 @@ jobs:
with:
fetch-depth: 1
- name: Pre-pull BuildKit image
shell: bash
env:
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
run: |
set -euo pipefail
for attempt in 1 2 3 4; do
if docker pull "${BUILDKIT_IMAGE}"; then
exit 0
fi
if [[ "${attempt}" == "4" ]]; then
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
exit 1
fi
sleep_seconds=$((attempt * 10))
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
sleep "${sleep_seconds}"
done
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}

View File

@@ -149,7 +149,7 @@ jobs:
- name: Run Codex docs agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
env:
DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }}
DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }}

View File

@@ -1139,7 +1139,16 @@ jobs:
summary:
name: Verify full validation
needs: [resolve_target, docker_runtime_assets_preflight, normal_ci, plugin_prerelease, release_checks, npm_telegram, performance]
needs:
[
resolve_target,
docker_runtime_assets_preflight,
normal_ci,
plugin_prerelease,
release_checks,
npm_telegram,
performance,
]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5

View File

@@ -112,7 +112,7 @@ jobs:
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
@@ -223,7 +223,7 @@ jobs:
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -245,7 +245,7 @@ jobs:
- name: Set up Blacksmith Docker Builder
if: steps.existing.outputs.exists != 'true'
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
@@ -311,7 +311,7 @@ jobs:
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -417,7 +417,7 @@ jobs:
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -429,7 +429,7 @@ jobs:
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
@@ -503,7 +503,7 @@ jobs:
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -542,7 +542,7 @@ jobs:
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000

View File

@@ -29,14 +29,14 @@ jobs:
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000

View File

@@ -13,7 +13,7 @@ on:
default: true
type: boolean
public_release_branch:
description: Public branch that contains the release tag commit, usually main or release/YYYY.M.D
description: Public branch that contains the release tag commit, usually main or release/YYYY.M.PATCH
required: false
default: main
type: string
@@ -73,7 +73,7 @@ jobs:
run: |
set -euo pipefail
if [[ "${PUBLIC_RELEASE_BRANCH}" != "main" && ! "${PUBLIC_RELEASE_BRANCH}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
echo "public_release_branch must be main or release/YYYY.M.D, got ${PUBLIC_RELEASE_BRANCH}." >&2
echo "public_release_branch must be main or release/YYYY.M.PATCH, got ${PUBLIC_RELEASE_BRANCH}." >&2
exit 1
fi
RELEASE_SHA=$(git rev-parse HEAD)

View File

@@ -37,7 +37,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);

View File

@@ -56,7 +56,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@@ -91,7 +91,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const defaultBaseline = "0bf06e953fdda290799fc9fb9244a8f67fdae593";
@@ -581,7 +581,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;

View File

@@ -56,7 +56,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@@ -91,7 +91,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const defaultBaseline = "synthetic-reverted-thread-filepath-fix";
@@ -603,7 +603,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;

View File

@@ -81,7 +81,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@@ -180,7 +180,7 @@ jobs:
run: pnpm build
- name: Cache Mantis candidate pnpm store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.local/share/pnpm/store

View File

@@ -79,7 +79,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
if (context.eventName === "pull_request_target") {
@@ -125,7 +125,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const eventName = context.eventName;
@@ -445,7 +445,7 @@ jobs:
sudo chown -R codex:codex "$GITHUB_WORKSPACE"
- name: Run Codex Mantis Telegram agent
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
env:
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
BASELINE_SHA: ${{ needs.validate_refs.outputs.baseline_revision }}
@@ -709,7 +709,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;

View File

@@ -68,7 +68,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@@ -105,7 +105,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const eventName = context.eventName;
@@ -327,7 +327,7 @@ jobs:
run: pnpm build
- name: Cache Mantis candidate pnpm store
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.local/share/pnpm/store
@@ -573,7 +573,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;

View File

@@ -126,7 +126,7 @@ jobs:
fetch-depth: 1
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000

View File

@@ -887,7 +887,7 @@ jobs:
summary=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker chunk summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
exit 1
fi
node .release-harness/scripts/docker-e2e.mjs summary "$summary" "Docker E2E chunk: ${DOCKER_E2E_CHUNK:-unknown}" >> "$GITHUB_STEP_SUMMARY"
@@ -897,7 +897,7 @@ jobs:
with:
name: docker-e2e-${{ matrix.chunk_id }}
path: .artifacts/docker-tests/
if-no-files-found: ignore
if-no-files-found: error
plan_docker_lane_groups:
needs: validate_selected_ref
@@ -1147,7 +1147,7 @@ jobs:
summary=".artifacts/docker-tests/targeted-${{ steps.plan.outputs.artifact_suffix }}/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker targeted summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
exit 1
fi
node .release-harness/scripts/docker-e2e.mjs summary "$summary" "Docker E2E targeted lanes" >> "$GITHUB_STEP_SUMMARY"
@@ -1157,7 +1157,7 @@ jobs:
with:
name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}
path: .artifacts/docker-tests/
if-no-files-found: ignore
if-no-files-found: error
validate_docker_openwebui:
needs: [validate_selected_ref, prepare_docker_e2e_image]
@@ -1274,7 +1274,7 @@ jobs:
summary=".artifacts/docker-tests/release-openwebui/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker Open WebUI summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
exit 1
fi
node .release-harness/scripts/docker-e2e.mjs summary "$summary" "Docker E2E chunk: openwebui" >> "$GITHUB_STEP_SUMMARY"
@@ -1284,7 +1284,7 @@ jobs:
with:
name: docker-e2e-openwebui
path: .artifacts/docker-tests/
if-no-files-found: ignore
if-no-files-found: error
prepare_docker_e2e_image:
needs: validate_selected_ref
@@ -1497,37 +1497,72 @@ jobs:
- name: Setup Docker builder
if: steps.image_exists.outputs.needs_build == '1'
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
- name: Build and push bare Docker E2E image
if: steps.plan.outputs.needs_bare_image == '1' && steps.image_exists.outputs.bare_exists != '1'
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: bare
platforms: linux/amd64
tags: ${{ steps.image.outputs.bare_image }}
sbom: true
provenance: mode=max
push: true
shell: bash
env:
IMAGE_REF: ${{ steps.image.outputs.bare_image }}
run: |
set -euo pipefail
build_cmd=(
docker buildx build
--file ./scripts/e2e/Dockerfile
--target bare
--platform linux/amd64
--tag "$IMAGE_REF"
--sbom=true
--provenance=mode=max
--push
.
)
for attempt in 1 2 3 4; do
if "${build_cmd[@]}"; then
exit 0
fi
if [[ "$attempt" == "4" ]]; then
echo "::error::Failed to build Docker E2E bare image after ${attempt} attempts"
exit 1
fi
sleep_seconds=$((attempt * 20))
echo "Docker E2E bare image build failed; retrying in ${sleep_seconds}s (${attempt}/4)."
sleep "$sleep_seconds"
done
- name: Build and push functional Docker E2E image
if: steps.plan.outputs.needs_functional_image == '1' && steps.image_exists.outputs.functional_exists != '1'
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: functional
build-contexts: |
openclaw_package=.artifacts/docker-e2e-package
platforms: linux/amd64
tags: ${{ steps.image.outputs.functional_image }}
sbom: true
provenance: mode=max
push: true
shell: bash
env:
IMAGE_REF: ${{ steps.image.outputs.functional_image }}
run: |
set -euo pipefail
build_cmd=(
docker buildx build
--file ./scripts/e2e/Dockerfile
--target functional
--build-context openclaw_package=.artifacts/docker-e2e-package
--platform linux/amd64
--tag "$IMAGE_REF"
--sbom=true
--provenance=mode=max
--push
.
)
for attempt in 1 2 3 4; do
if "${build_cmd[@]}"; then
exit 0
fi
if [[ "$attempt" == "4" ]]; then
echo "::error::Failed to build Docker E2E functional image after ${attempt} attempts"
exit 1
fi
sleep_seconds=$((attempt * 20))
echo "Docker E2E functional image build failed; retrying in ${sleep_seconds}s (${attempt}/4)."
sleep "$sleep_seconds"
done
prepare_live_test_image:
needs: validate_selected_ref
@@ -1558,8 +1593,11 @@ jobs:
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
live_image="ghcr.io/${repository}-live-test:${SELECTED_SHA}"
live_image_extensions="matrix,acpx"
live_image_tag_suffix="${live_image_extensions//,/-}"
live_image="ghcr.io/${repository}-live-test:${SELECTED_SHA}-${live_image_tag_suffix}"
echo "live_image=${live_image}" >> "$GITHUB_OUTPUT"
echo "live_image_extensions=${live_image_extensions}" >> "$GITHUB_OUTPUT"
echo "Shared live-test image: \`${live_image}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Log in to GHCR
@@ -1582,7 +1620,7 @@ jobs:
- name: Setup Docker builder
if: steps.image_exists.outputs.exists != '1'
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
@@ -1594,7 +1632,7 @@ jobs:
file: ./Dockerfile
target: build
build-args: |
OPENCLAW_EXTENSIONS=matrix
OPENCLAW_EXTENSIONS=${{ steps.image.outputs.live_image_extensions }}
platforms: linux/amd64
tags: ${{ steps.image.outputs.live_image }}
sbom: true
@@ -1918,7 +1956,7 @@ jobs:
profiles: stable full
- suite_id: native-live-src-gateway-core
label: Native live gateway core
command: node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core
command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core
timeout_minutes: 60
profile_env_only: false
profiles: beta minimum stable full
@@ -2038,7 +2076,7 @@ jobs:
profiles: full
- suite_id: native-live-src-gateway-backends
label: Native live gateway backends
command: node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-backends
command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-backends
timeout_minutes: 60
profile_env_only: false
profiles: stable full

View File

@@ -391,7 +391,7 @@ jobs:
tideclaw_alpha_publish=true
fi
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ "${tideclaw_alpha_publish}" != "true" ]]; then
echo "Real publish runs must be dispatched from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
echo "Real publish runs must be dispatched from main, release/YYYY.M.PATCH, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
exit 1
fi

View File

@@ -244,8 +244,8 @@ jobs:
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "OPENAI_API_KEY is not configured; live GPT 5.5 lane will be skipped." >> "$GITHUB_STEP_SUMMARY"
exit 0
echo "OPENAI_API_KEY is not configured; live GPT 5.5 lane cannot run without live evidence." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
kova setup --ci --json
kova setup --non-interactive --auth env-only --provider openai --env-var OPENAI_API_KEY --json
@@ -262,11 +262,6 @@ jobs:
set -euo pipefail
mkdir -p "$REPORT_DIR" "$BUNDLE_DIR" "$SUMMARY_DIR"
if [[ "$MATRIX_LIVE" == "true" && -z "${OPENAI_API_KEY:-}" ]]; then
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
repeat="$REQUESTED_REPEAT"
if [[ "$MATRIX_REPEAT" != "input" ]]; then
repeat="$MATRIX_REPEAT"
@@ -309,24 +304,7 @@ jobs:
report_md="${report_json%.json}.md"
effective_status="$status"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$status" != "0" ]]; then
if REPORT_JSON="$report_json" node <<'NODE'
const fs = require("node:fs");
const report = JSON.parse(fs.readFileSync(process.env.REPORT_JSON, "utf8"));
const statuses = report.summary?.statuses ?? {};
const nonPassStatuses = Object.entries(statuses)
.filter(([status, count]) => status !== "PASS" && Number(count) > 0);
const baselineRegressionCount =
Number(report.baseline?.comparison?.regressionCount ?? report.gate?.baseline?.regressionCount ?? 0);
const gate = report.gate;
const toleratedPartial =
gate?.verdict === "PARTIAL" &&
Number(gate.blockingCount ?? 0) === 0 &&
baselineRegressionCount === 0 &&
nonPassStatuses.length === 0;
if (!toleratedPartial) {
process.exit(1);
}
NODE
if node "$PERFORMANCE_HELPER_DIR/scripts/lib/kova-report-gate.mjs" "$report_json"
then
effective_status=0
{
@@ -377,6 +355,28 @@ jobs:
exit "$effective_status"
fi
- name: Validate Kova evidence
if: ${{ always() && steps.lane.outputs.run == 'true' }}
shell: bash
run: |
set -euo pipefail
missing=0
if ! find "$REPORT_DIR" -maxdepth 1 -type f -name '*.json' -size +0c -print -quit | grep -q .; then
echo "::error::Kova JSON report is missing for ${LANE_ID}."
missing=1
fi
if [[ ! -s "$BUNDLE_DIR/bundle.json" ]]; then
echo "::error::Kova bundle evidence is missing for ${LANE_ID}."
missing=1
fi
if [[ ! -s "$SUMMARY_DIR/${LANE_ID}.md" ]]; then
echo "::error::Kova summary evidence is missing for ${LANE_ID}."
missing=1
fi
if [[ "$missing" != "0" ]]; then
exit 1
fi
- name: Fetch previous source performance baseline
if: ${{ steps.lane.outputs.run == 'true' && matrix.lane == 'mock-provider' && steps.clawgrit.outputs.present == 'true' }}
env:
@@ -547,7 +547,7 @@ jobs:
.artifacts/kova/bundles/${{ matrix.lane }}
.artifacts/kova/summaries/${{ matrix.lane }}.md
.artifacts/openclaw-performance/source/${{ matrix.lane }}
if-no-files-found: ignore
if-no-files-found: error
retention-days: ${{ matrix.deep_profile == 'true' && 14 || 30 }}
- name: Prepare clawgrit reports checkout

View File

@@ -132,7 +132,7 @@ jobs:
fi
fi
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then
echo "Release checks must be dispatched from main, release/YYYY.M.D, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
echo "Release checks must be dispatched from main, release/YYYY.M.PATCH, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
exit 1
fi
@@ -346,6 +346,7 @@ jobs:
discord_selected=false
whatsapp_selected=false
slack_selected=false
disabled_required_lanes=()
IFS=', ' read -r -a filter_tokens <<< "$filter"
for token in "${filter_tokens[@]}"; do
@@ -361,6 +362,9 @@ jobs:
discord_selected="$qa_live_discord_ci_enabled"
whatsapp_selected="$qa_live_whatsapp_ci_enabled"
slack_selected="$qa_live_slack_ci_enabled"
[[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord")
[[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp")
[[ "$qa_live_slack_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-slack")
;;
qa-live-non-slack|qa-non-slack|non-slack|no-slack|without-slack)
qa_filter_seen=true
@@ -368,6 +372,8 @@ jobs:
telegram_selected=true
discord_selected="$qa_live_discord_ci_enabled"
whatsapp_selected="$qa_live_whatsapp_ci_enabled"
[[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord")
[[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp")
;;
qa-live-matrix|qa-matrix|matrix)
qa_filter_seen=true
@@ -380,18 +386,27 @@ jobs:
qa-live-discord|qa-discord|discord)
qa_filter_seen=true
discord_selected="$qa_live_discord_ci_enabled"
[[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord")
;;
qa-live-whatsapp|qa-whatsapp|whatsapp)
qa_filter_seen=true
whatsapp_selected="$qa_live_whatsapp_ci_enabled"
[[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp")
;;
qa-live-slack|qa-slack|slack)
qa_filter_seen=true
slack_selected="$qa_live_slack_ci_enabled"
[[ "$qa_live_slack_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-slack")
;;
esac
done
if [[ "${#disabled_required_lanes[@]}" -gt 0 ]]; then
echo "live_suite_filter explicitly requested disabled QA live lane(s): ${disabled_required_lanes[*]}" >&2
echo "Enable the matching OPENCLAW_RELEASE_QA_*_LIVE_CI_ENABLED repo variable or remove the lane from live_suite_filter." >&2
exit 1
fi
if [[ "$qa_filter_seen" == "true" ]]; then
qa_live_matrix_enabled="$matrix_selected"
qa_live_telegram_enabled="$telegram_selected"
@@ -801,6 +816,7 @@ jobs:
run: node scripts/build-all.mjs qaRuntime
- name: Run parity lane
id: run_lane
env:
QA_PARITY_LANE: ${{ matrix.lane }}
QA_PARITY_OUTPUT_DIR: ${{ matrix.output_dir }}
@@ -831,6 +847,7 @@ jobs:
--output-dir ".artifacts/qa-e2e/${QA_PARITY_OUTPUT_DIR}"
- name: Upload parity lane artifacts
id: upload_parity_lane_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -839,6 +856,52 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_lab_parity_lane_release_checks
RELEASE_CHECK_VARIANT: ${{ matrix.lane }}
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_parity_lane_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}-${RELEASE_CHECK_VARIANT}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'variant=%s\n' "$RELEASE_CHECK_VARIANT"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_parity_lane_release_checks-${{ matrix.lane }}.env
retention-days: 14
if-no-files-found: error
qa_lab_parity_report_release_checks:
name: Run QA Lab parity report
needs: [resolve_target, qa_lab_parity_lane_release_checks]
@@ -879,6 +942,7 @@ jobs:
run: node scripts/build-all.mjs qaRuntime
- name: Generate parity report
id: generate_report
run: |
pnpm openclaw qa parity-report \
--repo-root . \
@@ -889,6 +953,7 @@ jobs:
--output-dir .artifacts/qa-e2e/parity
- name: Upload parity artifacts
id: upload_parity_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -897,6 +962,50 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_lab_parity_report_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.generate_report.outcome }} ${{ steps.upload_parity_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-parity-report-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_parity_report_release_checks.env
retention-days: 14
if-no-files-found: error
qa_lab_runtime_parity_release_checks:
name: Run QA Lab runtime parity lane
needs: [resolve_target]
@@ -950,6 +1059,7 @@ jobs:
--output-dir ".artifacts/qa-e2e/runtime-parity"
- name: Run standard runtime parity tier
id: runtime_parity_standard_lane
if: ${{ always() && steps.runtime_parity_lane.outcome != 'skipped' && steps.runtime_parity_lane.outcome != 'cancelled' }}
run: |
set -euo pipefail
@@ -977,6 +1087,7 @@ jobs:
--output-dir ".artifacts/qa-e2e/runtime-parity-soak"
- name: Generate runtime parity report
id: generate_runtime_parity_report
if: always()
run: |
set -euo pipefail
@@ -987,6 +1098,7 @@ jobs:
--output-dir .artifacts/qa-e2e/runtime-parity-report
- name: Generate standard runtime parity report
id: generate_runtime_parity_standard_report
if: always()
run: |
set -euo pipefail
@@ -997,6 +1109,7 @@ jobs:
--output-dir .artifacts/qa-e2e/runtime-parity-standard-report
- name: Generate soak runtime parity report
id: generate_runtime_parity_soak_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
@@ -1012,6 +1125,7 @@ jobs:
--output-dir .artifacts/qa-e2e/runtime-parity-soak-report
- name: Upload runtime parity artifacts
id: upload_runtime_parity_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1020,6 +1134,50 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_lab_runtime_parity_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.runtime_parity_lane.outcome }} ${{ steps.runtime_parity_standard_lane.outcome }} ${{ steps.runtime_parity_soak_lane.outcome }} ${{ steps.generate_runtime_parity_report.outcome }} ${{ steps.generate_runtime_parity_standard_report.outcome }} ${{ steps.generate_runtime_parity_soak_report.outcome }} ${{ steps.upload_runtime_parity_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_runtime_parity_release_checks.env
retention-days: 14
if-no-files-found: error
runtime_tool_coverage_release_checks:
name: Enforce QA Lab runtime tool coverage
needs: [resolve_target, qa_lab_runtime_parity_release_checks]
@@ -1141,6 +1299,7 @@ jobs:
done
- name: Upload Matrix QA artifacts
id: upload_matrix_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1149,6 +1308,50 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_matrix_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_matrix_qa_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_matrix_release_checks.env
retention-days: 14
if-no-files-found: error
qa_live_telegram_release_checks:
name: Run QA Lab live Telegram lane
needs: [resolve_target]
@@ -1237,6 +1440,7 @@ jobs:
done
- name: Upload Telegram QA artifacts
id: upload_telegram_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1245,10 +1449,54 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_telegram_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_telegram_qa_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_telegram_release_checks.env
retention-days: 14
if-no-files-found: error
qa_live_discord_release_checks:
name: Run QA Lab live Discord lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_discord_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_DISCORD_LIVE_CI_ENABLED == 'true'
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_discord_enabled == 'true'
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 60
@@ -1332,6 +1580,7 @@ jobs:
done
- name: Upload Discord QA artifacts
id: upload_discord_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1340,10 +1589,54 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_discord_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_discord_qa_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_discord_release_checks.env
retention-days: 14
if-no-files-found: error
qa_live_whatsapp_release_checks:
name: Run QA Lab live WhatsApp lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_whatsapp_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED == 'true'
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_whatsapp_enabled == 'true'
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 60
@@ -1430,6 +1723,7 @@ jobs:
done
- name: Upload WhatsApp QA artifacts
id: upload_whatsapp_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1438,10 +1732,54 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_whatsapp_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_whatsapp_qa_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_whatsapp_release_checks.env
retention-days: 14
if-no-files-found: error
qa_live_slack_release_checks:
name: Run QA Lab live Slack lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_SLACK_LIVE_CI_ENABLED == 'true'
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true'
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 60
@@ -1525,6 +1863,7 @@ jobs:
done
- name: Upload Slack QA artifacts
id: upload_slack_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
with:
@@ -1533,6 +1872,50 @@ jobs:
retention-days: 14
if-no-files-found: warn
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_slack_release_checks
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_slack_qa_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
mark_status() {
case "$1" in
failure) status="failure" ;;
cancelled)
if [[ "$status" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
}
mark_status "${JOB_STATUS:-}"
for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do
mark_status "$outcome"
done
mkdir -p .artifacts/release-check-status
status_path=".artifacts/release-check-status/${RELEASE_CHECK_JOB}.env"
{
printf 'job=%s\n' "$RELEASE_CHECK_JOB"
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "${JOB_STATUS:-}"
printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}"
} > "$status_path"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
with:
name: release-check-status-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_slack_release_checks.env
retention-days: 14
if-no-files-found: error
summary:
name: Verify release checks
needs:
@@ -1553,9 +1936,19 @@ jobs:
- qa_live_slack_release_checks
if: always()
runs-on: ubuntu-24.04
permissions: {}
permissions:
actions: read
timeout-minutes: 5
steps:
- name: Download advisory status artifacts
if: always()
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: release-check-status-*
path: .artifacts/release-check-status
merge-multiple: true
- name: Verify release check results
shell: bash
env:
@@ -1567,6 +1960,49 @@ jobs:
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha=true
fi
release_check_result() {
local name="$1"
local fallback="$2"
local status_dir=".artifacts/release-check-status"
local saw=0
local saw_failure=0
local saw_cancelled=0
if [[ -d "$status_dir" ]]; then
while IFS= read -r -d '' file; do
saw=1
status="$(sed -n 's/^status=//p' "$file" | tail -n 1)"
case "$status" in
success|skipped) ;;
cancelled) saw_cancelled=1 ;;
failure|"") saw_failure=1 ;;
*) saw_failure=1 ;;
esac
done < <(find "$status_dir" -type f -name "${name}*.env" -print0)
fi
if [[ "$saw_failure" == "1" ]]; then
printf 'failure\n'
elif [[ "$saw_cancelled" == "1" ]]; then
printf 'cancelled\n'
elif [[ "$fallback" != "success" && "$fallback" != "skipped" ]]; then
printf '%s\n' "$fallback"
elif [[ "$saw" == "1" ]]; then
printf 'success\n'
elif [[ "$fallback" == "success" ]]; then
printf 'failure\n'
else
printf '%s\n' "$fallback"
fi
}
advisory_status_override_allowed() {
case "$1" in
qa_lab_parity_lane_release_checks|qa_lab_parity_report_release_checks|qa_lab_runtime_parity_release_checks|qa_live_matrix_release_checks|qa_live_telegram_release_checks|qa_live_discord_release_checks|qa_live_whatsapp_release_checks|qa_live_slack_release_checks)
return 0
;;
*)
return 1
;;
esac
}
for item in \
"prepare_release_package=${{ needs.prepare_release_package.result }}" \
"install_smoke_release_checks=${{ needs.install_smoke_release_checks.result }}" \
@@ -1585,7 +2021,12 @@ jobs:
"qa_live_slack_release_checks=${{ needs.qa_live_slack_release_checks.result }}"
do
name="${item%%=*}"
result="${item#*=}"
raw_result="${item#*=}"
if advisory_status_override_allowed "$name"; then
result="$(release_check_result "$name" "$raw_result")"
else
result="$raw_result"
fi
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
if [[ "$tideclaw_alpha" == "true" ]]; then
case "$name" in
@@ -1596,10 +2037,6 @@ jobs:
;;
esac
fi
if [[ "$name" == qa_* ]]; then
echo "::warning::${name} ended with ${result}; QA release-check lanes are advisory and do not block release validation."
continue
fi
echo "::error::${name} ended with ${result}"
failed=1
fi

View File

@@ -120,7 +120,7 @@ jobs:
tideclaw_alpha_publish=true
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ && "${tideclaw_alpha_publish}" != "true" ]]; then
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases." >&2
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.PATCH, or a Tideclaw alpha branch for alpha prereleases." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PLUGIN_PUBLISH_SCOPE}" != "all-publishable" ]]; then

View File

@@ -53,7 +53,7 @@ jobs:
scripts/run-opengrep.sh --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v4.36.2
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:

View File

@@ -84,7 +84,7 @@ jobs:
scripts/run-opengrep.sh --changed --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v4.36.2
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:

View File

@@ -24,6 +24,11 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
dry_run:
description: Validate the full ClawHub artifact handoff without publishing.
required: false
default: false
type: boolean
concurrency:
group: plugin-clawhub-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@@ -35,7 +40,7 @@ env:
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "facf20ceb6cc459e2872d941e71335a784bbc55c"
CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"
jobs:
preview_plugins_clawhub:
@@ -56,12 +61,6 @@ jobs:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
- name: Resolve checked-out ref
id: ref
env:
@@ -107,6 +106,12 @@ jobs:
echo "Plugin ClawHub publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
- name: Validate publishable plugin metadata
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
@@ -326,15 +331,12 @@ jobs:
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
pack_plugins_clawhub_artifacts:
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:
fail-fast: false
max-parallel: 32
@@ -407,73 +409,7 @@ jobs:
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Write ClawHub token config
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
if [[ -z "${CLAWHUB_TOKEN}" ]]; then
echo "No CLAWHUB_TOKEN secret configured; publish will rely on GitHub OIDC trusted publishing."
exit 0
fi
node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
import { join } from "node:path";
const path = join(process.env.RUNNER_TEMP, "clawhub-config.json");
writeFileSync(
path,
`${JSON.stringify(
{
registry: process.env.CLAWHUB_REGISTRY,
token: process.env.CLAWHUB_TOKEN,
},
null,
2,
)}\n`,
);
console.log(path);
EOF
echo "CLAWHUB_CONFIG_PATH=${RUNNER_TEMP}/clawhub-config.json" >> "$GITHUB_ENV"
- name: Check ClawHub package version
id: clawhub_package_version
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
status=""
for attempt in $(seq 1 8); do
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
if [[ "${status}" == "404" || "${status}" =~ ^2 ]]; then
break
fi
if [[ "${status}" == "429" || "${status}" =~ ^5 ]]; then
echo "ClawHub availability check returned ${status} for ${PACKAGE_NAME}@${PACKAGE_VERSION}; retrying (${attempt}/8)."
sleep 60
continue
fi
break
done
if [[ "${status}" =~ ^2 ]]; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
echo "already_published=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${status}" != "404" ]]; then
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
exit 1
fi
echo "already_published=false" >> "$GITHUB_OUTPUT"
- name: Publish
if: steps.clawhub_package_version.outputs.already_published != 'true'
- name: Pack ClawHub package artifact
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
@@ -481,8 +417,65 @@ jobs:
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR: ${{ runner.temp }}/clawhub-package-artifact
run: bash scripts/plugin-clawhub-publish.sh --pack "${PACKAGE_DIR}"
- name: Upload ClawHub package artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.plugin.artifactName }}
path: ${{ runner.temp }}/clawhub-package-artifact/*.tgz
if-no-files-found: error
retention-days: 7
approve_plugin_clawhub_release:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts]
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions: {}
steps:
- name: Approve ClawHub package publish
run: echo "ClawHub package publish approved."
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugin_clawhub_release]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugin_clawhub_release.result == 'success')
permissions:
actions: read
contents: read
id-token: write
strategy:
fail-fast: false
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854
with:
dry_run: ${{ inputs.dry_run }}
json: true
package_artifact_name: ${{ matrix.plugin.artifactName }}
registry: https://clawhub.ai
site: https://clawhub.ai
source_repo: ${{ github.repository }}
source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
source_ref: ${{ github.ref }}
tags: ${{ matrix.plugin.publishTag }}
secrets:
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}
verify_published_clawhub_package:
needs: [preview_plugins_clawhub, publish_plugins_clawhub]
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Verify published ClawHub package
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}

View File

@@ -65,7 +65,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
if (context.eventName === "schedule") {
@@ -159,7 +159,7 @@ jobs:
run_mock_parity:
name: Run QA Lab mock parity lane
needs: [validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 30
env:
QA_PARITY_CONCURRENCY: "1"
@@ -186,7 +186,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run OpenAI candidate lane
@@ -232,7 +232,7 @@ jobs:
name: Run live runtime token-efficiency lane
needs: [authorize_actor, validate_selected_ref]
if: github.event_name == 'schedule'
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 45
environment: qa-live-shared
env:
@@ -267,7 +267,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run live runtime parity lane
@@ -321,7 +321,7 @@ jobs:
name: Run Matrix live QA lane
needs: [authorize_actor, validate_selected_ref]
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all') }}
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
@@ -352,7 +352,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run Matrix live lane
@@ -397,7 +397,7 @@ jobs:
name: Run Matrix live QA lane (${{ matrix.profile }})
needs: [authorize_actor, validate_selected_ref]
if: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
strategy:
@@ -437,7 +437,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run Matrix live lane shard
@@ -480,7 +480,7 @@ jobs:
run_live_telegram:
name: Run Telegram live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
@@ -520,7 +520,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run Telegram live lane
@@ -575,7 +575,7 @@ jobs:
run_live_discord:
name: Run Discord live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
@@ -615,7 +615,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run Discord live lane
@@ -669,7 +669,7 @@ jobs:
run_live_whatsapp:
name: Run WhatsApp live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
concurrency:
group: qa-live-whatsapp-shared
@@ -712,7 +712,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run WhatsApp live lane
@@ -766,7 +766,7 @@ jobs:
run_live_slack:
name: Run Slack live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
@@ -806,7 +806,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
NODE_OPTIONS: --max-old-space-size=12288
run: pnpm build
- name: Run Slack live lane

View File

@@ -35,7 +35,7 @@ jobs:
submodules: false
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Build minimal sandbox base (USER sandbox)
shell: bash

View File

@@ -509,60 +509,62 @@ jobs:
let locked = 0;
let inspected = 0;
let cursor = null;
let page = 1;
while (true) {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "closed",
sort: "updated",
direction: "desc",
per_page: perPage,
page,
});
const result = await github.graphql(
`query ClosedIssuesForLocking(
$owner: String!
$repo: String!
$cursor: String
$perPage: Int!
) {
repository(owner: $owner, name: $repo) {
issues(
first: $perPage
after: $cursor
states: CLOSED
orderBy: { field: CREATED_AT, direction: ASC }
) {
nodes {
number
locked
closedAt
comments(last: 1) {
nodes {
createdAt
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}`,
{
owner,
repo,
cursor,
perPage,
},
);
const issues = result.repository.issues;
if (issues.length === 0) {
break;
}
for (const issue of issues) {
if (issue.pull_request) {
continue;
}
if (issue.locked) {
continue;
}
if (!issue.closed_at) {
for (const issue of issues.nodes) {
if (issue.locked || !issue.closedAt) {
continue;
}
inspected += 1;
const closedAtMs = Date.parse(issue.closed_at);
if (!Number.isFinite(closedAtMs)) {
continue;
}
if (closedAtMs > cutoffMs) {
const closedAtMs = Date.parse(issue.closedAt);
if (!Number.isFinite(closedAtMs) || closedAtMs > cutoffMs) {
continue;
}
let lastCommentMs = 0;
if (issue.comments > 0) {
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issue.number,
per_page: 1,
page: 1,
sort: "created",
direction: "desc",
});
if (comments.length > 0) {
lastCommentMs = Date.parse(comments[0].created_at);
}
}
const lastComment = issue.comments.nodes[0];
const lastCommentMs = lastComment ? Date.parse(lastComment.createdAt) : 0;
const lastActivityMs = Math.max(closedAtMs, lastCommentMs || 0);
if (lastActivityMs > cutoffMs) {
continue;
@@ -578,7 +580,10 @@ jobs:
locked += 1;
}
page += 1;
if (!issues.pageInfo.hasNextPage || !issues.pageInfo.endCursor) {
break;
}
cursor = issues.pageInfo.endCursor;
}
core.info(`Inspected ${inspected} closed issues; locked ${locked}.`);

View File

@@ -129,7 +129,7 @@ jobs:
- name: Run Codex test performance agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
with:
openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/test-performance-agent.md

View File

@@ -34,10 +34,25 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
sleep 5
done
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Fail on tabs in workflow files
@@ -78,10 +93,25 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
sleep 5
done
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
@@ -190,10 +220,25 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
sleep 5
done
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment

View File

@@ -2,6 +2,54 @@
Docs: https://docs.openclaw.ai
## 2026.6.5
### Highlights
- QQBot now strips model reasoning/thinking scaffolding before native delivery, preventing raw `<thinking>` content from leaking into channel replies. (#89913, #90132) Thanks @openperf.
- MCP tool results now coerce `resource_link`, `resource`, `audio`, malformed image, and future non-text/image blocks at the materialize boundary, preventing Anthropic 400s and poisoned session history after a tool returns richer MCP content. (#90710, #90728) Thanks @RanSHammer and @849261680.
- Anthropic extended-thinking sessions recover after prompt-cache expiry or Gateway restart because stream start events wait for `message_start`, letting pre-generation signature errors trigger the existing recovery retry. (#90667, #90697) Thanks @openperf.
- Parallel is now a bundled `web_search` provider with `PARALLEL_API_KEY` discovery, guarded endpoint handling, cache-safe session ids, onboarding picker support, and docs. (#85158) Thanks @NormallyGaussian.
- Google Vertex ADC users get static catalog rows and runtime model resolution again, while single-provider cooldown recovery and memory adapter status checks are more reliable. (#90506, #90609, #90717, #90816) Thanks @849261680.
- Matrix can preflight voice notes before mention gating, preserve thread reads/replies through Matrix relations pagination, and carry QA coverage for voice and thread flows. (#78016, #90415)
- Auth and plugin install state is more durable: auth profiles now live in SQLite, official npm plugin install records keep their trusted pins, and prerelease fallback integrity checks avoid carrying stale integrity forward. (#89102, #88585)
- macOS node mode no longer silently self-reconnects away from a healthy direct Gateway session, reducing unexpected companion app session churn. (#90668, #90815) Thanks @vrurg.
- Upgrade and service paths are safer: cron legacy JSON stores migrate during doctor preflight, service env placeholders no longer mask state-dir secrets, WhatsApp startup waits are bounded, and disabled WhatsApp accounts tear down on config reload. (#90072, #90208, #90277, #90488, #90486, #87951, #87965) Thanks @MonkeyLeeT, @sallyom, @mcaxtr, and @MukundaKatta.
### Changes
- Search/providers: add the Parallel bundled web-search plugin, live provider tests, registration contracts, onboarding/docs wiring, and guarded `api.parallel.ai/v1/search` support. (#85158) Thanks @NormallyGaussian.
- Matrix/channels: add voice-message preflight and thread-aware read/reply behavior, including Matrix QA scenario wiring and docs for voice-message behavior. (#78016, #90415)
- Skills/ClawHub: install ClawHub skills backed by GitHub repositories through the resolved install API, download the pinned GitHub commit, keep install-policy checks, and report install telemetry after success. (#90478) Thanks @Patrick-Erichsen.
- Google Chat/channels: add native approval card actions and click handling so Google Chat approvals use platform-native cards instead of generic message flow.
- Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, while iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, and unavailable Talk controls reachable.
- Memory: QMD search can use the new rerank toggle, and memory adapter status uses the resolved default model identity when checking plain status. (#61834)
- Docs/tooling: add Parallel search docs, refresh weather-skill guidance toward `web_fetch`, clarify legacy `openai-codex` auth, document release/test helper scripts, and tighten changed-test routing docs for CI/debugging work. (#90028, #90250) Thanks @fuller-stack-dev.
- Release/process: switch release trains to `YYYY.M.PATCH` monthly patch numbering, keep pre-transition tags compatible, and pin the June 2026 floor at `2026.6.5` after the published beta.
- Platform maintenance: refresh Android, Swift/macOS, Docker, CodeQL, Buildx, Docker build/push, and Codex Action dependencies for this release train. (#74980, #81757, #86481, #86483, #90601)
- QQBot: add `/bot-group-allways on|off` slash command (with named-account and default-account support) to toggle whether group messages require an `@mention` before the bot replies, and clear the runtime config snapshot after the write so the new account-level `defaultRequireMention` takes effect immediately without restart. (#91423) Thanks @cxyhhhhh.
### Fixes
- Channel content boundaries: QQBot now strips reasoning/thinking tags before sending, preserving final answers while hiding internal model narration from users. (#89913, #90132) Thanks @openperf.
- Agents/MCP/providers: coerce non-text/image MCP tool-result blocks before they reach provider converters, preserving valid images and turning richer MCP content into text instead of malformed image blocks. (#90710, #90728) Thanks @RanSHammer and @849261680.
- Anthropic/Codex/ACP/agent recovery: defer Anthropic stream start events until `message_start`, strip stale compaction thinking signatures before Anthropic replay, detect unsigned thinking-only stalls, refresh prompt fences after compaction writes, reject empty completion handoffs, preserve parent streaming-off overrides/shared progress commentary, forward heartbeat metadata to context-engine hooks, and cover Codex session/thread migration edge cases. (#90667, #90697, #90163, #90108, #89874, #89505, #90632, #89302, #90729, #90317, #90319) Thanks @openperf, @100yenadmin, and @ooiuuii.
- Provider/model resolution: preserve Google Vertex ADC auth markers in generated catalogs, re-probe a single-provider primary after cooldown, share Codex model visibility, fail closed for unknown model auth, preserve Codex alias availability, keep unresolved profile refs unknown, and avoid resolving auth while listing models. (#90506, #90609, #90717, #90702) Thanks @849261680.
- Gateway/macOS/mobile: avoid duplicate Gateway probe warnings by identity, rate-limit node pairing requests while preserving paired-node reconnects, keep macOS node mode on a healthy direct Gateway session, keep iOS diagnostics and gateway rows reachable, and avoid Linux ARM Gradle resource tasks during Android builds. (#85791, #90147, #90668, #90815) Thanks @giodl73-repo and @vrurg.
- TUI/chat/Workboard/auto-reply: optimistic user messages stay stable across stale history reloads, runId reassignment, and abort windows instead of disappearing, jumping, or lingering as ghost rows; Workboard stale lifecycle bulk updates no longer overwrite newer status/provenance; message-tool sends now count as delivery. (#86205, #89600, #88592, #90123) Thanks @RomneyDa.
- Cron/update/service env: doctor config preflight now migrates legacy cron JSON stores into SQLite before runtime reads, service env planning skips unresolved placeholders that would mask state-dir `.env` values, and session transcript rewrites keep registry markers/discriminants consistent. (#90072, #90208, #90277, #90488) Thanks @MonkeyLeeT and @sallyom.
- Security/config/tooling: guard MCP HTTP redirects, protect global agent config defaults, and keep release/test/tooling proof failures bounded and explicit. (#89732, #90145)
- Channels: WhatsApp restarts when per-account config changes, bounds background startup waits, closes failed sockets, and preserves reconnect behavior; Mattermost slash commands keep their state on `globalThis`; Feishu streaming cards preserve full merged content; voice-call tracks Twilio streams after connect; ClickClack reply tools respect `toolsAllow`. (#87951, #87965, #90486, #68113, #90534, #90181, #90607, #89500) Thanks @MukundaKatta, @mcaxtr, @infoanton, @mushuiyu886, and @sahibzada-allahyar.
- Feishu: retry transient send rate-limit errors (HTTP 429, per-chat code 230020, tenant-level code 11232) with linear backoff, including SDK responses that fulfill with rate-limit bodies instead of throwing, and route streaming-card sends through the retry wrapper. (#89659) Thanks @ladygege.
- Release/CI/E2E: main CI guard drift, PR merge diff scoping, live Docker credential staging, base-image qualification, installer Docker classification, Playwright dependency install recovery, API-key auth for Codex live Docker lanes, Parallels option terminators, and JSON-mode progress handling are tighter so release proof fails cleaner. (#90532, #90287, #90058) Thanks @RomneyDa, @hxy91819, and @mrunalp.
- Release/CI/E2E: Docker E2E and live Docker harness runs now apply default memory, CPU, and process ceilings while preserving explicit per-lane overrides.
- Release/CI/E2E: plugin lifecycle matrix resource sampling now fails phases that exceed RSS, wall-clock, or CPU ceilings instead of only logging the measurements.
- Release/CI/E2E: Codex npm plugin live assertions now cap transcript discovery and diagnostic log reads so failure proof stays bounded.
- Memory: keep doctor REM harness previews aligned with live REM by dropping short-term recall snippets whose source files disappeared before rendering preview output. Thanks @samzong and @frankekn.
- Tests/state isolation: QA Lab valid-tool-call metrics now require runtime tool-call evidence when runtime parity data is available instead of counting tool-backed scenario pass status alone.
- Tests/state isolation: QA Lab runtime parity now fails planned-only tool-call rows without matching tool results instead of treating matching mock plans as real tool evidence.
- Tests/state isolation: provider, media, auth, cron, task, session, sandbox, Gateway, and Codex timeout fixtures now scope more home/state/env data per test, reducing cross-test leakage and making release validation failures less noisy. (#90027, #89974)
## 2026.6.2
### Highlights

View File

@@ -48,6 +48,7 @@ These patterns are usually not vulnerabilities by themselves:
- Prompt injection without a policy, auth, approval, sandbox, or tool-boundary bypass.
- A trusted operator using an intentional local feature, such as local shell access or browser/script execution.
- A report whose only primitive is changing the process or child-process environment before running OpenClaw or an executable OpenClaw invokes.
- A malicious plugin after a trusted operator installs or enables it.
- Multiple adversarial users sharing one Gateway host/config and expecting per-user isolation.
- Scanner-only, dependency-only, or stale-path reports without a working repro and demonstrated OpenClaw impact.
@@ -103,6 +104,7 @@ These are frequently reported but are typically closed with no code change:
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
- Reports that depend on attacker-controlled environment variables changing executable behavior, including variables that redirect lookup paths, preload code, select wrappers/interpreters, alter package-manager or runtime hooks, or make one executable call another executable. Control of the process or child-process environment is trusted host/operator control in OpenClaw's model; these reports need a separate OpenClaw boundary bypass that lets untrusted input set or mutate that environment.
- Reports that depend on pre-existing symlinked skill/workspace filesystem state (for example symlink chains involving `skills/*/SKILL.md`) without showing an untrusted path that can create/control that state.
- Missing HSTS findings on default local/loopback deployments.
- Reports against test-only harnesses, QA Lab, QE Lab, E2E fixtures, benchmark rigs, or maintainer-only debugging tools when the vulnerable code is not shipped as a supported production surface.
@@ -161,6 +163,7 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Reports where exploitability depends on attacker-controlled pre-existing symlink/hardlink filesystem state in trusted local paths (for example extraction/install target trees) unless a separate untrusted boundary bypass is shown that creates that state.
- Reports whose only claim is sandbox/workspace read expansion through trusted local skill/workspace symlink state (for example `skills/*/SKILL.md` symlink chains) unless a separate untrusted boundary bypass is shown that creates/controls that state.
- Reports whose only claim is post-approval executable identity drift on a trusted host via same-path file replacement/rewrite unless a separate untrusted boundary bypass is shown for that host write primitive.
- Reports whose only claim is environment-variable-driven executable behavior change, including path lookup changes, preload hooks, wrapper/interpreter selection, package-manager/runtime hooks, or variables that make an executable invoke another executable, unless a separate OpenClaw boundary bypass lets untrusted input set or mutate that environment.
- Reports where the only demonstrated impact is an already-authorized sender intentionally invoking a local-action command (for example `/export-session` writing to an absolute host path) without bypassing auth, sandbox, or another documented boundary
- Reports whose only claim is use of an explicit trusted-operator control surface (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution) without demonstrating an auth, policy, allowlist, approval, or sandbox bypass.
- Reports where the only claim is that a trusted-installed/enabled plugin can execute with gateway/host privileges (documented trust model behavior).
@@ -181,6 +184,7 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
OpenClaw security guidance assumes:
- The host where OpenClaw runs is within a trusted OS/admin boundary.
- Anyone who can set or mutate the OpenClaw process environment, launcher environment, or child-process environment is inside that trusted host/operator boundary.
- Anyone who can modify `~/.openclaw` state/config (including `openclaw.json`) is effectively a trusted operator.
- A single Gateway shared by mutually untrusted people is **not a recommended setup**. Use separate gateways (or at minimum separate OS users/hosts) per trust boundary.
- Authenticated Gateway callers are treated as trusted operators. Session identifiers (for example `sessionKey`) are routing controls, not per-user authorization boundaries.

View File

@@ -2,6 +2,86 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.6.5</title>
<pubDate>Tue, 09 Jun 2026 19:06:49 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2606000590</sparkle:version>
<sparkle:shortVersionString>2026.6.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.6.5</h2>
<h3>Highlights</h3>
<ul>
<li>QQBot now strips model reasoning/thinking scaffolding before native delivery, preventing raw <code><thinking></code> content from leaking into channel replies. (#89913, #90132) Thanks @openperf.</li>
<li>MCP tool results now coerce <code>resource_link</code>, <code>resource</code>, <code>audio</code>, malformed image, and future non-text/image blocks at the materialize boundary, preventing Anthropic 400s and poisoned session history after a tool returns richer MCP content. (#90710, #90728) Thanks @RanSHammer and @849261680.</li>
<li>Anthropic extended-thinking sessions recover after prompt-cache expiry or Gateway restart because stream start events wait for <code>message_start</code>, letting pre-generation signature errors trigger the existing recovery retry. (#90667, #90697) Thanks @openperf.</li>
<li>Parallel is now a bundled <code>web_search</code> provider with <code>PARALLEL_API_KEY</code> discovery, guarded endpoint handling, cache-safe session ids, onboarding picker support, and docs. (#85158) Thanks @NormallyGaussian.</li>
<li>Google Vertex ADC users get static catalog rows and runtime model resolution again, while single-provider cooldown recovery and memory adapter status checks are more reliable. (#90506, #90609, #90717, #90816) Thanks @849261680.</li>
<li>Matrix can preflight voice notes before mention gating, preserve thread reads/replies through Matrix relations pagination, and carry QA coverage for voice and thread flows. (#78016, #90415)</li>
<li>Auth and plugin install state is more durable: auth profiles now live in SQLite, official npm plugin install records keep their trusted pins, and prerelease fallback integrity checks avoid carrying stale integrity forward. (#89102, #88585)</li>
<li>Agent, tool, and provider loops are stricter around MCP lease timestamps, prompt-cache tool names, local tool catalogs, unreadable dynamic tools, owner-only HTTP tools, and provider catalog metadata, reducing hidden retries and unsafe exposure. (#91124, #91233, #90022, #90261)</li>
<li>macOS node mode no longer silently self-reconnects away from a healthy direct Gateway session, reducing unexpected companion app session churn. (#90668, #90815) Thanks @vrurg.</li>
<li>Upgrade and service paths are safer: cron legacy JSON stores migrate during doctor preflight, service env placeholders no longer mask state-dir secrets, WhatsApp startup waits are bounded, and disabled WhatsApp accounts tear down on config reload. (#90072, #90208, #90277, #90488, #90486, #87951, #87965) Thanks @MonkeyLeeT, @sallyom, @mcaxtr, and @MukundaKatta.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Search/providers: add the Parallel bundled web-search plugin, live provider tests, registration contracts, onboarding/docs wiring, and guarded <code>api.parallel.ai/v1/search</code> support. (#85158) Thanks @NormallyGaussian.</li>
<li>Matrix/channels: add voice-message preflight and thread-aware read/reply behavior, including Matrix QA scenario wiring and docs for voice-message behavior. (#78016, #90415)</li>
<li>Skills/ClawHub: install ClawHub skills backed by GitHub repositories through the resolved install API, download the pinned GitHub commit, keep install-policy checks, and report install telemetry after success. (#90478) Thanks @Patrick-Erichsen.</li>
<li>Skills/ClawHub: avoid one filesystem watcher per skill file during refresh, keeping large skill trees from exhausting watcher limits.</li>
<li>Google Chat/channels: add native approval card actions and click handling so Google Chat approvals use platform-native cards instead of generic message flow.</li>
<li>Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, Android adds theme mode selection, and iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, fallback copy, and unavailable Talk controls reachable. (#90752, #91201)</li>
<li>Memory: QMD search can use the new rerank toggle, and memory adapter status uses the resolved default model identity when checking plain status. (#61834)</li>
<li>Docs/tooling: add Parallel search docs, refresh weather-skill guidance toward <code>web_fetch</code>, clarify legacy <code>openai-codex</code> auth, document release/test helper scripts, and tighten changed-test routing docs for CI/debugging work. (#90028, #90250) Thanks @fuller-stack-dev.</li>
<li>Release/process: switch release trains to <code>YYYY.M.PATCH</code> monthly patch numbering, keep pre-transition tags compatible, and pin the June 2026 floor at <code>2026.6.5</code>.</li>
<li>Release/process: defer the session-metadata SQLite migration from the <code>2026.6.5</code> beta train so this release keeps the existing JSON-backed session metadata path while the migration risk is worked on <code>main</code>.</li>
<li>Release metadata: align OpenClaw, publishable plugin manifests, generated shrinkwraps, app version metadata, iOS release notes, Matrix plugin changelog, and generated release baselines with the <code>2026.6.5</code> release train.</li>
<li>Platform maintenance: refresh Android, Swift/macOS, Docker, CodeQL, Buildx, Docker build/push, and Codex Action dependencies for this release train. (#74980, #81757, #86481, #86483, #90601)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Channel content boundaries: QQBot now strips reasoning/thinking tags before sending, preserving final answers while hiding internal model narration from users. (#89913, #90132) Thanks @openperf.</li>
<li>Agents/MCP/providers: coerce non-text/image MCP tool-result blocks before they reach provider converters, preserving valid images and turning richer MCP content into text instead of malformed image blocks. (#90710, #90728) Thanks @RanSHammer and @849261680.</li>
<li>Anthropic/Codex/ACP/agent recovery: defer Anthropic stream start events until <code>message_start</code>, strip stale compaction thinking signatures before Anthropic replay, detect unsigned thinking-only stalls, refresh prompt fences after compaction writes, reject empty completion handoffs, preserve parent streaming-off overrides/shared progress commentary, forward heartbeat metadata to context-engine hooks, and cover Codex session/thread migration edge cases. (#90667, #90697, #90163, #90108, #89874, #89505, #90632, #89302, #90729, #90317, #90319) Thanks @openperf, @100yenadmin, and @ooiuuii.</li>
<li>Agents/Codex/tools: MCP lease release no longer refreshes <code>lastUsedAt</code>, prompt-cache tool names are guarded, lean local tool catalogs stay compact, unreadable dynamic tools are quarantined, orphan tool errors still surface, native subagent completion results survive app-server monitoring, and background-session name derivation avoids regex backtracking risk. (#91124, #90612, #90022, #91235, #91233)</li>
<li>Provider/model resolution: preserve Google Vertex ADC auth markers in generated catalogs, re-probe a single-provider primary after cooldown, share Codex model visibility, fail closed for unknown model auth, preserve Codex alias availability, keep unresolved profile refs unknown, and avoid resolving auth while listing models. (#90506, #90609, #90717, #90702) Thanks @849261680.</li>
<li>Provider/model resolution: live provider model catalogs keep helper coverage, Ollama catalog metadata is preserved, Google provider prefixes are stripped from Gemini paths, Foundry Responses reasoning replay ids survive, MiniMax M3 thinking stays enabled, Vertex multi-region calls use the right regional host, and OpenRouter streamed generation cost is reconciled. (#91125)</li>
<li>Gateway/macOS/mobile: avoid duplicate Gateway probe warnings by identity, rate-limit node pairing requests while preserving paired-node reconnects, keep macOS node mode on a healthy direct Gateway session, keep iOS diagnostics and gateway rows reachable, and avoid Linux ARM Gradle resource tasks during Android builds. (#85791, #90147, #90668, #90815) Thanks @giodl73-repo and @vrurg.</li>
<li>Gateway/security/config: owner-only HTTP tools are gated, sandbox skills remain readable in writable sandboxes, legacy agent registry and Codex model metadata migrate safely, and stalled MCP response bodies time out instead of tying up Gateway workers. (#90261)</li>
<li>Gateway/config: <code>config.patch</code> now preserves explicit array replacement semantics for arrays without merge keys, so replacement patches do not accidentally merge stale entries. (#91551)</li>
<li>SDK: event pump failures now surface to clients instead of being swallowed behind a quiet iterator shutdown.</li>
<li>Agents/transcripts: inline image payload redaction now catches data URLs and repaired transcript images before they can leak raw image bytes into stored or exported transcripts. (#91529)</li>
<li>Plugins/Gateway: legacy flat Control UI descriptors from shipped JavaScript plugins now normalize <code>name</code> and missing surface fields into session descriptors, restoring Kitchen Sink RPC descriptor proof for package-backed plugin validation.</li>
<li>TUI/chat/Workboard/auto-reply: optimistic user messages stay stable across stale history reloads, runId reassignment, and abort windows instead of disappearing, jumping, or lingering as ghost rows; Workboard stale lifecycle bulk updates no longer overwrite newer status/provenance; message-tool sends now count as delivery. (#86205, #89600, #88592, #90123) Thanks @RomneyDa.</li>
<li>Cron/update/service env: doctor config preflight now migrates legacy cron JSON stores into SQLite before runtime reads, isolated agent turn payload messages preserve timeout context, service env planning skips unresolved placeholders that would mask state-dir <code>.env</code> values, and session transcript rewrites keep registry markers/discriminants consistent. (#90072, #90208, #91230, #90277, #90488) Thanks @MonkeyLeeT and @sallyom.</li>
<li>State/storage: Matrix sync and crypto sidecars, memory-wiki import/source-sync state, sandbox registry state, ACPX process state, device-pair notify state, Zalo hosted media, and plugin SDK dedupe state now use SQLite-owned storage instead of ad hoc runtime files. (#91100, #91108, #91056)</li>
<li>Security/config/tooling: guard MCP HTTP redirects, protect global agent config defaults, and keep release/test/tooling proof failures bounded and explicit. (#89732, #90145)</li>
<li>Channels: WhatsApp restarts when per-account config changes, bounds background startup waits, closes failed sockets, and preserves reconnect behavior; Mattermost slash commands keep their state on <code>globalThis</code> and default replies stay inside existing Mattermost threads instead of starting new ones; Feishu streaming cards preserve full merged content; iMessage private-API failures and send timeouts explain themselves while split-send coalescing honors balloon metadata; voice-call tracks Twilio streams after connect; ClickClack reply tools respect <code>toolsAllow</code>; Discord runtime adapters stay resolvable; and outbound delivery retries survive budget deferrals. (#87951, #87965, #90486, #68113, #90534, #90181, #90607, #89500, #91041, #90858, #91119, #91241) Thanks @MukundaKatta, @mcaxtr, @infoanton, @mushuiyu886, @sahibzada-allahyar, and @jacobtomlinson.</li>
<li>Feishu: retry transient send rate-limit errors (HTTP 429, per-chat code 230020, tenant-level code 11232) with linear backoff, including SDK responses that fulfill with rate-limit bodies instead of throwing, and route streaming-card sends through the retry wrapper. (#89659) Thanks @ladygege.</li>
<li>WhatsApp: captured replies after restart now route through the successor controller instead of the stale pre-restart controller. (#85823)</li>
<li>Release/CI/E2E: main CI guard drift, PR merge diff scoping, live Docker credential staging, base-image qualification, installer Docker classification, Playwright dependency install recovery, API-key auth for Codex live Docker lanes, Parallels option terminators, and JSON-mode progress handling are tighter so release proof fails cleaner. (#90532, #90287, #90058) Thanks @RomneyDa, @hxy91819, and @mrunalp.</li>
<li>Release/CI/E2E: installed-package root dist verification now allows the current package's JavaScript file count while keeping dependency, per-file-size, and scan-bound checks active.</li>
<li>Release/CI/E2E: Chutes OAuth model-discovery proof now accepts standard <code>Headers</code> requests, and QR package install smoke caps Docker CPU requests to the hosted runner capacity so beta validation fails on real package regressions.</li>
<li>Release/CI/E2E: Docker E2E and live Docker harness runs now apply default memory, CPU, and process ceilings while preserving explicit per-lane overrides.</li>
<li>Release/CI/E2E: Docker E2E CPU limits now cap to the runner capacity, keeping package Telegram acceptance on hosted 8-vCPU runners focused on package regressions instead of impossible Docker resource requests.</li>
<li>Release/CI/E2E: task maintenance release checks now reset pinned config around isolated temp state dirs, keeping normal CI focused on the active session-store fixture instead of stale process snapshots.</li>
<li>Release/CI/E2E: plugin lifecycle matrix resource sampling now fails phases that exceed RSS, wall-clock, or CPU ceilings instead of only logging the measurements.</li>
<li>Release/CI/E2E: Codex npm plugin live assertions now cap transcript discovery and diagnostic log reads so failure proof stays bounded.</li>
<li>Release/CI/E2E: browser snapshot, release-scenario, release-user-journey, Telegram desktop/RTT/package, web-search, Parallels update, plugin update, doctor switch, and upgrade-survivor diagnostics now stream or bound log/artifact reads so failed proof stays inspectable without unbounded output.</li>
<li>Release/CI/E2E: Parallels smoke validation now runs without requiring <code>pnpm</code> on the host, supports already-started Windows/Linux guests without snapshots, reports empty snapshot metadata clearly, and finds portable user-local Node on Windows.</li>
<li>Release/CI/E2E: ClawHub publish jobs prepare dependencies after checking out the target ref, and Docker store seed package discovery now targets the intended production packages. (#91547)</li>
<li>Release/CI/E2E: QA Lab capability-flip release validation now marks intentional <code>tools.deny</code> restores as array replacements, so beta validation fails only on real capability regressions.</li>
<li>Tests/state isolation: QA Lab valid-tool-call metrics now require runtime tool-call evidence when runtime parity data is available instead of counting tool-backed scenario pass status alone.</li>
<li>Tests/state isolation: QA Lab runtime parity now fails planned-only tool-call rows without matching tool results instead of treating matching mock plans as real tool evidence.</li>
<li>Tests/state isolation: QA Lab runtime parity now treats matching controlled tool errors as equivalent and falls back to transcript tool results when mock debug rows miss async image-generation starts.</li>
<li>Tests/state isolation: QA suites now fail closed on skipped summaries, missing runtime tool proof, planned-only rows, loose release limits, missing live/provider artifacts, failed agent reply markers, and package Telegram summary failures.</li>
<li>Tests/state isolation: provider, media, auth, cron, task, session, sandbox, Gateway, and Codex timeout fixtures now scope more home/state/env data per test, reducing cross-test leakage and making release validation failures less noisy. (#90027, #89974)</li>
<li>Sessions: the beta SQLite downgrade rescue now skips extra pre-reads for active non-empty JSON session stores, preserving cache race detection while still restoring missing or empty beta session files.</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.6.5/OpenClaw-2026.6.5.zip" length="55725877" type="application/octet-stream" sparkle:edSignature="EKr7gCfpEVStis9HSADJk1CWYbmH2MHMqSgNfZvLbBFCBWmk3pjBJS6K2qkxkq5lIbTj4H+Lo7Iri6ip/xTGDA=="/>
</item>
<item>
<title>2026.6.1</title>
<pubDate>Wed, 03 Jun 2026 21:26:22 +0000</pubDate>
@@ -193,52 +273,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.28/OpenClaw-2026.5.28.zip" length="54750142" type="application/octet-stream" sparkle:edSignature="U4O55uMdPU+OqSx9QR1ApUJ8wg65wxTydzD7iyCn1GHtm1MBK9noEeiA/yoUKkqb/bx0hzi1gNhn+ye19RXnCA=="/>
</item>
<item>
<title>2026.5.27</title>
<pubDate>Thu, 28 May 2026 12:12:19 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052790</sparkle:version>
<sparkle:shortVersionString>2026.5.27</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.27</h2>
<h3>Highlights</h3>
<ul>
<li>Stronger security and content boundaries: group prompt text is kept out of the system prompt, repeated-dot hostnames are normalized, side-effecting command wrappers and unsafe Node runtime env overrides are blocked, no-auth Tailscale exposure is rejected, and node/device-role approvals now require admin authority. (#87144, #87305, #87292, #87308, #87146) Thanks @eleqtrizit and @pgondhi987.</li>
<li>More reliable Codex app-server runs: Codex runtime models resolve first, workspace memory is routed through tools, shared app-server clients survive startup and spawned-helper failures, native hook relay generations survive restarts and rotate on fresh fallbacks, and false runtime live switches are avoided. (#87383, #87403, #87375, #72574, #87428) Thanks @yetval.</li>
<li>Faster Gateway and reply paths: session reads, plugin metadata fingerprints, auth env snapshots, auto-enabled plugin config, tool-search catalogs, and stable metadata caches do less hot-path rediscovery while visible replies no longer inherit hidden cleanup timeouts. (#86439, #87044) Thanks @keshavbotagent.</li>
<li>Better provider and model coverage: OpenAI-compatible embedding providers are core, DeepInfra catalog browsing loads the full credential-aware model set, Pixverse adds video generation and API region selection, VLLM thinking params are wired, Claude CLI OAuth overlays load for PI auth profiles, and bare direct Anthropic model ids work. (#85269, #84549, #87167) Thanks @dutifulbob, @ats3v, and @joshavant.</li>
<li>Channel delivery is steadier: Telegram <code>sendMessage</code> actions use durable outbound delivery, iMessage suppresses duplicate native exec approval prompts and sends, Slack keeps delivered final replies during late cleanup, Matrix mention previews/finals are stricter, QQBot fallback approval buttons honor slash-command auth, Discord guild requester checks are tighter, recovered Discord tool-warning artifacts stay out of successful replies, and Google Chat stops thread sends in DMs. (#87261, #87154) Thanks @mbelinky and @eleqtrizit.</li>
<li>Release, package, and CI proof paths are harder to wedge: npm/package inventory honors dist exclusions, shrinkwrap override pins merge correctly, Docker runtime workspace templates are packaged and smoked, release postpublish checks are stricter, beta smoke rejects empty runs, and E2E log/probe waits are bounded.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Memory: add a core OpenAI-compatible embedding provider for local and hosted OpenAI-style endpoints, with config, doctor, and docs support. (#85269) Thanks @dutifulbob.</li>
<li>Plugin SDK: mark memory-specific embedding provider registration as deprecated compatibility and surface non-bundled usage in plugin compatibility diagnostics. (#85072) Thanks @mbelinky.</li>
<li>Providers: add the Pixverse video generation provider, API region selection, docs, and external plugin packaging support.</li>
<li>DeepInfra: load the full model catalog when users browse models during onboarding, preserve configured API-key catalogs, refresh media/video defaults, and keep pricing/default model metadata aligned. (#84549) Thanks @ats3v.</li>
<li>Plugin SDK: expose plugin approval action metadata and stop exporting Vitest test helpers from the public SDK surface. (#87120) Thanks @RomneyDa.</li>
<li>Channel SDK: move channel message compatibility into core, remove old channel turn runtime aliases, and preserve runtime catalog markdown metadata for plugins.</li>
<li>ClawHub: add plugin display metadata so catalog/package listings use cleaner names. (#87354) Thanks @thewilloftheshadow.</li>
<li>Agents: split the heartbeat runtime template out of docs assets and add compatibility repair for legacy heartbeat template content. (#85416) Thanks @hxy91819.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security/content boundaries: route untrusted group prompt metadata outside system prompts, normalize repeated trailing hostname dots, block side-effecting command wrappers, reject unsafe Node runtime env overrides, reject no-auth Tailscale exposure, block untrusted Microsoft Teams service URLs, enforce <code>/allowlist configWrites</code> origin policy, gate QQBot fallback approval buttons, and require admin for node/device-role approvals. (#87144, #87305, #87292, #87308, #87146, #87154, #87334) Thanks @eleqtrizit and @pgondhi987.</li>
<li>Codex: resolve Codex runtime models before generic routing, route workspace memory through tools, preserve shared app-server clients after startup and spawned-helper failures, preserve native hook relay generations across restarts and fresh fallbacks, keep raw reasoning/source-reply guards intact, report quarantined dynamic tools, keep the attempt watchdog armed for queued terminal turns, and route Codex OAuth compaction through OpenAI-Codex. (#87383, #87403, #87375, #72574, #87428) Thanks @yetval.</li>
<li>Agents/runtime: avoid session event queue self-waits, bound compaction wake and steering retries, preserve grace for pending error diagnostics, avoid false Codex runtime live switches, avoid stale restart continuation reuse, preserve session fallback errors, suppress duplicate Claude CLI skill prompts, keep runtime context before active user turns, strip stale Anthropic thinking, quarantine unsupported tool schemas, recover completed write timeouts safely, release retained session write locks on timeout abort, and validate forced plugin harness support before pinning. (#86123, #55424, #86855, #74341, #87278) Thanks @luoyanglang, @cathrynlavery, and @openperf.</li>
<li>Reply/session delivery: keep visible turn admission unbounded, keep visible fallback delivery on latest targets, preserve bridge hook context, classify direct fallback targets by channel grammar, report approval resolutions in bridge mode, and avoid stale source-reply artifacts. (#87044) Thanks @keshavbotagent.</li>
<li>Channels: make Telegram <code>sendMessage</code> action replies durable and preserve SecretRef prompt config, suppress duplicate iMessage native exec approval prompts and sends, keep iMessage approval polling alive after denied reactions, keep Slack delivered final replies during late cleanup, keep Matrix mention previews/finals mention-inert and normally delivered, ignore filename-embedded Matrix IDs, suppress recovered Discord tool-warning artifacts from successful replies, suppress Google Chat thread sends in DMs, and harden Discord guild requester checks. (#87261, #87452) Thanks @mbelinky.</li>
<li>Memory: salvage QMD search JSON after nonzero exits and keep workspace memory routing through the Codex tool path where possible. (#87225, #87383, #87403) Thanks @osolmaz.</li>
<li>Providers/models: forward cached token usage in OpenAI-compatible chat completions, load Claude CLI OAuth overlays for PI auth profiles, send bare direct Anthropic model ids, wire configured VLLM thinking params, honor OpenAI-compatible cache retention, normalize OpenAI Responses replay tool ids, resolve OpenAI <code>gpt-5.5</code> without a cached catalog, preserve <code>retry-after</code> fallback handling, bound GitHub Copilot auth requests, and load DeepInfra custom/live catalogs consistently. (#82062, #87167, #84549) Thanks @caz0075, @joshavant, and @ats3v.</li>
<li>Gateway/performance: borrow read-only session metadata and active session working stores, cache current/stable plugin metadata fingerprints, cache auto-enabled plugin config, slim metadata identity caches, trust current metadata lifecycle caches, stabilize isolated cron prompt-cache affinity, persist model auth profile suffixes, drain probe client closes, expire browser tokens after auth rotation, and keep default status fast paths bounded. Thanks @ferminquant.</li>
<li>CLI/help/config: reject loose or malformed numeric options for gateway timeouts, model limits, directory limits, message options, webhooks, and partial values; respect subcommand version options; route generated/root/plugin help targets correctly; keep skills JSON output flushing naturally; and keep plugin descriptor loading quiet in root help. (#87398) Thanks @Patrick-Erichsen.</li>
<li>Plugin state/tool search: evict the current namespace when plugin rows hit caps, reuse unchanged tool-search catalogs, align the release catalog reuse wrapper, and keep fallback tool warnings mention-inert.</li>
<li>Install/package/release: match npm globstar exclusions, honor dist package exclusions in inventory, omit unpacked test helpers, skip Homebrew until macOS packages need it, package Docker runtime workspace templates, smoke Docker runtime templates during full validation, merge nested shrinkwrap override pins, preserve forked shrinkwrap pins, pin aged <code>lru-cache</code>, harden postpublish verification, accept main full-validation proof, and reject empty beta smoke runs.</li>
<li>E2E/QA/Crabbox: bound Telegram, Open WebUI, ClawHub, Matrix, Tool Search, MCP, gateway network, bundled runtime, kitchen-sink, codex media, config reload, and agent-turn assertion waits; prefer Azure for Windows targets; reinitialize invalid changed-gate git dirs; full-sync sparse container runs; and fail empty explicit test requests. (#87186)</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.27/OpenClaw-2026.5.27.zip" length="54488811" type="application/octet-stream" sparkle:edSignature="c5w2T1UO6vpPs70hyYH93cIyWEOd5sl5z2NkhU53E+XQBSd+jAr+xd0qf3KzWbeX2mfXYMQmnx+VMls3L22EDg=="/>
</item>
</channel>
</rss>

View File

@@ -41,7 +41,7 @@ plugins {
android {
namespace = "ai.openclaw.app"
compileSdk = 36
compileSdk = 37
// Release signing is local-only; keep the keystore path and passwords out of the repo.
signingConfigs {

View File

@@ -1,8 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
@@ -50,7 +51,7 @@
<service
android:name=".NodeForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|microphone" />
android:foregroundServiceType="connectedDevice|microphone" />
<service
android:name=".node.DeviceNotificationListenerService"
android:label="@string/app_name"

View File

@@ -0,0 +1,25 @@
package ai.openclaw.app
/** User-selectable app theme mode for Android appearance settings. */
enum class AppearanceThemeMode(
val rawValue: String,
val displayLabel: String,
) {
System(rawValue = "system", displayLabel = "System"),
Dark(rawValue = "dark", displayLabel = "Dark"),
Light(rawValue = "light", displayLabel = "Light"),
;
fun isDark(systemDark: Boolean): Boolean =
when (this) {
System -> systemDark
Dark -> true
Light -> false
}
companion object {
fun fromRawValue(value: String?): AppearanceThemeMode = entries.firstOrNull { it.rawValue == value?.trim()?.lowercase() } ?: Dark
fun fromDisplayLabel(label: String): AppearanceThemeMode = entries.firstOrNull { it.displayLabel.equals(label.trim(), ignoreCase = true) } ?: Dark
}
}

View File

@@ -2,18 +2,36 @@ package ai.openclaw.app
import ai.openclaw.app.ui.OpenClawTheme
import ai.openclaw.app.ui.RootScreen
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Main Android activity that owns Compose UI attachment and runtime UI wiring.
@@ -21,18 +39,89 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var permissionRequester: PermissionRequester
private var initializedViewModel: MainViewModel? = null
private var didAttachRuntimeUi = false
private var didStartNodeService = false
private var didStartViewModelCollectors = false
private var foreground = false
private var pendingIntent: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleAssistantIntent(intent)
pendingIntent = intent
WindowCompat.setDecorFitsSystemWindows(window, false)
permissionRequester = PermissionRequester(this)
setContent {
var activeViewModel by remember { mutableStateOf<MainViewModel?>(null) }
LaunchedEffect(Unit) {
withFrameNanos { }
withContext(Dispatchers.Default) {
(application as NodeApp).prefs
}
val readyViewModel = viewModel
activateViewModel(readyViewModel)
activeViewModel = readyViewModel
}
val currentViewModel = activeViewModel
if (currentViewModel == null) {
OpenClawTheme {
StartupSurface()
}
} else {
val appearanceThemeMode by currentViewModel.appearanceThemeMode.collectAsState()
OpenClawTheme(themeMode = appearanceThemeMode) {
RootScreen(viewModel = currentViewModel)
}
}
}
}
override fun onStart() {
super.onStart()
foreground = true
initializedViewModel?.setForeground(true)
}
override fun onStop() {
foreground = false
initializedViewModel?.setForeground(false)
super.onStop()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
pendingIntent = intent
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = intent) }
}
/**
* Wires MainViewModel only after Activity first draw and background prefs warm-up.
*/
private fun activateViewModel(readyViewModel: MainViewModel) {
if (initializedViewModel != null) return
initializedViewModel = readyViewModel
readyViewModel.setForeground(foreground)
startViewModelCollectors(readyViewModel)
pendingIntent?.let { initialIntent ->
handleAssistantIntent(viewModel = readyViewModel, intent = initialIntent)
pendingIntent = null
}
}
/**
* Starts lifecycle collectors after ViewModel construction so they cannot force early startup.
*/
private fun startViewModelCollectors(readyViewModel: MainViewModel) {
if (didStartViewModelCollectors) return
didStartViewModelCollectors = true
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.preventSleep.collect { enabled ->
readyViewModel.preventSleep.collect { enabled ->
if (enabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
@@ -44,10 +133,10 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.runtimeInitialized.collect { ready ->
readyViewModel.runtimeInitialized.collect { ready ->
if (!ready || didAttachRuntimeUi) return@collect
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
didAttachRuntimeUi = true
if (!didStartNodeService) {
NodeForegroundService.start(this@MainActivity)
@@ -56,36 +145,15 @@ class MainActivity : ComponentActivity() {
}
}
}
setContent {
OpenClawTheme {
Surface(modifier = Modifier) {
RootScreen(viewModel = viewModel)
}
}
}
}
override fun onStart() {
super.onStart()
viewModel.setForeground(true)
}
override fun onStop() {
viewModel.setForeground(false)
super.onStop()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleAssistantIntent(intent)
}
/**
* Routes assistant/app-action intents into ViewModel state without recreating the activity.
*/
private fun handleAssistantIntent(intent: android.content.Intent?) {
private fun handleAssistantIntent(
viewModel: MainViewModel,
intent: Intent?,
) {
parseHomeDestinationIntent(intent)?.let { destination ->
viewModel.requestHomeDestination(destination)
return
@@ -94,3 +162,23 @@ class MainActivity : ComponentActivity() {
viewModel.handleAssistantLaunch(request)
}
}
@Composable
private fun StartupSurface() {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Black,
contentColor = Color.White,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = "OPENCLAW",
fontSize = 22.sp,
fontWeight = FontWeight.Medium,
)
}
}
}

View File

@@ -4,6 +4,8 @@ import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.gateway.DeviceAuthStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
import ai.openclaw.app.node.CameraCaptureManager
@@ -14,6 +16,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -21,6 +24,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* UI-facing bridge that exposes NodeRuntime and preference state as Compose-friendly StateFlows.
@@ -32,7 +36,11 @@ class MainViewModel(
private val nodeApp = app as NodeApp
private val prefs = nodeApp.prefs
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
private var foreground = true
@Volatile private var foreground = false
@Volatile private var runtimeStartupQueued = false
private val _requestedHomeDestination = MutableStateFlow<HomeDestination?>(null)
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
private val _startOnboardingAtGatewaySetup = MutableStateFlow(false)
@@ -53,6 +61,19 @@ class MainViewModel(
return runtime
}
/**
* Starts the node runtime off the main thread so fresh installs can render
* the shell before encrypted prefs, device identity, and gateway setup warm up.
*/
private fun queueRuntimeStartup() {
if (runtimeRef.value != null || runtimeStartupQueued) return
runtimeStartupQueued = true
viewModelScope.launch(Dispatchers.Default) {
runCatching { ensureRuntime() }
runtimeStartupQueued = false
}
}
/**
* Adapts a runtime StateFlow to a stable ViewModel StateFlow before runtime startup.
*/
@@ -91,6 +112,7 @@ class MainViewModel(
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = runtimeState(initial = null) { it.gatewayConnectionProblem }
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 }
@@ -150,6 +172,7 @@ class MainViewModel(
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
val appearanceThemeMode: StateFlow<AppearanceThemeMode> = prefs.appearanceThemeMode
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
@@ -180,12 +203,6 @@ class MainViewModel(
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
init {
if (prefs.onboardingCompleted.value) {
ensureRuntime()
}
}
val canvas: CanvasController
get() = ensureRuntime().canvas
@@ -213,13 +230,10 @@ class MainViewModel(
*/
fun setForeground(value: Boolean) {
foreground = value
val runtime =
if (value && prefs.onboardingCompleted.value) {
ensureRuntime()
} else {
runtimeRef.value
}
runtime?.setForeground(value)
if (value && prefs.onboardingCompleted.value) {
queueRuntimeStartup()
}
runtimeRef.value?.setForeground(value)
}
fun setDisplayName(value: String) {
@@ -270,9 +284,51 @@ class MainViewModel(
prefs.setGatewayPassword(value)
}
/** Clears setup credentials through the runtime so active gateway sessions drop stale auth state. */
fun resetGatewaySetupAuth() {
ensureRuntime().resetGatewaySetupAuth()
/** Clears setup credentials without starting the runtime just to discard first-run pairing auth. */
private fun resetGatewaySetupAuth() {
runtimeRef.value?.resetGatewaySetupAuth() ?: resetGatewaySetupAuthWithoutRuntime()
}
private fun resetGatewaySetupAuthWithoutRuntime() {
prefs.clearGatewaySetupAuth()
val deviceId = DeviceIdentityStore(nodeApp).loadOrCreate().deviceId
val deviceAuthStore = DeviceAuthStore(prefs)
deviceAuthStore.clearToken(deviceId, "node")
deviceAuthStore.clearToken(deviceId, "operator")
}
fun saveGatewayConfigAndConnect(
host: String,
port: Int,
tls: Boolean,
token: String,
bootstrapToken: String,
password: String,
resetSetupAuth: Boolean,
) {
// Gateway pairing touches encrypted prefs, identity files, and sockets; keep
// the whole sequence off the Compose thread so retries cannot trigger ANRs.
viewModelScope.launch(Dispatchers.Default) {
if (resetSetupAuth) {
resetGatewaySetupAuth()
}
prefs.setManualEnabled(true)
prefs.setManualHost(host)
prefs.setManualPort(port)
prefs.setManualTls(tls)
prefs.setGatewayBootstrapToken(bootstrapToken)
prefs.setGatewayToken(token)
prefs.setGatewayPassword(password)
ensureRuntime()
.connect(
GatewayEndpoint.manual(host = host, port = port),
NodeRuntime.GatewayConnectAuth(
token = token.ifEmpty { null },
bootstrapToken = bootstrapToken.ifEmpty { null },
password = password.ifEmpty { null },
),
)
}
}
/** Marks onboarding complete and starts the runtime before UI observes connected-state flows. */
@@ -285,10 +341,12 @@ class MainViewModel(
/** Re-enters gateway setup after disconnecting and clearing one-time setup credentials. */
fun pairNewGateway() {
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
_startOnboardingAtGatewaySetup.value = true
prefs.setOnboardingCompleted(false)
viewModelScope.launch(Dispatchers.Default) {
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
prefs.setOnboardingCompleted(false)
_startOnboardingAtGatewaySetup.value = true
}
}
/** Acknowledges the one-shot request that opens onboarding at the gateway setup step. */
@@ -383,14 +441,30 @@ class MainViewModel(
ensureRuntime().setSpeakerEnabled(enabled)
}
fun setAppearanceThemeMode(mode: AppearanceThemeMode) {
prefs.setAppearanceThemeMode(mode)
}
fun refreshGatewayConnection() {
ensureRuntime().refreshGatewayConnection()
viewModelScope.launch(Dispatchers.Default) {
ensureRuntime().refreshGatewayConnection()
}
}
fun startGatewayDiscovery() {
queueRuntimeStartup()
}
fun connect(endpoint: GatewayEndpoint) {
ensureRuntime().connect(endpoint)
}
fun connectInBackground(endpoint: GatewayEndpoint) {
viewModelScope.launch(Dispatchers.Default) {
ensureRuntime().connect(endpoint)
}
}
fun connect(
endpoint: GatewayEndpoint,
token: String?,

View File

@@ -23,7 +23,6 @@ import kotlinx.coroutines.launch
class NodeForegroundService : Service() {
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var notificationJob: Job? = null
private var didStartForeground = false
private var voiceCaptureMode = VoiceCaptureMode.Off
override fun onCreate() {
@@ -183,13 +182,7 @@ class NodeForegroundService : Service() {
private fun startForegroundWithTypes(notification: Notification) {
val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
if (didStartForeground) {
// Re-issue startForeground when Talk mode toggles so Android sees the microphone service type.
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
return
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
didStartForeground = true
}
companion object {
@@ -200,19 +193,16 @@ class NodeForegroundService : Service() {
private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
/** Starts the persistent node foreground service from UI lifecycle code. */
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)
context.startForegroundService(intent)
}
/** Requests disconnect through the service action path so notification actions and UI share behavior. */
fun stop(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
context.startService(intent)
}
/** Updates Android's foreground-service type before voice capture mode changes require microphone access. */
fun setVoiceCaptureMode(
context: Context,
mode: VoiceCaptureMode,
@@ -231,11 +221,8 @@ class NodeForegroundService : Service() {
}
}
/**
* Foreground-service type mask required by Android for the current voice capture mode.
*/
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
return if (mode == VoiceCaptureMode.TalkMode) {
base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
@@ -243,9 +230,6 @@ internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
}
}
/**
* Compact notification suffix for voice state; kept pure for service-notification tests.
*/
internal fun voiceNotificationSuffix(
mode: VoiceCaptureMode,
manualMicEnabled: Boolean,

View File

@@ -78,6 +78,25 @@ import java.util.concurrent.atomic.AtomicLong
/**
* Process runtime that owns gateway sessions, node command handlers, capture managers, and UI-facing state.
*/
data class GatewayConnectionProblem(
val code: String?,
val message: String,
val reason: String?,
val requestId: String?,
val recommendedNextStep: String?,
val pauseReconnect: Boolean,
val retryable: Boolean,
) {
val isPairingRequired: Boolean = code == "PAIRING_REQUIRED"
val canAutoRetry: Boolean =
isPairingRequired &&
(
retryable ||
!pauseReconnect ||
recommendedNextStep == "wait_then_retry"
)
}
class NodeRuntime(
context: Context,
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
@@ -285,6 +304,8 @@ class NodeRuntime(
private val _statusText = MutableStateFlow("Offline")
val statusText: StateFlow<String> = _statusText.asStateFlow()
private val _gatewayConnectionProblem = MutableStateFlow<GatewayConnectionProblem?>(null)
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = _gatewayConnectionProblem.asStateFlow()
private val _pendingGatewayTrust = MutableStateFlow<GatewayTrustPrompt?>(null)
val pendingGatewayTrust: StateFlow<GatewayTrustPrompt?> = _pendingGatewayTrust.asStateFlow()
@@ -410,6 +431,7 @@ class NodeRuntime(
identityStore = identityStore,
deviceAuthStore = deviceAuthStore,
onConnected = { hello ->
_gatewayConnectionProblem.value = null
operatorConnected = true
operatorStatusText = "Connected"
_serverName.value = hello.serverName
@@ -457,6 +479,7 @@ class NodeRuntime(
updateStatus()
micCapture.onGatewayConnectionChanged(false)
},
onConnectFailure = ::handleGatewayConnectFailure,
onEvent = { event, payloadJson ->
handleGatewayEvent(event, payloadJson)
},
@@ -468,6 +491,7 @@ class NodeRuntime(
identityStore = identityStore,
deviceAuthStore = deviceAuthStore,
onConnected = {
_gatewayConnectionProblem.value = null
_nodeConnected.value = true
nodeStatusText = "Connected"
didAutoRequestCanvasRehydrate = false
@@ -493,6 +517,7 @@ class NodeRuntime(
updateStatus()
showLocalCanvasOnDisconnect()
},
onConnectFailure = ::handleGatewayConnectFailure,
onEvent = { _, _ -> },
onInvoke = { req ->
invokeDispatcher.handleInvoke(req.command, req.paramsJson)
@@ -687,6 +712,23 @@ class NodeRuntime(
updateHomeCanvasState()
}
private fun handleGatewayConnectFailure(
error: GatewaySession.ErrorShape,
pauseReconnect: Boolean,
) {
val details = error.details
_gatewayConnectionProblem.value =
GatewayConnectionProblem(
code = details?.code ?: error.code,
message = error.message,
reason = details?.reason,
requestId = details?.requestId,
recommendedNextStep = details?.recommendedNextStep,
pauseReconnect = pauseReconnect || details?.pauseReconnect == true,
retryable = details?.retryable == true,
)
}
private fun resolveMainSessionKey(): String {
val trimmed = _mainSessionKey.value.trim()
return if (trimmed.isEmpty()) "main" else trimmed
@@ -1410,11 +1452,14 @@ class NodeRuntime(
}
fun refreshGatewayConnection() {
val endpoint =
connectedEndpoint ?: run {
_statusText.value = "Failed: no cached gateway endpoint"
return
}
val endpoint = connectedEndpoint
if (endpoint == null) {
resolvePreferredGatewayEndpoint()?.let(::connect)
?: run {
_statusText.value = "Failed: no saved gateway endpoint"
}
return
}
operatorStatusText = "Connecting…"
updateStatus()
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
@@ -1524,6 +1569,7 @@ class NodeRuntime(
connectAttemptId: Long,
) {
if (!isCurrentConnectAttempt(connectAttemptId)) return
_gatewayConnectionProblem.value = null
connectedEndpoint = endpoint
operatorStatusText = "Connecting…"
nodeStatusText = "Connecting…"
@@ -1620,6 +1666,7 @@ class NodeRuntime(
stopActiveVoiceSession()
connectedEndpoint = null
activeGatewayAuth = null
_gatewayConnectionProblem.value = null
_pendingGatewayTrust.value = null
operatorSession.disconnect()
nodeSession.disconnect()
@@ -1858,7 +1905,7 @@ class NodeRuntime(
return
}
try {
val modelsRes = operatorSession.request("models.list", """{"view":"all"}""")
val modelsRes = operatorSession.request("models.list", "{}")
val modelsRoot = json.parseToJsonElement(modelsRes).asObjectOrNull()
_modelCatalog.value = parseGatewayModels(modelsRoot?.get("models") as? JsonArray)
@@ -2085,6 +2132,7 @@ class NodeRuntime(
id = id,
name = obj["name"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: id,
provider = provider,
available = obj.optionalBoolean("available"),
supportsVision = "image" in inputTypes,
supportsAudio = "audio" in inputTypes,
supportsDocuments = "document" in inputTypes,
@@ -2701,6 +2749,7 @@ data class GatewayModelSummary(
val id: String,
val name: String,
val provider: String,
val available: Boolean?,
val supportsVision: Boolean,
val supportsAudio: Boolean,
val supportsDocuments: Boolean,
@@ -2883,6 +2932,15 @@ private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonP
private fun JsonObject?.boolean(key: String): Boolean = (this?.get(key) as? JsonPrimitive)?.content?.trim() == "true"
private fun JsonObject?.optionalBoolean(key: String): Boolean? =
(this?.get(key) as? JsonPrimitive)?.content?.trim()?.lowercase()?.let { value ->
when (value) {
"true" -> true
"false" -> false
else -> null
}
}
internal fun cronJobLastRunStatus(state: JsonObject?): String? =
state
.cronStatus("lastStatus")

View File

@@ -53,6 +53,7 @@ class PermissionRequester internal constructor(
private val mutex = Mutex()
private val requestSlotsLock = Any()
private val mainHandler = Handler(Looper.getMainLooper())
// ActivityResult launchers cannot be registered after start; pre-register a small pool for nested UI flows.
private val launchers = List(4) { createPermissionRequestSlot(launcherFactory) }

View File

@@ -42,6 +42,7 @@ class SecurePrefs(
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
private const val voiceMicEnabledKey = "voice.micEnabled"
private const val appearanceThemeModeKey = "appearance.themeMode"
}
private val appContext = context.applicationContext
@@ -181,6 +182,10 @@ class SecurePrefs(
private val _speakerEnabled = MutableStateFlow(plainPrefs.getBoolean("voice.speakerEnabled", true))
val speakerEnabled: StateFlow<Boolean> = _speakerEnabled
private val _appearanceThemeMode =
MutableStateFlow(AppearanceThemeMode.fromRawValue(plainPrefs.getString(appearanceThemeModeKey, null)))
val appearanceThemeMode: StateFlow<AppearanceThemeMode> = _appearanceThemeMode
fun setLastDiscoveredStableId(value: String) {
val trimmed = value.trim()
plainPrefs.edit { putString("gateway.lastDiscoveredStableID", trimmed) }
@@ -525,6 +530,11 @@ class SecurePrefs(
_speakerEnabled.value = value
}
fun setAppearanceThemeMode(mode: AppearanceThemeMode) {
plainPrefs.edit { putString(appearanceThemeModeKey, mode.rawValue) }
_appearanceThemeMode.value = mode
}
private fun loadNotificationForwardingPackages(): Set<String> {
val raw = plainPrefs.getString(notificationsForwardingPackagesKey, null)?.trim()
if (raw.isNullOrEmpty()) {

View File

@@ -61,9 +61,11 @@ class ChatController(
private val pendingRuns = mutableSetOf<String>()
private val pendingRunTimeoutJobs = ConcurrentHashMap<String, Job>()
// Preserve sent messages locally until chat.history includes the gateway-confirmed copy.
private val optimisticMessagesByRunId = LinkedHashMap<String, ChatMessage>()
private val pendingRunTimeoutMs = 120_000L
// Drops stale history responses after session switches or refresh races.
private val historyLoadGeneration = AtomicLong(0)
@@ -225,6 +227,7 @@ class ChatController(
role = "user",
content = userContent,
timestampMs = System.currentTimeMillis(),
idempotencyKey = "$runId:user",
)
optimisticMessagesByRunId[runId] = optimisticMessage
_messages.value = _messages.value + optimisticMessage
@@ -350,6 +353,7 @@ class ChatController(
)
if (!isCurrentHistoryLoad(sessionKey, _sessionKey.value, generation, historyLoadGeneration.get())) return
val history = parseHistory(historyJson, sessionKey = sessionKey, previousMessages = _messages.value)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
_historyLoading.value = false
@@ -422,10 +426,8 @@ class ChatController(
}
if (runId != null) {
clearPendingRun(runId)
optimisticMessagesByRunId.remove(runId)
} else {
clearPendingRuns()
optimisticMessagesByRunId.clear()
clearPendingRuns(clearOptimisticMessages = false)
}
pendingToolCallsById.clear()
publishPendingToolCalls()
@@ -455,6 +457,7 @@ class ChatController(
sessionKey = currentSessionKey,
previousMessages = _messages.value,
)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
history.thinkingLevel
@@ -561,12 +564,14 @@ class ChatController(
}
}
private fun clearPendingRuns() {
private fun clearPendingRuns(clearOptimisticMessages: Boolean = true) {
for ((_, job) in pendingRunTimeoutJobs) {
job.cancel()
}
pendingRunTimeoutJobs.clear()
optimisticMessagesByRunId.clear()
if (clearOptimisticMessages) {
optimisticMessagesByRunId.clear()
}
synchronized(pendingRuns) {
pendingRuns.clear()
_pendingRunCount.value = 0
@@ -578,6 +583,15 @@ class ChatController(
_messages.value = _messages.value.filterNot { it.id == message.id }
}
private fun prunePersistedOptimisticMessages(incoming: List<ChatMessage>) {
val retained =
retainUnmatchedOptimisticMessages(
incoming = incoming,
optimistic = optimisticMessagesByRunId.values,
).toSet()
optimisticMessagesByRunId.entries.removeAll { entry -> entry.value !in retained }
}
private fun parseHistory(
historyJson: String,
sessionKey: String,
@@ -592,13 +606,14 @@ class ChatController(
array.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val role = obj["role"].asStringOrNull() ?: return@mapNotNull null
val content = obj["content"].asArrayOrNull()?.mapNotNull(::parseChatMessageContent) ?: emptyList()
val content = parseChatMessageContents(obj)
val ts = obj["timestamp"].asLongOrNull()
ChatMessage(
id = UUID.randomUUID().toString(),
role = role,
content = content,
timestampMs = ts,
idempotencyKey = obj["idempotencyKey"].asStringOrNull(),
)
}
@@ -674,6 +689,19 @@ internal fun parseChatMessageContent(el: JsonElement): ChatMessageContent? {
}
}
internal fun parseChatMessageContents(obj: JsonObject): List<ChatMessageContent> {
obj["content"].asArrayOrNull()?.let { content ->
return content.mapNotNull(::parseChatMessageContent)
}
obj["content"].asStringOrNull()?.let { text ->
return listOf(ChatMessageContent(type = "text", text = text))
}
obj["text"].asStringOrNull()?.let { text ->
return listOf(ChatMessageContent(type = "text", text = text))
}
return emptyList()
}
internal data class MainSessionState(
val currentSessionKey: String,
val appliedMainSessionKey: String,
@@ -732,29 +760,41 @@ internal fun mergeOptimisticMessages(
): 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
}
}
val missingOptimistic = retainUnmatchedOptimisticMessages(incoming = incoming, optimistic = optimistic)
if (missingOptimistic.isEmpty()) return incoming
return (incoming + missingOptimistic).sortedWith(compareBy<ChatMessage> { it.timestampMs ?: Long.MAX_VALUE }.thenBy { it.id })
}
internal fun retainUnmatchedOptimisticMessages(
incoming: List<ChatMessage>,
optimistic: Collection<ChatMessage>,
): List<ChatMessage> {
if (optimistic.isEmpty()) return emptyList()
val unmatchedIncoming = incoming.toMutableList()
return optimistic.filter { message ->
val matchIndex =
unmatchedIncoming.indexOfFirst { incomingMessage ->
incomingMessageConsumesOptimistic(incomingMessage, message)
}
if (matchIndex >= 0) {
unmatchedIncoming.removeAt(matchIndex)
false
} else {
true
}
}
}
/**
* Message identity used only for refresh reconciliation; it avoids exposing gateway ids as UI keys.
*/
internal fun messageIdentityKey(message: ChatMessage): String? {
val idempotencyKey = message.idempotencyKey?.trim().orEmpty()
if (idempotencyKey.isNotEmpty()) {
return listOf(message.role.trim().lowercase(), idempotencyKey).joinToString(separator = "|")
}
val contentKey = messageContentIdentityKey(message) ?: return null
val timestamp = message.timestampMs?.toString().orEmpty()
if (timestamp.isEmpty() && contentKey.isEmpty()) return null
@@ -767,6 +807,10 @@ private fun incomingMessageConsumesOptimistic(
incoming: ChatMessage,
optimistic: ChatMessage,
): Boolean {
val optimisticIdempotencyKey = optimistic.idempotencyKey?.trim().orEmpty()
if (optimisticIdempotencyKey.isNotEmpty()) {
return incoming.idempotencyKey?.trim() == optimisticIdempotencyKey
}
if (optimisticMessageIdentityKey(incoming) != optimisticMessageIdentityKey(optimistic)) return false
val incomingTimestamp = incoming.timestampMs ?: return false
val optimisticTimestamp = optimistic.timestampMs ?: return true

View File

@@ -8,6 +8,7 @@ data class ChatMessage(
val role: String,
val content: List<ChatMessageContent>,
val timestampMs: Long?,
val idempotencyKey: String? = null,
)
/**

View File

@@ -49,6 +49,19 @@ import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private fun createDnsResolver(context: Context): DnsResolver =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
createContextDnsResolver(context)
} else {
createLegacyDnsResolver()
}
@TargetApi(Build.VERSION_CODES.CINNAMON_BUN)
private fun createContextDnsResolver(context: Context): DnsResolver = DnsResolver(context, null)
@Suppress("DEPRECATION")
private fun createLegacyDnsResolver(): DnsResolver = DnsResolver.getInstance()
/**
* Watches local DNS-SD and optional wide-area DNS-SD for reachable OpenClaw gateways.
*/
@@ -58,7 +71,7 @@ class GatewayDiscovery(
) {
private val nsd = context.getSystemService(NsdManager::class.java)
private val connectivity = context.getSystemService(ConnectivityManager::class.java)
private val dns = DnsResolver.getInstance()
private val dns = createDnsResolver(context)
private val serviceType = "_openclaw-gw._tcp."
private val wideAreaDomain = System.getenv("OPENCLAW_WIDE_AREA_DOMAIN")
private val logTag = "OpenClaw/GatewayDiscovery"
@@ -66,10 +79,12 @@ class GatewayDiscovery(
private val localById = ConcurrentHashMap<String, GatewayEndpoint>()
private val unicastById = ConcurrentHashMap<String, GatewayEndpoint>()
private val _gateways = MutableStateFlow<List<GatewayEndpoint>>(emptyList())
/** Current discovered gateway list, merged from local DNS-SD and optional wide-area DNS-SD. */
val gateways: StateFlow<List<GatewayEndpoint>> = _gateways.asStateFlow()
private val _statusText = MutableStateFlow("Searching…")
/** Short diagnostic text shown by connect UI while discovery is running. */
val statusText: StateFlow<String> = _statusText.asStateFlow()

View File

@@ -77,6 +77,8 @@ data class GatewayConnectErrorDetails(
val recommendedNextStep: String?,
val pauseReconnect: Boolean? = null,
val reason: String? = null,
val requestId: String? = null,
val retryable: Boolean = false,
)
/**
@@ -120,6 +122,7 @@ class GatewaySession(
private val deviceAuthStore: DeviceAuthTokenStore,
private val onConnected: (GatewayHelloSummary) -> Unit,
private val onDisconnected: (message: String) -> Unit,
private val onConnectFailure: (error: ErrorShape, pauseReconnect: Boolean) -> Unit = { _, _ -> },
private val onEvent: (event: String, payloadJson: String?) -> Unit,
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null,
private val onTlsFingerprint: ((stableId: String, fingerprint: String) -> Unit)? = null,
@@ -127,6 +130,7 @@ class GatewaySession(
private companion object {
// Keep connect timeout above observed gateway unauthorized close on lower-end devices.
private const val CONNECT_RPC_TIMEOUT_MS = 12_000L
private val PAIRING_REQUEST_ID_PATTERN = Regex("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
}
/**
@@ -923,6 +927,8 @@ class GatewaySession(
recommendedNextStep = it["recommendedNextStep"].asStringOrNull(),
pauseReconnect = it["pauseReconnect"].asBooleanOrNull(),
reason = it["reason"].asStringOrNull(),
requestId = normalizePairingRequestId(it["requestId"].asStringOrNull()),
retryable = it["retryable"].asBooleanOrNull() == true,
)
}
ErrorShape(code, msg, details)
@@ -948,6 +954,11 @@ class GatewaySession(
onEvent(event, payloadJson)
}
private fun normalizePairingRequestId(requestId: String?): String? {
val trimmed = requestId?.trim()?.takeIf { it.isNotEmpty() } ?: return null
return trimmed.takeIf { PAIRING_REQUEST_ID_PATTERN.matches(it) }
}
private suspend fun awaitConnectNonce(): String =
try {
withTimeout(2_000) { connectNonceDeferred.await() }
@@ -1061,10 +1072,14 @@ class GatewaySession(
} catch (err: Throwable) {
attempt += 1
onDisconnected("Gateway error: ${err.message ?: err::class.java.simpleName}")
if (
err is GatewayConnectFailure &&
shouldPauseReconnectAfterAuthFailure(err.gatewayError)
) {
val gatewayConnectFailure = err as? GatewayConnectFailure
val pauseForAuthFailure =
gatewayConnectFailure
?.let { shouldPauseReconnectAfterAuthFailure(it.gatewayError) } == true
if (gatewayConnectFailure != null) {
onConnectFailure(gatewayConnectFailure.gatewayError, pauseForAuthFailure)
}
if (pauseForAuthFailure) {
reconnectPausedForAuthFailure = true
continue
}

View File

@@ -30,8 +30,7 @@ private const val MAX_DEVICE_APPS_LIMIT = 200
private const val DEVICE_APPS_SYSTEM_FLAGS =
ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
internal fun isSystemDeviceApp(appInfo: ApplicationInfo): Boolean =
(appInfo.flags and DEVICE_APPS_SYSTEM_FLAGS) != 0
internal fun isSystemDeviceApp(appInfo: ApplicationInfo): Boolean = (appInfo.flags and DEVICE_APPS_SYSTEM_FLAGS) != 0
internal data class DeviceAppEntry(
val label: String,

View File

@@ -297,17 +297,15 @@ private fun CommandSectionLabel(title: String) {
}
}
/** Builds provider quick-action metadata from current gateway/catalog state. */
private fun providerCommandSubtitle(
internal 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 (!isConnected) return "Connect Gateway to view providers"
val readyProviderCount = providerRows(providers = providers, models = models).count { it.ready }
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
if (models.isNotEmpty()) return "${models.size} models available"
return "Configure model access"
return "No ready providers"
}
/** Falls back to the canonical main-session label when gateway display names are blank. */

View File

@@ -1,7 +1,7 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayConnectionProblem
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.ui.mobileCardSurface
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
@@ -66,6 +66,7 @@ private enum class ConnectInputMode {
fun ConnectTabScreen(viewModel: MainViewModel) {
val context = LocalContext.current
val statusText by viewModel.statusText.collectAsState()
val gatewayConnectionProblem by viewModel.gatewayConnectionProblem.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
val remoteAddress by viewModel.remoteAddress.collectAsState()
val manualHost by viewModel.manualHost.collectAsState()
@@ -147,13 +148,10 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
}
}
val showDiagnostics = !isConnected && gatewayStatusHasDiagnostics(statusText)
val pairingRequired = !isConnected && gatewayStatusLooksLikePairing(statusText)
val statusLabel = gatewayStatusForDisplay(statusText)
PairingAutoRetryEffect(enabled = pairingRequired) {
viewModel.refreshGatewayConnection()
}
val showDiagnostics = !isConnected && (gatewayConnectionProblem != null || gatewayStatusHasDiagnostics(statusText))
val pairingRequired = !isConnected && (gatewayConnectionProblem?.isPairingRequired == true || gatewayStatusLooksLikePairing(statusText))
val pairingInstruction = gatewayPairingInstruction(gatewayConnectionProblem)
val statusLabel = gatewayStatusForDisplay(gatewayConnectionProblem?.message ?: statusText)
Column(
modifier = Modifier.verticalScroll(rememberScrollState()).padding(horizontal = 20.dp, vertical = 16.dp),
@@ -291,27 +289,14 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
}
validationText = null
if (inputMode == ConnectInputMode.SetupCode) {
// Setup-code auth should replace old bootstrap/shared credentials;
// manual reconnects keep existing typed credentials.
viewModel.resetGatewaySetupAuth()
}
viewModel.setManualEnabled(true)
viewModel.setManualHost(config.host)
viewModel.setManualPort(config.port)
viewModel.setManualTls(config.tls)
viewModel.setGatewayBootstrapToken(config.bootstrapToken)
if (config.token.isNotBlank()) {
viewModel.setGatewayToken(config.token)
} else if (config.bootstrapToken.isNotBlank()) {
viewModel.setGatewayToken("")
}
viewModel.setGatewayPassword(config.password)
viewModel.connect(
GatewayEndpoint.manual(host = config.host, port = config.port),
token = config.token.ifEmpty { null },
bootstrapToken = config.bootstrapToken.ifEmpty { null },
password = config.password.ifEmpty { null },
viewModel.saveGatewayConfigAndConnect(
host = config.host,
port = config.port,
tls = config.tls,
token = config.token,
bootstrapToken = config.bootstrapToken,
password = config.password,
resetSetupAuth = inputMode == ConnectInputMode.SetupCode,
)
},
modifier = Modifier.fillMaxWidth().height(52.dp),
@@ -341,7 +326,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
Text(statusLabel, style = mobileBody.copy(fontFamily = FontFamily.Monospace), color = mobileText)
if (pairingRequired) {
Text(
"Approve this phone on the gateway. OpenClaw retries automatically while this screen stays open.",
pairingInstruction,
style = mobileCallout,
color = mobileTextSecondary,
)
@@ -590,6 +575,13 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
}
}
private fun gatewayPairingInstruction(problem: GatewayConnectionProblem?): String =
if (problem?.canAutoRetry == true) {
"Approve this phone on the gateway. OpenClaw will reconnect automatically."
} else {
"Approve this phone on the gateway, then retry the connection."
}
@Composable
private fun MethodChip(
label: String,

View File

@@ -1,53 +0,0 @@
package ai.openclaw.app.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
internal const val PAIRING_INITIAL_AUTO_RETRY_MS = 1_500L
internal const val PAIRING_AUTO_RETRY_MS = 4_000L
/** Retries pairing-only gateway refreshes while the screen is visible and started. */
@Composable
internal fun PairingAutoRetryEffect(
enabled: Boolean,
onRetry: () -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
var lifecycleStarted by
remember(lifecycleOwner) {
mutableStateOf(lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED))
}
DisposableEffect(lifecycleOwner) {
val observer =
LifecycleEventObserver { _, _ ->
lifecycleStarted = lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect(enabled, lifecycleStarted) {
if (!enabled || !lifecycleStarted) {
return@LaunchedEffect
}
// Give the gateway a short settling window before the first retry so an
// approval response is not immediately chased by a redundant reconnect.
delay(PAIRING_INITIAL_AUTO_RETRY_MS)
while (true) {
onRetry()
delay(PAIRING_AUTO_RETRY_MS)
}
}
}

View File

@@ -41,27 +41,27 @@ internal data class MobileColors(
internal fun lightMobileColors() =
MobileColors(
surface = Color(0xFFF6F7FA),
surfaceStrong = Color(0xFFECEEF3),
surface = Color(0xFFFAFBFC),
surfaceStrong = Color(0xFFEFF3F8),
cardSurface = Color(0xFFFFFFFF),
border = Color(0xFFE5E7EC),
borderStrong = Color(0xFFD6DAE2),
text = Color(0xFF17181C),
textSecondary = Color(0xFF5D6472),
textTertiary = Color(0xFF99A0AE),
accent = Color(0xFF1D5DD8),
accentSoft = Color(0xFFECF3FF),
accentBorderStrong = Color(0xFF184DAF),
success = Color(0xFF2F8C5A),
successSoft = Color(0xFFEEF9F3),
warning = Color(0xFFC8841A),
warningSoft = Color(0xFFFFF8EC),
danger = Color(0xFFD04B4B),
dangerSoft = Color(0xFFFFF2F2),
codeBg = Color(0xFF15171B),
codeText = Color(0xFFE8EAEE),
codeBorder = Color(0xFF2B2E35),
codeAccent = Color(0xFF3FC97A),
border = Color(0xFFDDE3EC),
borderStrong = Color(0xFFC7D0DC),
text = Color(0xFF16181D),
textSecondary = Color(0xFF505B6A),
textTertiary = Color(0xFF8E98A7),
accent = Color(0xFF1B5ACB),
accentSoft = Color(0xFFEAF2FF),
accentBorderStrong = Color(0xFF174CA9),
success = Color(0xFF287F52),
successSoft = Color(0xFFEAF7F0),
warning = Color(0xFFAF7418),
warningSoft = Color(0xFFFFF4DF),
danger = Color(0xFFC94343),
dangerSoft = Color(0xFFFFECEC),
codeBg = Color(0xFFEFF3F8),
codeText = Color(0xFF172033),
codeBorder = Color(0xFFD7DDE7),
codeAccent = Color(0xFF287F52),
chipBorderConnected = Color(0xFFCFEBD8),
chipBorderConnecting = Color(0xFFD5E2FA),
chipBorderWarning = Color(0xFFEED8B8),

View File

@@ -1,9 +1,10 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayConnectionProblem
import ai.openclaw.app.LocationMode
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.R
import ai.openclaw.app.SensitiveFeatureConfig
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.node.DeviceNotificationListenerService
import ai.openclaw.app.ui.design.ClawDesignTheme
import ai.openclaw.app.ui.design.ClawErrorState
@@ -31,24 +32,31 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
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.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
@@ -88,10 +96,13 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -120,14 +131,19 @@ fun OnboardingFlow(
viewModel: MainViewModel,
modifier: Modifier = Modifier,
) {
ClawDesignTheme {
val appearanceThemeMode by viewModel.appearanceThemeMode.collectAsState()
val onboardingDark = appearanceThemeMode.isDark(systemDark = isSystemInDarkTheme())
ClawDesignTheme(dark = onboardingDark) {
val context = LocalContext.current
val statusText by viewModel.statusText.collectAsState()
val gatewayConnectionProblem by viewModel.gatewayConnectionProblem.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
val isNodeConnected by viewModel.isNodeConnected.collectAsState()
val runtimeInitialized by viewModel.runtimeInitialized.collectAsState()
val serverName by viewModel.serverName.collectAsState()
val remoteAddress by viewModel.remoteAddress.collectAsState()
val gateways by viewModel.gateways.collectAsState()
val discoveryStatusText by viewModel.discoveryStatusText.collectAsState()
val savedToken by viewModel.gatewayToken.collectAsState()
val pendingTrust by viewModel.pendingGatewayTrust.collectAsState()
val startAtGatewaySetup by viewModel.startOnboardingAtGatewaySetup.collectAsState()
@@ -142,9 +158,12 @@ fun OnboardingFlow(
var password by rememberSaveable { mutableStateOf("") }
var setupError by rememberSaveable { mutableStateOf<String?>(null) }
var attemptedConnect by rememberSaveable { mutableStateOf(false) }
var attemptedGatewayName by rememberSaveable { mutableStateOf<String?>(null) }
var connectAttemptStartedAtMs by rememberSaveable { mutableLongStateOf(0L) }
var recoveryNowMs by remember { mutableLongStateOf(SystemClock.elapsedRealtime()) }
OpenClawSystemBarAppearance(lightAppearance = !onboardingDark && step != OnboardingStep.Welcome)
val qrScannerOptions =
remember {
GmsBarcodeScannerOptions
@@ -163,6 +182,12 @@ fun OnboardingFlow(
}
}
LaunchedEffect(step) {
if (step == OnboardingStep.Gateway) {
viewModel.startGatewayDiscovery()
}
}
LaunchedEffect(ready, attemptedConnect) {
if (attemptedConnect && ready) {
step = OnboardingStep.Permissions
@@ -203,10 +228,12 @@ fun OnboardingFlow(
when (step) {
OnboardingStep.Welcome ->
WelcomeScreen(
modifier = modifier,
onConnect = { step = OnboardingStep.Gateway },
)
ClawDesignTheme(dark = true) {
WelcomeScreen(
modifier = modifier,
onConnect = { step = OnboardingStep.Gateway },
)
}
OnboardingStep.Gateway ->
GatewaySetupScreen(
modifier = modifier,
@@ -217,6 +244,8 @@ fun OnboardingFlow(
token = token,
password = password,
nearbyGatewayName = gateways.firstOrNull()?.name,
discoveryStatusText = discoveryStatusText,
discoveryStarted = runtimeInitialized,
error = setupError,
onBack = { step = OnboardingStep.Welcome },
onScan = {
@@ -253,8 +282,10 @@ fun OnboardingFlow(
onPasswordChange = { password = it },
onUseNearby = {
val endpoint = gateways.firstOrNull() ?: return@GatewaySetupScreen
attemptedGatewayName = endpoint.name
attemptedConnect = true
viewModel.connect(endpoint)
connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
viewModel.connectInBackground(endpoint)
step = OnboardingStep.Recovery
},
onPair = {
@@ -273,23 +304,17 @@ fun OnboardingFlow(
}
setupError = null
attemptedGatewayName = null
attemptedConnect = true
connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
// Setup-code pairing replaces any stale shared credentials before
// the bootstrap token is stored for the first authenticated connect.
viewModel.resetGatewaySetupAuth()
viewModel.setManualEnabled(true)
viewModel.setManualHost(config.host)
viewModel.setManualPort(config.port)
viewModel.setManualTls(config.tls)
viewModel.setGatewayBootstrapToken(config.bootstrapToken)
viewModel.setGatewayToken(config.token)
viewModel.setGatewayPassword(config.password)
viewModel.connect(
GatewayEndpoint.manual(host = config.host, port = config.port),
token = config.token.ifEmpty { null },
bootstrapToken = config.bootstrapToken.ifEmpty { null },
password = config.password.ifEmpty { null },
viewModel.saveGatewayConfigAndConnect(
host = config.host,
port = config.port,
tls = config.tls,
token = config.token,
bootstrapToken = config.bootstrapToken,
password = config.password,
resetSetupAuth = true,
)
step = OnboardingStep.Recovery
},
@@ -299,11 +324,11 @@ fun OnboardingFlow(
modifier = modifier,
statusText = statusText,
serverName = serverName,
attemptedGatewayName = attemptedGatewayName,
remoteAddress = remoteAddress,
ready = ready,
attemptedConnect = attemptedConnect,
gatewayConnectionProblem = gatewayConnectionProblem,
connectSettling = recoveryNowMs - connectAttemptStartedAtMs < GATEWAY_CONNECT_SETTLING_MS,
onAutoRetry = viewModel::refreshGatewayConnection,
onBack = { step = OnboardingStep.Gateway },
onRetry = {
attemptedConnect = true
@@ -317,11 +342,14 @@ fun OnboardingFlow(
token = token,
password = password,
) ?: return@GatewayRecoveryScreen
viewModel.connect(
GatewayEndpoint.manual(host = config.host, port = config.port),
token = config.token.ifEmpty { null },
bootstrapToken = config.bootstrapToken.ifEmpty { null },
password = config.password.ifEmpty { null },
viewModel.saveGatewayConfigAndConnect(
host = config.host,
port = config.port,
tls = config.tls,
token = config.token,
bootstrapToken = config.bootstrapToken,
password = config.password,
resetSetupAuth = false,
)
},
onEdit = { step = OnboardingStep.Gateway },
@@ -346,20 +374,39 @@ private fun WelcomeScreen(
onConnect: () -> Unit,
modifier: Modifier = Modifier,
) {
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 18.dp)) {
val welcomeBackground =
Brush.verticalGradient(
colors =
listOf(
Color(0xFFFF4D4D),
Color(0xFFD73332),
Color(0xFF991B1B),
Color(0xFF260707),
),
)
Box(
modifier =
modifier
.fillMaxSize()
.background(welcomeBackground)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(horizontal = 24.dp, vertical = 18.dp),
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(96.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(18.dp)) {
WelcomeLogo()
Text(
text = "OPENCLAW",
style = ClawTheme.type.display.copy(fontSize = 34.sp, lineHeight = 38.sp, fontWeight = FontWeight.Black),
color = ClawTheme.colors.text,
)
Text(
text = "Your AI command center.\nPrivate. Local. Under your control.",
text = "Your personal AI assistant.\nExfoliate! Exfoliate!",
style = ClawTheme.type.section,
color = ClawTheme.colors.text,
textAlign = TextAlign.Center,
@@ -370,19 +417,26 @@ private fun WelcomeScreen(
Spacer(modifier = Modifier.height(30.dp))
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
HeroPrimaryAction(title = "Connect Gateway", onClick = onConnect)
OutlinedAction(title = "Enter setup code", icon = Icons.AutoMirrored.Filled.KeyboardArrowRight, onClick = onConnect)
Surface(onClick = onConnect, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Text(text = "Already have a setup? ", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
Text(text = "Sign in", style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold), color = ClawTheme.colors.text)
}
}
}
Spacer(modifier = Modifier.height(104.dp))
}
}
}
@Composable
private fun WelcomeLogo() {
Surface(
modifier = Modifier.size(82.dp),
shape = CircleShape,
color = Color.White.copy(alpha = 0.92f),
contentColor = Color.Unspecified,
) {
Box(modifier = Modifier.fillMaxSize().padding(12.dp), contentAlignment = Alignment.Center) {
Image(painter = painterResource(id = R.drawable.openclaw_logo), contentDescription = "OpenClaw logo", modifier = Modifier.fillMaxSize())
}
}
}
@Composable
private fun WelcomeHorizon() {
Canvas(modifier = Modifier.fillMaxWidth().height(120.dp)) {
@@ -428,6 +482,8 @@ private fun GatewaySetupScreen(
token: String,
password: String,
nearbyGatewayName: String?,
discoveryStatusText: String,
discoveryStarted: Boolean,
error: String?,
onBack: () -> Unit,
onScan: () -> Unit,
@@ -442,6 +498,29 @@ private fun GatewaySetupScreen(
modifier: Modifier = Modifier,
) {
var advancedOpen by rememberSaveable { mutableStateOf(false) }
var nearbySearchTimedOut by remember { mutableStateOf(false) }
LaunchedEffect(nearbyGatewayName, discoveryStatusText, discoveryStarted) {
if (!nearbyGatewayName.isNullOrBlank()) {
nearbySearchTimedOut = false
return@LaunchedEffect
}
if (!discoveryStarted) {
nearbySearchTimedOut = false
return@LaunchedEffect
}
nearbySearchTimedOut = false
delay(5_000)
nearbySearchTimedOut = true
}
val nearbyGateway =
nearbyGatewayUiState(
nearbyGatewayName = nearbyGatewayName,
discoveryStatusText = discoveryStatusText,
discoveryStarted = discoveryStarted,
searchTimedOut = nearbySearchTimedOut,
)
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
Column(modifier = Modifier.fillMaxSize().imePadding(), verticalArrangement = Arrangement.SpaceBetween) {
@@ -461,9 +540,9 @@ private fun GatewaySetupScreen(
GatewayOption(
icon = Icons.Default.WifiTethering,
title = "Nearby gateway",
subtitle = nearbyGatewayName ?: "Discovery ready",
status = nearbyGatewayName?.let { "Found" },
onClick = onUseNearby,
subtitle = nearbyGateway.subtitle,
status = nearbyGateway.status,
onClick = onUseNearby.takeIf { nearbyGateway.canConnect },
)
}
item {
@@ -527,20 +606,19 @@ private fun GatewaySetupScreen(
private fun GatewayRecoveryScreen(
statusText: String,
serverName: String?,
attemptedGatewayName: String?,
remoteAddress: String?,
ready: Boolean,
attemptedConnect: Boolean,
gatewayConnectionProblem: GatewayConnectionProblem?,
connectSettling: Boolean,
onAutoRetry: () -> Unit,
onBack: () -> Unit,
onRetry: () -> Unit,
onEdit: () -> Unit,
onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
val recoveryState = gatewayRecoveryUiState(ready = ready, statusText = statusText, connectSettling = connectSettling)
val recoveryState = gatewayRecoveryUiState(ready = ready, statusText = statusText, connectSettling = connectSettling, gatewayConnectionProblem = gatewayConnectionProblem)
val context = LocalContext.current
PairingAutoRetryEffect(enabled = recoveryState.canAutoRetry && attemptedConnect, onRetry = onAutoRetry)
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(18.dp)) {
@@ -551,6 +629,7 @@ private fun GatewayRecoveryScreen(
imageVector =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> Icons.Default.CheckCircle
GatewayRecoveryUiState.ApprovalRequired -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Pairing -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Finishing -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Failed -> Icons.Default.ErrorOutline
@@ -560,6 +639,7 @@ private fun GatewayRecoveryScreen(
tint =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawTheme.colors.success
GatewayRecoveryUiState.ApprovalRequired -> ClawTheme.colors.warning
GatewayRecoveryUiState.Pairing -> ClawTheme.colors.text
GatewayRecoveryUiState.Finishing -> ClawTheme.colors.text
GatewayRecoveryUiState.Failed -> ClawTheme.colors.warning
@@ -577,12 +657,16 @@ private fun GatewayRecoveryScreen(
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Last gateway", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
Text(text = serverName?.takeIf { it.isNotBlank() } ?: "Home Gateway", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = recoveryGatewayDetail(ready = ready, remoteAddress = remoteAddress, statusText = statusText), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
Text(text = recoveryGatewayName(serverName = serverName, attemptedGatewayName = attemptedGatewayName), style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = recoveryGatewayDetail(ready = ready, remoteAddress = remoteAddress, statusText = statusText, gatewayConnectionProblem = gatewayConnectionProblem), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
recoveryGatewayApprovalCommand(gatewayConnectionProblem)?.let { command ->
ApprovalCommandBlock(command = command, onCopy = { copyApprovalCommand(context, command) })
}
ClawStatusPill(
text =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> "Healthy"
GatewayRecoveryUiState.ApprovalRequired -> "Needs approval"
GatewayRecoveryUiState.Pairing -> "Pairing"
GatewayRecoveryUiState.Finishing -> "Connecting"
GatewayRecoveryUiState.Failed -> "Needs attention"
@@ -590,6 +674,7 @@ private fun GatewayRecoveryScreen(
status =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawStatus.Success
GatewayRecoveryUiState.ApprovalRequired -> ClawStatus.Warning
GatewayRecoveryUiState.Pairing -> ClawStatus.Neutral
GatewayRecoveryUiState.Finishing -> ClawStatus.Neutral
GatewayRecoveryUiState.Failed -> ClawStatus.Warning
@@ -606,7 +691,42 @@ private fun GatewayRecoveryScreen(
modifier = Modifier.fillMaxWidth(),
)
OutlinedAction(title = "Edit connection", icon = Icons.Default.Edit, onClick = onEdit)
OutlinedAction(title = "Copy diagnostic", icon = Icons.Default.ContentCopy, onClick = { copyGatewayDiagnostic(context, statusText, serverName, remoteAddress, ready) })
OutlinedAction(title = "Copy diagnostic", icon = Icons.Default.ContentCopy, onClick = { copyGatewayDiagnostic(context, statusText, serverName, remoteAddress, ready, gatewayConnectionProblem) })
}
}
}
}
@Composable
private fun ApprovalCommandBlock(
command: String,
onCopy: () -> Unit,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
color = ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 6.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
SelectionContainer(modifier = Modifier.weight(1f)) {
Text(text = command, style = ClawTheme.type.body.copy(fontFamily = FontFamily.Monospace), color = ClawTheme.colors.text)
}
Surface(
onClick = onCopy,
modifier = Modifier.size(36.dp),
shape = RoundedCornerShape(8.dp),
color = ClawTheme.colors.surfaceRaised,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = "Copy approval command", modifier = Modifier.size(18.dp))
}
}
}
}
@@ -703,7 +823,7 @@ private fun GatewayOption(
icon: ImageVector,
title: String,
subtitle: String,
onClick: () -> Unit,
onClick: (() -> Unit)?,
status: String? = null,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) {
@@ -712,9 +832,12 @@ private fun GatewayOption(
subtitle = subtitle,
metadata = status,
leading = { Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(22.dp), tint = ClawTheme.colors.text) },
trailing = {
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open $title", modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
},
trailing =
onClick?.let {
{
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open $title", modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
}
},
onClick = onClick,
)
}
@@ -890,38 +1013,80 @@ private fun PermissionContinueButton(onClick: () -> Unit) {
internal enum class GatewayRecoveryUiState(
val title: String,
val message: String,
val canAutoRetry: Boolean,
) {
Connected(
title = "Connected",
message = "Your Gateway is ready.",
canAutoRetry = false,
),
ApprovalRequired(
title = "Pairing Gateway",
message = "Approve this phone on the gateway.\nThen retry the connection.",
),
Pairing(
title = "Pairing Gateway",
message = "Approval is in progress.\nOpenClaw will reconnect automatically.",
canAutoRetry = true,
),
Finishing(
title = "Finishing Setup",
message = "Gateway approved this phone.\nOpenClaw is bringing the node online.",
canAutoRetry = true,
title = "Connecting Gateway",
message = "OpenClaw is checking gateway and node access.",
),
Failed(
title = "Connection issue",
message = "We could not reach your Gateway.\nLet's fix this.",
canAutoRetry = false,
),
}
internal data class NearbyGatewayUiState(
val subtitle: String,
val status: String?,
val canConnect: Boolean,
)
/** Maps best-effort discovery into row copy and clickability for onboarding. */
internal fun nearbyGatewayUiState(
nearbyGatewayName: String?,
discoveryStatusText: String,
discoveryStarted: Boolean = true,
searchTimedOut: Boolean = false,
): NearbyGatewayUiState {
val name = nearbyGatewayName?.trim().takeUnless { it.isNullOrEmpty() }
if (name != null) {
return NearbyGatewayUiState(subtitle = name, status = "Found", canConnect = true)
}
if (!discoveryStarted) {
return NearbyGatewayUiState(subtitle = "Starting discovery...", status = "Starting", canConnect = false)
}
val status = discoveryStatusText.trim()
val searching =
status.isEmpty() ||
status.equals("Searching…", ignoreCase = true) ||
status.contains("Searching", ignoreCase = true) ||
status.endsWith("?", ignoreCase = true)
return if (searching) {
if (searchTimedOut) {
NearbyGatewayUiState(subtitle = "No gateway found", status = "Not found", canConnect = false)
} else {
NearbyGatewayUiState(subtitle = "Searching for gateways...", status = "Searching", canConnect = false)
}
} else {
NearbyGatewayUiState(subtitle = "No gateway found", status = "Not found", canConnect = false)
}
}
/** Derives recovery screen state from gateway/node readiness and transient status text. */
internal fun gatewayRecoveryUiState(
ready: Boolean,
statusText: String,
connectSettling: Boolean,
gatewayConnectionProblem: GatewayConnectionProblem? = null,
): GatewayRecoveryUiState =
when {
ready -> GatewayRecoveryUiState.Connected
gatewayConnectionProblem?.isPairingRequired == true &&
!gatewayConnectionProblem.canAutoRetry -> GatewayRecoveryUiState.ApprovalRequired
gatewayConnectionProblem?.isPairingRequired == true -> GatewayRecoveryUiState.Pairing
gatewayConnectionProblem?.pauseReconnect == true -> GatewayRecoveryUiState.Failed
connectSettling -> GatewayRecoveryUiState.Finishing
gatewayStatusLooksLikePairing(statusText) -> GatewayRecoveryUiState.Pairing
gatewayStatusLooksLikePartialConnect(statusText) -> GatewayRecoveryUiState.Finishing
@@ -934,6 +1099,18 @@ internal fun gatewayStatusLooksLikePartialConnect(statusText: String): Boolean {
return lower.contains("operator offline") || lower.contains("node offline")
}
internal fun recoveryGatewayName(
serverName: String?,
attemptedGatewayName: String?,
): String =
serverName
?.trim()
?.takeIf { it.isNotEmpty() }
?: attemptedGatewayName
?.trim()
?.takeIf { it.isNotEmpty() }
?: "Home Gateway"
private data class GatewayConfig(
val host: String,
val port: Int,
@@ -993,11 +1170,16 @@ private fun recoveryGatewayDetail(
ready: Boolean,
remoteAddress: String?,
statusText: String,
gatewayConnectionProblem: GatewayConnectionProblem?,
): String =
remoteAddress
?.takeIf { it.isNotBlank() }
?: if (ready) {
"Ready for chat and voice"
} else if (gatewayConnectionProblem?.isPairingRequired == true && !gatewayConnectionProblem.canAutoRetry) {
recoveryGatewayApprovalCommand(gatewayConnectionProblem)
?.let { "Gateway approval is pending. Run this on the gateway host:" }
?: "Gateway approval is pending. Run openclaw devices list on the gateway host, approve this phone, then retry."
} else if (statusText.contains("operator offline", ignoreCase = true)) {
"Gateway paired. Waiting for operator access."
} else if (gatewayStatusLooksLikePairing(statusText)) {
@@ -1006,6 +1188,25 @@ private fun recoveryGatewayDetail(
"Gateway unreachable"
}
private fun recoveryGatewayApprovalCommand(gatewayConnectionProblem: GatewayConnectionProblem?): String? {
if (gatewayConnectionProblem?.isPairingRequired != true || gatewayConnectionProblem.canAutoRetry) return null
val requestId = gatewayConnectionProblem.requestId?.trim()?.takeIf { it.isNotEmpty() }
return if (requestId != null) {
"openclaw devices approve $requestId"
} else {
"openclaw devices list"
}
}
private fun copyApprovalCommand(
context: Context,
command: String,
) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw pairing approval command", command))
Toast.makeText(context, "Approval command copied", Toast.LENGTH_SHORT).show()
}
/** Copies the onboarding recovery snapshot for support without including credentials. */
private fun copyGatewayDiagnostic(
context: Context,
@@ -1013,11 +1214,16 @@ private fun copyGatewayDiagnostic(
serverName: String?,
remoteAddress: String?,
ready: Boolean,
gatewayConnectionProblem: GatewayConnectionProblem?,
) {
val approvalCommand = recoveryGatewayApprovalCommand(gatewayConnectionProblem)
val diagnostic =
listOf(
listOfNotNull(
"OpenClaw Android gateway diagnostic",
"Status: $statusText",
gatewayConnectionProblem?.message?.let { "Gateway problem: $it" },
gatewayConnectionProblem?.requestId?.let { "Pairing request: $it" },
approvalCommand?.let { "Approval command: $it" },
"Gateway: ${serverName?.takeIf { it.isNotBlank() } ?: "Home Gateway"}",
"Address: ${remoteAddress?.takeIf { it.isNotBlank() } ?: "Not available"}",
"Ready: ${if (ready) "yes" else "no"}",

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.AppearanceThemeMode
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -8,34 +9,51 @@ import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val LocalOpenClawDarkTheme = staticCompositionLocalOf { true }
/**
* App theme wrapper that installs dynamic Material colors and legacy mobile color tokens.
*/
@Composable
fun OpenClawTheme(content: @Composable () -> Unit) {
fun OpenClawTheme(
themeMode: AppearanceThemeMode = AppearanceThemeMode.Dark,
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
val isDark = themeMode.isDark(systemDark = isSystemInDarkTheme())
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
val mobileColors = if (isDark) darkMobileColors() else lightMobileColors()
OpenClawSystemBarAppearance(lightAppearance = !isDark)
CompositionLocalProvider(
LocalMobileColors provides mobileColors,
LocalOpenClawDarkTheme provides isDark,
) {
MaterialTheme(colorScheme = colorScheme, content = content)
}
}
@Composable
internal fun OpenClawSystemBarAppearance(lightAppearance: Boolean) {
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
val window = (view.context as? Activity)?.window ?: return@SideEffect
WindowCompat
.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = !isDark
.isAppearanceLightStatusBars = lightAppearance
WindowCompat
.getInsetsController(window, window.decorView)
.isAppearanceLightNavigationBars = lightAppearance
}
}
CompositionLocalProvider(LocalMobileColors provides mobileColors) {
MaterialTheme(colorScheme = colorScheme, content = content)
}
}
/**
@@ -44,9 +62,9 @@ fun OpenClawTheme(content: @Composable () -> Unit) {
@Composable
fun overlayContainerColor(): Color {
val scheme = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val isDark = LocalOpenClawDarkTheme.current
val base = if (isDark) scheme.surfaceContainerLow else scheme.surfaceContainerHigh
// Light mode: background stays dark (canvas), so clamp overlays away from pure-white glare.
// Light mode keeps overlays away from pure-white glare on the app canvas.
return if (isDark) base else base.copy(alpha = 0.88f)
}

View File

@@ -6,7 +6,6 @@ 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
@@ -17,27 +16,20 @@ 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.WindowInsetsSides
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.only
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.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
@@ -46,25 +38,20 @@ 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
/** Android providers/models browser backed by the gateway catalog. */
/** Android provider readiness screen backed by the configured gateway model view. */
@Composable
internal fun ProvidersModelsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
onAddProvider: () -> Unit,
) {
val isConnected by viewModel.isConnected.collectAsState()
val models by viewModel.modelCatalog.collectAsState()
@@ -72,9 +59,6 @@ internal fun ProvidersModelsScreen(
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) {
@@ -100,12 +84,11 @@ internal fun ProvidersModelsScreen(
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.",
text = "Review provider readiness\nand configured models.",
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textMuted,
)
@@ -119,26 +102,17 @@ internal fun ProvidersModelsScreen(
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.")
ClawEmptyState(title = "Gateway offline", body = "Connect your Gateway to load provider readiness.")
} else {
ProviderList(rows = providerRows, refreshing = refreshing)
}
@@ -151,50 +125,12 @@ internal fun ProvidersModelsScreen(
}
}
}
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(
internal data class ProviderRow(
val id: String,
val name: String,
val status: String,
@@ -202,28 +138,28 @@ private data class ProviderRow(
val modelCount: Int,
)
/** Combines auth-provider readiness rows with catalog-only providers. */
private fun providerRows(
/** Combines gateway auth-provider readiness with configured model providers. */
internal 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,
)
}
// Static/catalog-only providers may expose models without a matching auth
// provider row; keep them visible as ready providers.
val missingAuthRows =
providers
.map { provider ->
val ready = modelProviderReady(provider.status)
ProviderRow(
id = provider.id,
name = provider.displayName,
status = if (ready) "Ready" else "Needs attention",
ready = ready,
modelCount = modelCounts[provider.id] ?: 0,
)
}
val authProviderIds = authRows.mapTo(mutableSetOf()) { it.id.trim().lowercase() }
val configuredModelRows =
modelCounts.keys
.filter { provider -> authRows.none { it.id == provider } }
.filter { provider -> provider.trim().lowercase() !in authProviderIds }
.map { provider ->
ProviderRow(
id = provider,
@@ -233,33 +169,9 @@ private fun providerRows(
modelCount = modelCounts[provider] ?: 0,
)
}
return (authRows + missingAuthRows).sortedWith(compareBy(::providerPriority, { it.name.lowercase() }))
return (authRows + configuredModelRows).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"
}
/** Normalizes gateway provider status strings into a ready/not-ready boolean. */
internal fun modelProviderReady(status: String): Boolean {
val normalized = status.trim().lowercase()
@@ -270,14 +182,6 @@ internal fun modelProviderReady(status: String): Boolean {
normalized == "static"
}
/** Groups models by provider using the same display priority as provider rows. */
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 =
@@ -299,7 +203,15 @@ private fun ProviderList(
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))
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 ->
@@ -320,7 +232,6 @@ private fun ProviderOverviewPanel(
modelCount: Int,
refreshing: Boolean,
onRefresh: () -> Unit,
onSetup: () -> Unit,
) {
val readyCount = providerRows.count { it.ready }
val needsSetupCount = providerRows.count { !it.ready }
@@ -329,17 +240,14 @@ private fun ProviderOverviewPanel(
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))
ProviderMetricTile(label = "Needs", 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.",
text = if (isConnected) "Refresh to recheck provider readiness from your Gateway." else "Connect your Gateway to view provider readiness.",
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))
}
ClawSecondaryButton(text = if (refreshing) "Refreshing" else "Refresh", onClick = onRefresh, enabled = isConnected && !refreshing, modifier = Modifier.fillMaxWidth())
}
}
}
@@ -364,55 +272,13 @@ private fun ProviderMetricTile(
}
}
@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)
Text(text = if (row.modelCount > 0) "${row.modelCount} models" else "No configured models", 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))
@@ -439,78 +305,6 @@ private fun providerInitials(value: String): String =
.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))
}
}
/** Derives compact capability chips for model catalog rows. */
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) {
@@ -538,39 +332,3 @@ private fun ProviderHeaderIconButton(
}
}
}
@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

@@ -217,7 +217,7 @@ private fun SessionRow(
compact: Boolean,
onClick: () -> Unit,
) {
Surface(onClick = onClick, color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Column {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(vertical = 5.dp),

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.AppearanceThemeMode
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.GatewayAgentSummary
import ai.openclaw.app.GatewayCronJobSummary
@@ -8,7 +9,6 @@ import ai.openclaw.app.LocationMode
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.NotificationPackageFilterMode
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.node.DeviceNotificationListenerService
import ai.openclaw.app.ui.design.ClawDetailRow
import ai.openclaw.app.ui.design.ClawIconBadge
@@ -147,7 +147,7 @@ internal fun SettingsDetailScreen(
SettingsRoute.Notifications -> NotificationSettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.PhoneCapabilities -> PhoneCapabilitiesScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.Gateway -> GatewaySettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.Appearance -> AppearanceSettingsScreen(onBack = onBack)
SettingsRoute.Appearance -> AppearanceSettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.Health -> HealthLogsSettingsScreen(viewModel = viewModel, onBack = onBack)
SettingsRoute.About -> AboutSettingsScreen(viewModel = viewModel, onBack = onBack)
}
@@ -897,18 +897,14 @@ private fun GatewaySettingsScreen(
.orEmpty()
.ifEmpty { passwordInput.trim() }
validationText = null
viewModel.setManualEnabled(true)
viewModel.setManualHost(endpointConfig.host)
viewModel.setManualPort(endpointConfig.port)
viewModel.setManualTls(endpointConfig.tls)
viewModel.setGatewayBootstrapToken(bootstrapToken)
viewModel.setGatewayToken(token)
viewModel.setGatewayPassword(password)
viewModel.connect(
GatewayEndpoint.manual(host = endpointConfig.host, port = endpointConfig.port),
token = token.ifEmpty { null },
bootstrapToken = bootstrapToken.ifEmpty { null },
password = password.ifEmpty { null },
viewModel.saveGatewayConfigAndConnect(
host = endpointConfig.host,
port = endpointConfig.port,
tls = endpointConfig.tls,
token = token,
bootstrapToken = bootstrapToken,
password = password,
resetSetupAuth = setup != null,
)
},
modifier = Modifier.fillMaxWidth(),
@@ -919,22 +915,40 @@ private fun GatewaySettingsScreen(
}
@Composable
private fun AppearanceSettingsScreen(onBack: () -> Unit) {
private fun AppearanceSettingsScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
) {
val themeMode by viewModel.appearanceThemeMode.collectAsState()
SettingsDetailFrame(title = "Appearance", subtitle = "A calm, high-contrast OpenClaw interface.", icon = Icons.Default.Palette, onBack = onBack) {
SettingsMetricPanel(
rows =
listOf(
SettingsMetric("Theme", "Dark"),
SettingsMetric("Theme", appearanceThemeSummary(themeMode)),
SettingsMetric("Contrast", "High"),
SettingsMetric("Typography", "Readable"),
),
)
ClawPanel {
Text(text = "OpenClaw uses a fixed premium dark theme so it stays consistent across devices.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Theme", style = ClawTheme.type.section, color = ClawTheme.colors.text)
ClawSegmentedControl(
options = appearanceThemeOptions(),
selected = appearanceThemeSummary(themeMode),
onSelect = { selected -> viewModel.setAppearanceThemeMode(appearanceThemeModeForLabel(selected)) },
)
}
}
}
}
internal fun appearanceThemeSummary(mode: AppearanceThemeMode): String = mode.displayLabel
internal fun appearanceThemeOptions(): List<String> = AppearanceThemeMode.entries.map { it.displayLabel }
internal fun appearanceThemeModeForLabel(label: String): AppearanceThemeMode = AppearanceThemeMode.fromDisplayLabel(label)
/** Converts raw gateway connection text into stable settings metric labels. */
private fun gatewayStatusLabel(
statusText: String,
@@ -971,7 +985,7 @@ private fun AboutSettingsScreen(
listOf(
SettingsMetric("Android App", BuildConfig.VERSION_NAME),
SettingsMetric("Build", BuildConfig.VERSION_CODE.toString()),
SettingsMetric("Channel", "Play"),
SettingsMetric("Channel", androidDistributionChannel()),
SettingsMetric("Gateway", currentGatewayVersion ?: "Not connected"),
),
)
@@ -994,6 +1008,14 @@ private fun AboutSettingsScreen(
}
}
internal fun androidDistributionChannel(flavor: String = BuildConfig.FLAVOR): String =
when (flavor.trim()) {
"play" -> "Play"
"thirdParty" -> "Third-party"
"" -> "Unknown"
else -> flavor.trim()
}
@Composable
private fun AboutStatusRow(
title: String,

View File

@@ -22,6 +22,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -103,7 +104,10 @@ private val shellNavTabs = listOf(Tab.Overview, Tab.Chat, Tab.Voice, Tab.Setting
private val shellContentInsets: WindowInsets
@Composable get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
internal fun shellBottomNavVisible(keyboardVisible: Boolean, commandOpen: Boolean): Boolean = !keyboardVisible && !commandOpen
internal fun shellBottomNavVisible(
keyboardVisible: Boolean,
commandOpen: Boolean,
): Boolean = !keyboardVisible && !commandOpen
/** Main post-onboarding shell that owns top-level Android navigation state. */
@Composable
@@ -111,13 +115,18 @@ fun ShellScreen(
viewModel: MainViewModel,
modifier: Modifier = Modifier,
) {
ClawDesignTheme {
val appearanceThemeMode by viewModel.appearanceThemeMode.collectAsState()
val shellDark = appearanceThemeMode.isDark(systemDark = isSystemInDarkTheme())
OpenClawSystemBarAppearance(lightAppearance = !shellDark)
ClawDesignTheme(dark = shellDark) {
var activeTab by rememberSaveable { mutableStateOf(Tab.Overview) }
var settingsRoute by rememberSaveable { mutableStateOf(SettingsRoute.Home) }
var returnToOverviewFromSettings by rememberSaveable { mutableStateOf(false) }
var commandOpen by rememberSaveable { mutableStateOf(false) }
var voiceScreenWasActive by rememberSaveable { mutableStateOf(false) }
val requestedHomeDestination by viewModel.requestedHomeDestination.collectAsState()
val pendingTrust by viewModel.pendingGatewayTrust.collectAsState()
val runtimeInitialized by viewModel.runtimeInitialized.collectAsState()
LaunchedEffect(requestedHomeDestination) {
val destination = requestedHomeDestination ?: return@LaunchedEffect
@@ -138,8 +147,12 @@ fun ShellScreen(
viewModel.clearRequestedHomeDestination()
}
LaunchedEffect(activeTab) {
viewModel.setVoiceScreenActive(activeTab == Tab.Voice)
LaunchedEffect(activeTab, runtimeInitialized) {
val voiceScreenActive = activeTab == Tab.Voice
if (voiceScreenActive || voiceScreenWasActive || runtimeInitialized) {
viewModel.setVoiceScreenActive(voiceScreenActive)
}
voiceScreenWasActive = voiceScreenActive
}
BackHandler(enabled = activeTab != Tab.Overview) {
@@ -213,11 +226,6 @@ fun ShellScreen(
ProvidersModelsScreen(
viewModel = viewModel,
onBack = { activeTab = Tab.Overview },
onAddProvider = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
)
Tab.Sessions ->
SessionsScreen(
@@ -342,7 +350,7 @@ private fun OverviewScreen(
val cronStatus by viewModel.cronStatus.collectAsState()
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
val channelsSummary by viewModel.channelsSummary.collectAsState()
val readyProviderCount = providers.count { modelProviderReady(it.status) }
val readyProviderCount = providerRows(providers = providers, models = models).count { it.ready }
val attentionRows =
homeAttentionRows(
isConnected = isConnected,
@@ -455,13 +463,12 @@ private fun OverviewScreen(
ModuleRow("Sessions", "Conversation history", if (sessions.isEmpty()) "Empty" else "${sessions.size} recent", Icons.Outlined.AccessTime, Tab.Sessions),
ModuleRow(
title = "Providers & Models",
subtitle = "Model setup",
subtitle = "Provider readiness",
metadata =
when {
!isConnected -> "Offline"
readyProviderCount > 0 -> "$readyProviderCount ready"
models.isNotEmpty() -> "${models.size} models"
else -> "Setup"
else -> "No ready"
},
icon = Icons.Outlined.Inventory2,
tab = Tab.ProvidersModels,
@@ -541,6 +548,7 @@ internal fun homeAttentionRows(
channelsSummary: GatewayChannelsSummary,
nodesDevicesSummary: GatewayNodesDevicesSummary,
readyProviderCount: Int,
expiringProviderCount: Int = 0,
): List<HomeAttentionRow> =
listOfNotNull(
if (!isConnected) {
@@ -564,7 +572,7 @@ internal fun homeAttentionRows(
null
},
if (isConnected && readyProviderCount == 0) {
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.Settings, SettingsRoute.Gateway)
} else {
null
},
@@ -747,7 +755,7 @@ private fun RecentSessionRowContent(
metadata: String,
onClick: () -> Unit,
) {
Surface(color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
Surface(color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier =
Modifier
@@ -845,6 +853,7 @@ private fun SettingsShellScreen(
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
val channelsSummary by viewModel.channelsSummary.collectAsState()
val dreamingSummary by viewModel.dreamingSummary.collectAsState()
val appearanceThemeMode by viewModel.appearanceThemeMode.collectAsState()
LaunchedEffect(isConnected) {
if (isConnected) {
@@ -906,7 +915,7 @@ private fun SettingsShellScreen(
SettingsRow("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
SettingsRow("Appearance", "Dark", Icons.Default.Palette, route = SettingsRoute.Appearance),
SettingsRow("Appearance", appearanceThemeSummary(appearanceThemeMode), Icons.Default.Palette, route = SettingsRoute.Appearance),
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
),

View File

@@ -97,6 +97,7 @@ fun VoiceScreen(
val talkModeEnabled by viewModel.talkModeEnabled.collectAsState()
val talkModeListening by viewModel.talkModeListening.collectAsState()
val talkModeSpeaking by viewModel.talkModeSpeaking.collectAsState()
val talkModeStatusText by viewModel.talkModeStatusText.collectAsState()
val talkModeConversation by viewModel.talkModeConversation.collectAsState()
var pendingAction by remember { mutableStateOf<VoiceAction?>(null) }
@@ -119,6 +120,16 @@ fun VoiceScreen(
val activeConversation = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) talkModeConversation else micConversation
val voiceActive = micEnabled || micIsSending || talkModeEnabled
val gatewayReady = gatewayStatus.isVoiceGatewayReady()
val voiceAttentionStatus =
voiceAttentionStatus(
talkModeStatusText = talkModeStatusText,
voiceCaptureMode = voiceCaptureMode,
micEnabled = micEnabled,
micIsSending = micIsSending,
talkModeEnabled = talkModeEnabled,
talkModeListening = talkModeListening,
talkModeSpeaking = talkModeSpeaking,
)
val activeStatus =
voiceStatusLabel(
gatewayStatus = gatewayStatus,
@@ -128,6 +139,7 @@ fun VoiceScreen(
micIsSending = micIsSending,
talkModeListening = talkModeListening,
talkModeSpeaking = talkModeSpeaking,
voiceAttentionStatus = voiceAttentionStatus,
)
if (talkModeEnabled) {
@@ -169,7 +181,7 @@ fun VoiceScreen(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
VoiceHeader(
statusText = if (voiceActive || !gatewayReady) activeStatus else "Your voice command center.",
statusText = voiceAttentionStatus ?: if (voiceActive || !gatewayReady) activeStatus else "Your voice command center.",
speakerEnabled = speakerEnabled,
onToggleSpeaker = { viewModel.setSpeakerEnabled(!speakerEnabled) },
onOpenCommand = onOpenCommand,
@@ -184,6 +196,7 @@ fun VoiceScreen(
talkModeSpeaking = talkModeSpeaking,
micLiveTranscript = micLiveTranscript,
gatewayReady = gatewayReady,
voiceAttentionStatus = voiceAttentionStatus,
onStartTalk = {
runVoiceAction(
action = VoiceAction.Talk,
@@ -242,7 +255,9 @@ private fun DictationScreen(
) {
val lastUserText = conversation.lastOrNull { it.role == VoiceConversationRole.User }?.text
val draftText = liveTranscript?.takeIf { it.isNotBlank() } ?: lastUserText.orEmpty()
val speechProviderReady = gatewayStatus.isVoiceGatewayReady()
val providerAttentionStatus = voiceRuntimeAttentionStatus(statusText)
val displayStatusText = providerAttentionStatus ?: statusText
val speechProviderReady = providerAttentionStatus == null && gatewayStatus.isVoiceGatewayReady()
Column(
modifier =
Modifier
@@ -278,7 +293,7 @@ private fun DictationScreen(
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)
Text(text = displayStatusText, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
}
@@ -298,13 +313,20 @@ private fun DictationScreen(
}
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)
Text(
text = providerAttentionStatus ?: 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"
providerAttentionStatus != null -> "Attention"
speechProviderReady -> "Ready"
else -> "Offline"
},
@@ -312,6 +334,7 @@ private fun DictationScreen(
color =
when {
sending -> ClawTheme.colors.warning
providerAttentionStatus != null -> ClawTheme.colors.warning
speechProviderReady -> ClawTheme.colors.success
else -> ClawTheme.colors.textMuted
},
@@ -324,6 +347,7 @@ private fun DictationScreen(
.background(
when {
sending -> ClawTheme.colors.warning
providerAttentionStatus != null -> ClawTheme.colors.warning
speechProviderReady -> ClawTheme.colors.success
else -> ClawTheme.colors.textSubtle
},
@@ -594,6 +618,7 @@ private fun VoiceHero(
talkModeSpeaking: Boolean,
micLiveTranscript: String?,
gatewayReady: Boolean,
voiceAttentionStatus: String?,
onStartTalk: () -> Unit,
onStartDictation: () -> Unit,
onConnectGateway: () -> Unit,
@@ -616,6 +641,7 @@ private fun VoiceHero(
Text(
text =
when {
voiceAttentionStatus != null -> voiceAttentionStatus
talkModeSpeaking -> "OpenClaw is replying"
talkModeListening -> "Listening"
talkModeEnabled -> "Talk is live"
@@ -672,7 +698,7 @@ private fun VoiceHero(
)
}
VoiceProviderCard(gatewayStatus = gatewayStatus)
VoiceProviderCard(gatewayStatus = gatewayStatus, voiceAttentionStatus = voiceAttentionStatus)
VoicePrimaryAction(
text =
@@ -734,8 +760,11 @@ private fun VoiceModeRow(
}
@Composable
private fun VoiceProviderCard(gatewayStatus: String) {
val ready = gatewayStatus.isVoiceGatewayReady()
private fun VoiceProviderCard(
gatewayStatus: String,
voiceAttentionStatus: String?,
) {
val ready = voiceAttentionStatus == null && gatewayStatus.isVoiceGatewayReady()
Surface(
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp),
shape = RoundedCornerShape(ClawTheme.radii.panel),
@@ -761,7 +790,13 @@ private fun VoiceProviderCard(gatewayStatus: String) {
}
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)
Text(
text = voiceAttentionStatus ?: 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(
@@ -769,9 +804,25 @@ private fun VoiceProviderCard(gatewayStatus: String) {
Modifier
.size(7.dp)
.clip(CircleShape)
.background(if (ready) ClawTheme.colors.success else ClawTheme.colors.textSubtle),
.background(
when {
ready -> ClawTheme.colors.success
voiceAttentionStatus != null -> ClawTheme.colors.warning
else -> ClawTheme.colors.textSubtle
},
),
)
Text(
text =
when {
ready -> "Ready"
voiceAttentionStatus != null -> "Attention"
else -> "Offline"
},
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
maxLines = 1,
)
Text(text = if (ready) "Ready" else "Offline", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@@ -968,7 +1019,7 @@ private fun runVoiceAction(
}
}
private fun voiceStatusLabel(
internal fun voiceStatusLabel(
gatewayStatus: String,
voiceCaptureMode: VoiceCaptureMode,
micStatusText: String,
@@ -976,8 +1027,10 @@ private fun voiceStatusLabel(
micIsSending: Boolean,
talkModeListening: Boolean,
talkModeSpeaking: Boolean,
voiceAttentionStatus: String?,
): String =
when {
voiceAttentionStatus != null -> voiceAttentionStatus
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeSpeaking -> "OpenClaw is speaking"
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeListening -> "Listening"
voiceCaptureMode == VoiceCaptureMode.TalkMode -> "Talk is live"
@@ -988,6 +1041,69 @@ private fun voiceStatusLabel(
else -> "Ready to talk"
}
internal fun voiceAttentionStatus(
talkModeStatusText: String,
voiceCaptureMode: VoiceCaptureMode,
micEnabled: Boolean,
micIsSending: Boolean,
talkModeEnabled: Boolean,
talkModeListening: Boolean,
talkModeSpeaking: Boolean,
): String? {
if (voiceCaptureMode != VoiceCaptureMode.Off || micEnabled || micIsSending) return null
if (talkModeEnabled || talkModeListening || talkModeSpeaking) return null
val status = talkModeStatusText.trim()
if (status.isBlank()) return null
val lower = status.lowercase()
if (lower == "off" || lower == "ready" || lower == "listening" || lower == "connecting…") return null
return status
.takeIf {
lower.contains("failed") ||
lower.contains("unavailable") ||
lower.contains("permission required") ||
lower.contains("not connected") ||
lower.contains("error")
}?.let(::userFacingVoiceAttentionStatus)
}
internal fun voiceRuntimeAttentionStatus(statusText: String): String? {
val status = statusText.trim()
if (status.isBlank()) return null
val lower = status.lowercase()
return status
.takeIf {
lower.contains("transcription unavailable") ||
lower.contains("provider unavailable") ||
(lower.contains("provider") && lower.contains("not configured")) ||
lower.contains("no realtime transcription provider") ||
lower.contains("failed")
}?.let(::userFacingVoiceAttentionStatus)
}
private fun userFacingVoiceAttentionStatus(status: String): String {
val normalized =
status
.removePrefix("Start failed:")
.trim()
.removePrefix("Transcription unavailable:")
.trim()
.removePrefix("UNAVAILABLE:")
.trim()
.removePrefix("Error:")
.trim()
val lower = normalized.lowercase()
if (lower.contains("realtime voice provider") && lower.contains("not configured")) {
return "Realtime voice provider is not configured."
}
if (lower.contains("no realtime transcription provider")) {
return "Realtime transcription provider is not configured."
}
if (lower.contains("microphone permission required")) {
return "Microphone permission is required."
}
return if (normalized.length <= 90) normalized else "${normalized.take(87)}..."
}
private fun String.isVoiceGatewayReady(): Boolean {
val status = lowercase()
return !status.contains("offline") && !status.contains("not connected") && !status.contains("failed") && !status.contains("error")

View File

@@ -15,7 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
@@ -40,17 +40,19 @@ fun ChatMessageListCard(
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val displayMessages = remember(messages) { messages.asReversed() }
val stream = streamingAssistantText?.trim()
val timeline =
remember(messages, pendingRunCount, pendingToolCalls, streamingAssistantText) {
buildChatTimeline(
messages = messages,
pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText,
)
}
// New list items/tool rows should animate into view, but token streaming should not restart
// that animation on every delta.
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
listState.animateScrollToItem(index = 0)
}
LaunchedEffect(stream) {
if (!stream.isNullOrEmpty()) {
listState.scrollToItem(index = 0)
LaunchedEffect(timeline.scrollTargetIndex, timeline.items.size, pendingRunCount, pendingToolCalls.size) {
timeline.scrollTargetIndex?.let { index ->
listState.animateScrollToItem(index = index)
}
}
@@ -64,32 +66,17 @@ fun ChatMessageListCard(
androidx.compose.foundation.layout
.PaddingValues(bottom = 8.dp),
) {
// With reverseLayout = true, index 0 renders at the BOTTOM.
// So we emit newest items first: streaming → tools → typing → messages (newest→oldest).
if (!stream.isNullOrEmpty()) {
item(key = "stream") {
ChatStreamingAssistantBubble(text = stream)
itemsIndexed(items = timeline.items, key = { _, item -> chatTimelineItemKey(item) }) { _, item ->
when (item) {
is ChatTimelineItem.Message -> ChatMessageBubble(message = item.message)
is ChatTimelineItem.PendingTools -> ChatPendingToolsBubble(toolCalls = item.toolCalls)
is ChatTimelineItem.StreamingAssistant -> ChatStreamingAssistantBubble(text = item.text)
ChatTimelineItem.Thinking -> ChatTypingIndicatorBubble()
}
}
if (pendingToolCalls.isNotEmpty()) {
item(key = "tools") {
ChatPendingToolsBubble(toolCalls = pendingToolCalls)
}
}
if (pendingRunCount > 0) {
item(key = "typing") {
ChatTypingIndicatorBubble()
}
}
items(items = displayMessages, key = { it.id }) { message ->
ChatMessageBubble(message = message)
}
}
if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && streamingAssistantText.isNullOrBlank()) {
if (timeline.items.isEmpty()) {
if (historyLoading) {
LoadingChatHint(modifier = Modifier.align(Alignment.Center))
} else {

View File

@@ -31,7 +31,7 @@ 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.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -406,15 +406,19 @@ private fun ChatMessageList(
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val displayMessages = remember(messages) { messages.asReversed() }
val stream = streamingAssistantText?.trim()
val timeline =
remember(messages, pendingRunCount, pendingToolCalls, streamingAssistantText) {
buildChatTimeline(
messages = messages,
pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText,
)
}
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
listState.animateScrollToItem(index = 0)
}
LaunchedEffect(stream) {
if (!stream.isNullOrEmpty()) {
listState.scrollToItem(index = 0)
LaunchedEffect(timeline.scrollTargetIndex, timeline.items.size, pendingRunCount, pendingToolCalls.size) {
timeline.scrollTargetIndex?.let { index ->
listState.animateScrollToItem(index = index)
}
}
@@ -426,30 +430,29 @@ private fun ChatMessageList(
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)
itemsIndexed(items = timeline.items, key = { _, item -> chatTimelineItemKey(item) }) { _, item ->
when (item) {
is ChatTimelineItem.Message ->
ChatBubble(
role = item.message.role,
live = false,
content = item.message.content,
timestampMs = item.message.timestampMs,
)
is ChatTimelineItem.PendingTools -> ToolBubble(toolCalls = item.toolCalls)
is ChatTimelineItem.StreamingAssistant ->
ChatBubble(
role = "assistant",
live = true,
content = listOf(ChatMessageContent(text = item.text)),
timestampMs = null,
)
ChatTimelineItem.Thinking -> ChatThinkingBubble()
}
}
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()) {
if (timeline.items.isEmpty()) {
if (historyLoading) {
ClawLoadingState(title = "Loading session", modifier = Modifier.align(Alignment.Center))
} else {

View File

@@ -0,0 +1,69 @@
package ai.openclaw.app.ui.chat
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
internal sealed class ChatTimelineItem {
data class Message(
val message: ChatMessage,
) : ChatTimelineItem()
data class StreamingAssistant(
val text: String,
) : ChatTimelineItem()
data class PendingTools(
val toolCalls: List<ChatPendingToolCall>,
) : ChatTimelineItem()
object Thinking : ChatTimelineItem()
}
internal data class ChatTimeline(
val items: List<ChatTimelineItem>,
val scrollTargetIndex: Int?,
)
internal fun buildChatTimeline(
messages: List<ChatMessage>,
pendingRunCount: Int,
pendingToolCalls: List<ChatPendingToolCall>,
streamingAssistantText: String?,
): ChatTimeline {
val stream = streamingAssistantText?.trim()?.takeIf { it.isNotEmpty() }
val hasActiveRun = pendingRunCount > 0 || pendingToolCalls.isNotEmpty() || stream != null
val items =
buildList {
if (stream != null) add(ChatTimelineItem.StreamingAssistant(stream))
if (pendingToolCalls.isNotEmpty()) add(ChatTimelineItem.PendingTools(pendingToolCalls))
if (pendingRunCount > 0) add(ChatTimelineItem.Thinking)
messages.asReversed().forEach { message -> add(ChatTimelineItem.Message(message)) }
}
if (items.isEmpty()) return ChatTimeline(items = items, scrollTargetIndex = null)
// In reverseLayout, index 0 is bottom-most. During an active run, keep the prompt
// anchored so streaming/tool rows do not immediately push the just-sent message away.
val activePromptIndex =
if (hasActiveRun) {
items.indexOfFirst { item ->
item is ChatTimelineItem.Message &&
item.message.role
.trim()
.equals("user", ignoreCase = true)
}
} else {
-1
}
return ChatTimeline(
items = items,
scrollTargetIndex = activePromptIndex.takeIf { it >= 0 } ?: 0,
)
}
internal fun chatTimelineItemKey(item: ChatTimelineItem): String =
when (item) {
is ChatTimelineItem.Message -> "message:${item.message.id}"
is ChatTimelineItem.PendingTools -> "tools"
is ChatTimelineItem.StreamingAssistant -> "stream"
ChatTimelineItem.Thinking -> "thinking"
}

View File

@@ -82,7 +82,12 @@ fun resolveCompactSessionChoices(
)
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
val current = currentSessionKey.trim().let { if (it == "main" && mainKey != "main") mainKey else it }
val pinnedRank = listOf(mainKey, current).filter { it.isNotBlank() }.distinct().withIndex().associate { it.value to it.index }
val pinnedRank =
listOf(mainKey, current)
.filter { it.isNotBlank() }
.distinct()
.withIndex()
.associate { it.value to it.index }
val unpinnedRank = pinnedRank.size
return allChoices

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app.ui.design
import ai.openclaw.app.ui.LocalMobileColors
import ai.openclaw.app.ui.darkMobileColors
import ai.openclaw.app.ui.lightMobileColors
import ai.openclaw.app.ui.mobileFontFamily
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -110,22 +113,22 @@ private val ClawDarkColors =
private val ClawLightColors =
ClawColors(
canvas = Color(0xFFF7F7F7),
surface = Color(0xFFFFFFFF),
canvas = Color(0xFFFAFBFC),
surface = Color(0xFFFFFEFB),
surfaceRaised = Color(0xFFFFFFFF),
surfacePressed = Color(0xFFEDEDED),
border = Color(0xFFE0E0E0),
borderStrong = Color(0xFFBDBDBD),
text = Color(0xFF070707),
textMuted = Color(0xFF595959),
textSubtle = Color(0xFF8A8A8A),
primary = Color(0xFF050505),
surfacePressed = Color(0xFFE9EDF3),
border = Color(0xFFDDE3EC),
borderStrong = Color(0xFFC7D0DC),
text = Color(0xFF111318),
textMuted = Color(0xFF505865),
textSubtle = Color(0xFF8993A2),
primary = Color(0xFF111827),
primaryText = Color(0xFFFFFFFF),
success = Color(0xFF157A3E),
successSoft = Color(0xFFEAF8EF),
warning = Color(0xFF9A6A12),
warningSoft = Color(0xFFFFF5DD),
danger = Color(0xFFB42323),
success = Color(0xFF217747),
successSoft = Color(0xFFE9F7EF),
warning = Color(0xFFA56F17),
warningSoft = Color(0xFFFFF3DC),
danger = Color(0xFFB82929),
dangerSoft = Color(0xFFFFE9E9),
)
@@ -168,10 +171,12 @@ internal fun ClawDesignTheme(
content: @Composable () -> Unit,
) {
val colors = if (dark) ClawDarkColors else ClawLightColors
val mobileColors = if (dark) darkMobileColors() else lightMobileColors()
val typography = clawTypography(mobileFontFamily)
CompositionLocalProvider(
LocalClawColors provides colors,
LocalMobileColors provides mobileColors,
LocalClawSpacing provides ClawSpacing(),
LocalClawRadii provides ClawRadii(),
LocalClawTypography provides typography,

View File

@@ -104,6 +104,7 @@ class MicCaptureManager(
private val messageQueue = ArrayDeque<String>()
private val messageQueueLock = Any()
private var flushedPartialTranscript: String? = null
// Correlates chat events with the idempotency key generated before sendChat returns.
private var pendingRunId: String? = null
private var pendingAssistantEntryId: String? = null

View File

@@ -168,6 +168,7 @@ class TalkModeManager internal constructor(
@Volatile private var realtimeSessionId: String? = null
private var realtimeCaptureJob: Job? = null
private var realtimeAppendJob: Job? = null
// Realtime tool calls can complete before their chat final arrives; cache by call/run id until both sides meet.
private val realtimeToolRuns = LinkedHashMap<String, RealtimeToolRun>()
private val pendingRealtimeToolCalls = LinkedHashSet<String>()

View File

@@ -0,0 +1,67 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="120dp"
android:height="120dp"
android:viewportWidth="120"
android:viewportHeight="120">
<path android:pathData="M60,10 C30,10 15,35 15,55 C15,75 30,95 45,100 L45,110 L55,110 L55,100 C55,100 60,102 65,100 L65,110 L75,110 L75,100 C90,95 105,75 105,55 C105,35 90,10 60,10Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="120"
android:endY="120"
android:startColor="#ff4d4d"
android:startX="0"
android:startY="0"
android:type="linear"
android:endColor="#991b1b" />
</aapt:attr>
</path>
<path android:pathData="M20,45 C5,40 0,50 5,60 C10,70 20,65 25,55 C28,48 25,45 20,45Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="120"
android:endY="120"
android:startColor="#ff4d4d"
android:startX="0"
android:startY="0"
android:type="linear"
android:endColor="#991b1b" />
</aapt:attr>
</path>
<path android:pathData="M100,45 C115,40 120,50 115,60 C110,70 100,65 95,55 C92,48 95,45 100,45Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="120"
android:endY="120"
android:startColor="#ff4d4d"
android:startX="0"
android:startY="0"
android:type="linear"
android:endColor="#991b1b" />
</aapt:attr>
</path>
<path
android:fillColor="@android:color/transparent"
android:pathData="M45,15 Q35,5 30,8"
android:strokeColor="#ff4d4d"
android:strokeLineCap="round"
android:strokeWidth="3" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M75,15 Q85,5 90,8"
android:strokeColor="#ff4d4d"
android:strokeLineCap="round"
android:strokeWidth="3" />
<path
android:fillColor="#050810"
android:pathData="M45,35 m-6,0 a6,6 0,1 0,12 0 a6,6 0,1 0,-12 0" />
<path
android:fillColor="#050810"
android:pathData="M75,35 m-6,0 a6,6 0,1 0,12 0 a6,6 0,1 0,-12 0" />
<path
android:fillColor="#00e5cc"
android:pathData="M46,34 m-2.5,0 a2.5,2.5 0,1 0,5 0 a2.5,2.5 0,1 0,-5 0" />
<path
android:fillColor="#00e5cc"
android:pathData="M76,34 m-2.5,0 a2.5,2.5 0,1 0,5 0 a2.5,2.5 0,1 0,-5 0" />
</vector>

View File

@@ -294,6 +294,38 @@ class GatewayBootstrapAuthTest {
assertEquals("aaaaaaaa", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
}
@Test
fun refreshGatewayConnection_reconnectsSavedManualEndpointAfterDisconnect() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
android.content.Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
prefs.setManualEnabled(true)
prefs.setManualHost("127.0.0.1")
prefs.setManualPort(18789)
prefs.setManualTls(false)
prefs.setGatewayToken("shared-token")
val runtime = NodeRuntime(app, prefs)
runtime.connect(
GatewayEndpoint.manual(host = "127.0.0.1", port = 18789),
NodeRuntime.GatewayConnectAuth(token = "initial-token", bootstrapToken = null, password = null),
)
runtime.disconnect()
assertNull(desiredConnection(runtime, "nodeSession"))
runtime.refreshGatewayConnection()
val desired = desiredConnection(runtime, "nodeSession") ?: error("Expected desired node connection")
val endpoint = readField<GatewayEndpoint>(desired, "endpoint")
assertEquals("127.0.0.1", endpoint.host)
assertEquals(18789, endpoint.port)
assertEquals("shared-token", readField<String?>(desired, "token"))
}
@Test
fun connect_showsSecureEndpointGuidanceWhenTlsProbeFails() {
val app = RuntimeEnvironment.getApplication()

View File

@@ -34,15 +34,15 @@ class NodeForegroundServiceTest {
@Test
fun foregroundServiceTypesForVoiceMode_addsMicrophoneOnlyForTalkMode() {
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.Off),
)
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.ManualMic),
)
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.TalkMode),
)
}

View File

@@ -77,6 +77,31 @@ class SecurePrefsTest {
assertTrue(plainPrefs.getBoolean("device.apps.sharing.enabled", false))
}
@Test
fun appearanceThemeMode_defaultsDarkForExistingInstalls() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().commit()
val prefs = SecurePrefs(context)
assertEquals(AppearanceThemeMode.Dark, prefs.appearanceThemeMode.value)
assertFalse(plainPrefs.contains("appearance.themeMode"))
}
@Test
fun setAppearanceThemeMode_persistsSelectedMode() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().commit()
val prefs = SecurePrefs(context)
prefs.setAppearanceThemeMode(AppearanceThemeMode.Light)
assertEquals(AppearanceThemeMode.Light, prefs.appearanceThemeMode.value)
assertEquals("light", plainPrefs.getString("appearance.themeMode", null))
assertEquals(AppearanceThemeMode.Light, SecurePrefs(context).appearanceThemeMode.value)
}
@Test
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
val context = RuntimeEnvironment.getApplication()

View File

@@ -1,10 +1,44 @@
package ai.openclaw.app.chat
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
class ChatControllerMessageIdentityTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun parseChatMessageContentsReadsGatewayStringContent() {
val obj =
json
.parseToJsonElement(
"""
{"role":"user","content":"Hello","idempotencyKey":"run-1:user"}
""".trimIndent(),
).jsonObject
val content = parseChatMessageContents(obj)
assertEquals(listOf(ChatMessageContent(type = "text", text = "Hello")), content)
}
@Test
fun parseChatMessageContentsFallsBackToTopLevelText() {
val obj =
json
.parseToJsonElement(
"""
{"role":"assistant","text":"Hi there"}
""".trimIndent(),
).jsonObject
val content = parseChatMessageContents(obj)
assertEquals(listOf(ChatMessageContent(type = "text", text = "Hi there")), content)
}
@Test
fun reconcileMessageIdsReusesMatchingIdsAcrossHistoryReload() {
val previous =
@@ -101,6 +135,62 @@ class ChatControllerMessageIdentityTest {
assertEquals(listOf("local-user", "remote-assistant"), merged.map { it.id })
}
@Test
fun retainUnmatchedOptimisticMessagesKeepsOutgoingUserTurnWhenHistoryOmitsIt() {
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 retained = retainUnmatchedOptimisticMessages(incoming = listOf(assistant), optimistic = listOf(optimistic))
assertEquals(listOf("local-user"), retained.map { it.id })
}
@Test
fun retainUnmatchedOptimisticMessagesDropsGatewayPersistedUserTurn() {
val optimistic =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "hello")),
timestampMs = 1000L,
idempotencyKey = "run-1:user",
)
val remoteUser = optimistic.copy(id = "remote-user", timestampMs = 500L)
val retained = retainUnmatchedOptimisticMessages(incoming = listOf(remoteUser), optimistic = listOf(optimistic))
assertEquals(emptyList<String>(), retained.map { it.id })
}
@Test
fun retainUnmatchedOptimisticMessagesKeepsDistinctIdempotencyKey() {
val optimistic =
ChatMessage(
id = "local-user",
role = "user",
content = listOf(ChatMessageContent(type = "text", text = "hello")),
timestampMs = 1000L,
idempotencyKey = "run-2:user",
)
val remoteUser = optimistic.copy(id = "remote-user", timestampMs = 2000L, idempotencyKey = "run-1:user")
val retained = retainUnmatchedOptimisticMessages(incoming = listOf(remoteUser), optimistic = listOf(optimistic))
assertEquals(listOf("local-user"), retained.map { it.id })
}
@Test
fun mergeOptimisticMessagesDoesNotDuplicateHistoryTurns() {
val user =

View File

@@ -20,6 +20,7 @@ import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@@ -233,7 +234,75 @@ class GatewaySessionReconnectTest {
)
}
private fun createReconnectHarness(): ReconnectHarness {
@Test
fun pairingRequiredFailureNotifiesPauseReconnectProblem() =
runBlocking {
val json = Json { ignoreUnknownKeys = true }
val connectFailure = CompletableDeferred<Pair<GatewaySession.ErrorShape, Boolean>>()
val server =
startGatewayServer(json = json) { webSocket, id, method ->
if (method == "connect") {
webSocket.send(
"""
{"type":"res","id":"$id","ok":false,"error":{"code":"NOT_PAIRED","message":"pairing required: device approval is required","details":{"code":"PAIRING_REQUIRED","reason":"not-paired","requestId":"request-1"}}}
""".trimIndent(),
)
}
}
val harness =
createReconnectHarness { error, pauseReconnect ->
connectFailure.complete(error to pauseReconnect)
}
try {
connectNodeSession(harness.session, server.port)
val (error, pauseReconnect) = withTimeout(LIFECYCLE_TEST_TIMEOUT_MS) { connectFailure.await() }
assertEquals("PAIRING_REQUIRED", error.details?.code)
assertEquals("not-paired", error.details?.reason)
assertEquals("request-1", error.details?.requestId)
assertTrue(pauseReconnect)
} finally {
shutdownReconnectHarness(harness, server)
}
}
@Test
fun pairingRequiredFailureDropsUnsafeRequestId() =
runBlocking {
val json = Json { ignoreUnknownKeys = true }
val connectFailure = CompletableDeferred<Pair<GatewaySession.ErrorShape, Boolean>>()
val server =
startGatewayServer(json = json) { webSocket, id, method ->
if (method == "connect") {
webSocket.send(
"""
{"type":"res","id":"$id","ok":false,"error":{"code":"NOT_PAIRED","message":"pairing required: device approval is required","details":{"code":"PAIRING_REQUIRED","reason":"not-paired","requestId":"request-1;echo unsafe"}}}
""".trimIndent(),
)
}
}
val harness =
createReconnectHarness { error, pauseReconnect ->
connectFailure.complete(error to pauseReconnect)
}
try {
connectNodeSession(harness.session, server.port)
val (error, pauseReconnect) = withTimeout(LIFECYCLE_TEST_TIMEOUT_MS) { connectFailure.await() }
assertEquals("PAIRING_REQUIRED", error.details?.code)
assertEquals("not-paired", error.details?.reason)
assertNull(error.details?.requestId)
assertTrue(pauseReconnect)
} finally {
shutdownReconnectHarness(harness, server)
}
}
private fun createReconnectHarness(
onConnectFailure: (GatewaySession.ErrorShape, Boolean) -> Unit = { _, _ -> },
): ReconnectHarness {
val app = RuntimeEnvironment.getApplication()
val sessionJob = SupervisorJob()
val session =
@@ -243,6 +312,7 @@ class GatewaySessionReconnectTest {
deviceAuthStore = ReconnectDeviceAuthStore(),
onConnected = {},
onDisconnected = { _ -> },
onConnectFailure = onConnectFailure,
onEvent = { _, _ -> },
onInvoke = { GatewaySession.InvokeResult.ok("""{"handled":true}""") },
)

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayConnectionProblem
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -26,6 +27,53 @@ class OnboardingFlowLogicTest {
assertTrue(canFinishOnboarding(isConnected = true, isNodeConnected = true))
}
@Test
fun nearbyGatewayFoundStateIsConnectable() {
assertEquals(
NearbyGatewayUiState(subtitle = "Studio Gateway", status = "Found", canConnect = true),
nearbyGatewayUiState(nearbyGatewayName = "Studio Gateway", discoveryStatusText = "Searching…", discoveryStarted = false),
)
}
@Test
fun nearbyGatewayBeforeDiscoveryStartsIsNotConnectable() {
assertEquals(
NearbyGatewayUiState(subtitle = "Starting discovery...", status = "Starting", canConnect = false),
nearbyGatewayUiState(nearbyGatewayName = null, discoveryStatusText = "Searching…", discoveryStarted = false, searchTimedOut = true),
)
}
@Test
fun nearbyGatewaySearchingStateIsNotConnectable() {
assertEquals(
NearbyGatewayUiState(subtitle = "Searching for gateways...", status = "Searching", canConnect = false),
nearbyGatewayUiState(nearbyGatewayName = null, discoveryStatusText = "Searching for gateways…"),
)
}
@Test
fun nearbyGatewayTimedOutSearchShowsEmptyState() {
assertEquals(
NearbyGatewayUiState(subtitle = "No gateway found", status = "Not found", canConnect = false),
nearbyGatewayUiState(nearbyGatewayName = null, discoveryStatusText = "Searching for gateways…", searchTimedOut = true),
)
}
@Test
fun nearbyGatewayEmptyResultStateIsNotConnectable() {
assertEquals(
NearbyGatewayUiState(subtitle = "No gateway found", status = "Not found", canConnect = false),
nearbyGatewayUiState(nearbyGatewayName = null, discoveryStatusText = "Local: 0 • Wide: 0"),
)
}
@Test
fun recoveryGatewayNamePrefersServerThenAttemptedGateway() {
assertEquals("Server Gateway", recoveryGatewayName(serverName = "Server Gateway", attemptedGatewayName = "Discovered Gateway"))
assertEquals("Discovered Gateway", recoveryGatewayName(serverName = null, attemptedGatewayName = "Discovered Gateway"))
assertEquals("Home Gateway", recoveryGatewayName(serverName = " ", attemptedGatewayName = " "))
}
@Test
fun showsPairingStateForPairingRequiredGatewayStatus() {
assertEquals(
@@ -50,6 +98,50 @@ class OnboardingFlowLogicTest {
)
}
@Test
fun showsApprovalRequiredForPausedPairingProblem() {
assertEquals(
GatewayRecoveryUiState.ApprovalRequired,
gatewayRecoveryUiState(
ready = false,
statusText = "Connecting…",
connectSettling = false,
gatewayConnectionProblem =
GatewayConnectionProblem(
code = "PAIRING_REQUIRED",
message = "pairing required: device approval is required",
reason = "not-paired",
requestId = "request-1",
recommendedNextStep = null,
pauseReconnect = true,
retryable = false,
),
),
)
}
@Test
fun showsPairingForRetryablePairingProblem() {
assertEquals(
GatewayRecoveryUiState.Pairing,
gatewayRecoveryUiState(
ready = false,
statusText = "Connecting…",
connectSettling = false,
gatewayConnectionProblem =
GatewayConnectionProblem(
code = "PAIRING_REQUIRED",
message = "pairing required: device approval is required",
reason = "not-paired",
requestId = "request-1",
recommendedNextStep = "wait_then_retry",
pauseReconnect = false,
retryable = true,
),
),
)
}
@Test
fun showsFinishingStateWhileGatewayConnectionSettles() {
assertEquals(

View File

@@ -1,5 +1,8 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayModelProviderSummary
import ai.openclaw.app.GatewayModelSummary
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -10,8 +13,55 @@ class ProviderModelStatusTest {
assertTrue(modelProviderReady("static"))
}
@Test
fun expiringProviderStatusIsNotFullyReady() {
assertFalse(modelProviderReady("expiring"))
}
@Test
fun missingProviderStatusIsNotReady() {
assertFalse(modelProviderReady("missing"))
}
@Test
fun providerRowsIncludeConfiguredModelProvidersWithoutAuthRows() {
val rows =
providerRows(
providers =
listOf(
GatewayModelProviderSummary(
id = "openai",
displayName = "OpenAI",
status = "ok",
profileCount = 1,
),
),
models =
listOf(
model(provider = "openai", id = "gpt-5.5"),
model(provider = "byteplus", id = "seed-1-8-251228"),
),
)
assertEquals(listOf("openai", "byteplus"), rows.map { it.id })
assertEquals(1, rows.first { it.id == "openai" }.modelCount)
assertEquals(1, rows.first { it.id == "byteplus" }.modelCount)
assertTrue(rows.first { it.id == "byteplus" }.ready)
}
private fun model(
provider: String,
id: String,
): GatewayModelSummary =
GatewayModelSummary(
id = id,
name = id,
provider = provider,
supportsVision = false,
supportsAudio = false,
supportsDocuments = false,
supportsReasoning = false,
contextTokens = null,
available = null,
)
}

View File

@@ -0,0 +1,13 @@
package ai.openclaw.app.ui
import org.junit.Assert.assertEquals
import org.junit.Test
class SettingsScreensTest {
@Test
fun androidDistributionChannelUsesBuildFlavorLabels() {
assertEquals("Play", androidDistributionChannel("play"))
assertEquals("Third-party", androidDistributionChannel("thirdParty"))
assertEquals("Unknown", androidDistributionChannel(""))
}
}

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.AppearanceThemeMode
import ai.openclaw.app.GatewayChannelSummary
import ai.openclaw.app.GatewayChannelsSummary
import ai.openclaw.app.GatewayNodesDevicesSummary
@@ -17,6 +18,28 @@ class ShellScreenLogicTest {
assertFalse(shellBottomNavVisible(keyboardVisible = false, commandOpen = true))
}
@Test
fun appearanceThemeModeDefaultsToDarkForExistingInstalls() {
assertEquals(AppearanceThemeMode.Dark, AppearanceThemeMode.fromRawValue(null))
assertEquals(AppearanceThemeMode.Dark, AppearanceThemeMode.fromRawValue("unknown"))
}
@Test
fun appearanceThemeLabelsRoundTripFromSettingsOptions() {
assertEquals(listOf("System", "Dark", "Light"), appearanceThemeOptions())
assertEquals(AppearanceThemeMode.System, appearanceThemeModeForLabel("System"))
assertEquals(AppearanceThemeMode.Dark, appearanceThemeModeForLabel("Dark"))
assertEquals(AppearanceThemeMode.Light, appearanceThemeModeForLabel("Light"))
}
@Test
fun appearanceThemeModeResolvesAgainstSystemPreference() {
assertFalse(AppearanceThemeMode.System.isDark(systemDark = false))
assertTrue(AppearanceThemeMode.System.isDark(systemDark = true))
assertTrue(AppearanceThemeMode.Dark.isDark(systemDark = false))
assertFalse(AppearanceThemeMode.Light.isDark(systemDark = true))
}
@Test
fun homeAttentionRowsSurfaceGatewayWhenDisconnected() {
val rows =
@@ -76,6 +99,9 @@ class ShellScreenLogicTest {
)
assertEquals(listOf("Approvals", "Channels", "Nodes & Devices", "Providers"), rows.map { it.title })
val providersRow = rows.single { it.title == "Providers" }
assertEquals(Tab.Settings, providersRow.tab)
assertEquals(SettingsRoute.Gateway, providersRow.settingsRoute)
}
@Test

View File

@@ -0,0 +1,75 @@
package ai.openclaw.app.ui
import ai.openclaw.app.VoiceCaptureMode
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class VoiceScreenLogicTest {
@Test
fun voiceAttentionStatusKeepsFailedTalkStartVisibleAfterModeStops() {
val attention =
voiceAttentionStatus(
talkModeStatusText = "Start failed: Error: Realtime voice provider \"openai\" is not configured",
voiceCaptureMode = VoiceCaptureMode.Off,
micEnabled = false,
micIsSending = false,
talkModeEnabled = false,
talkModeListening = false,
talkModeSpeaking = false,
)
assertEquals("Realtime voice provider is not configured.", attention)
assertEquals(
attention,
voiceStatusLabel(
gatewayStatus = "Online",
voiceCaptureMode = VoiceCaptureMode.Off,
micStatusText = "Mic off",
micQueuedMessages = 0,
micIsSending = false,
talkModeListening = false,
talkModeSpeaking = false,
voiceAttentionStatus = attention,
),
)
}
@Test
fun voiceAttentionStatusDoesNotOverrideActiveTalkState() {
assertNull(
voiceAttentionStatus(
talkModeStatusText = "Start failed: provider unavailable",
voiceCaptureMode = VoiceCaptureMode.TalkMode,
micEnabled = false,
micIsSending = false,
talkModeEnabled = true,
talkModeListening = false,
talkModeSpeaking = false,
),
)
}
@Test
fun voiceAttentionStatusDoesNotOverrideDictationState() {
assertNull(
voiceAttentionStatus(
talkModeStatusText = "Start failed: provider unavailable",
voiceCaptureMode = VoiceCaptureMode.ManualMic,
micEnabled = true,
micIsSending = false,
talkModeEnabled = false,
talkModeListening = false,
talkModeSpeaking = false,
),
)
}
@Test
fun voiceRuntimeAttentionStatusSanitizesTranscriptionProviderFailures() {
assertEquals(
"Realtime transcription provider is not configured.",
voiceRuntimeAttentionStatus("Transcription unavailable: UNAVAILABLE: Error: No realtime transcription provider registered"),
)
}
}

View File

@@ -0,0 +1,94 @@
package ai.openclaw.app.ui.chat
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatMessageContent
import ai.openclaw.app.chat.ChatPendingToolCall
import org.junit.Assert.assertEquals
import org.junit.Test
class ChatTimelineTest {
@Test
fun activeRunAnchorsNewestUserPromptInsteadOfThinkingRow() {
val user = textMessage(id = "user-1", role = "user", text = "hello")
val timeline =
buildChatTimeline(
messages = listOf(user),
pendingRunCount = 1,
pendingToolCalls = emptyList(),
streamingAssistantText = null,
)
assertEquals(listOf("thinking", "message:user-1"), timeline.items.map(::chatTimelineItemKey))
assertEquals(1, timeline.scrollTargetIndex)
}
@Test
fun activeRunAnchorsNewestUserPromptWhileAssistantStreams() {
val olderAssistant = textMessage(id = "assistant-1", role = "assistant", text = "previous")
val user = textMessage(id = "user-1", role = "user", text = "next")
val tool =
ChatPendingToolCall(
toolCallId = "tool-1",
name = "memory.search",
startedAtMs = 1000L,
)
val timeline =
buildChatTimeline(
messages = listOf(olderAssistant, user),
pendingRunCount = 1,
pendingToolCalls = listOf(tool),
streamingAssistantText = "streaming",
)
assertEquals(
listOf("stream", "tools", "thinking", "message:user-1", "message:assistant-1"),
timeline.items.map(::chatTimelineItemKey),
)
assertEquals(3, timeline.scrollTargetIndex)
}
@Test
fun finishedRunAnchorsNewestPersistedMessage() {
val user = textMessage(id = "user-1", role = "user", text = "hello")
val assistant = textMessage(id = "assistant-1", role = "assistant", text = "done")
val timeline =
buildChatTimeline(
messages = listOf(user, assistant),
pendingRunCount = 0,
pendingToolCalls = emptyList(),
streamingAssistantText = null,
)
assertEquals(listOf("message:assistant-1", "message:user-1"), timeline.items.map(::chatTimelineItemKey))
assertEquals(0, timeline.scrollTargetIndex)
}
@Test
fun emptyTimelineHasNoScrollTarget() {
val timeline =
buildChatTimeline(
messages = emptyList(),
pendingRunCount = 0,
pendingToolCalls = emptyList(),
streamingAssistantText = null,
)
assertEquals(emptyList<String>(), timeline.items.map(::chatTimelineItemKey))
assertEquals(null, timeline.scrollTargetIndex)
}
private fun textMessage(
id: String,
role: String,
text: String,
): ChatMessage =
ChatMessage(
id = id,
role = role,
content = listOf(ChatMessageContent(type = "text", text = text)),
timestampMs = null,
)
}

View File

@@ -5,7 +5,7 @@ plugins {
android {
namespace = "ai.openclaw.app.benchmark"
compileSdk = 36
compileSdk = 37
defaultConfig {
minSdk = 31

View File

@@ -1,10 +1,10 @@
[versions]
agp = "9.2.0"
agp = "9.2.1"
androidx-activity = "1.13.0"
androidx-benchmark = "1.4.1"
androidx-camera = "1.6.0"
androidx-compose-bom = "2026.04.01"
androidx-core = "1.18.0"
androidx-compose-bom = "2026.05.01"
androidx-core = "1.19.0"
androidx-exifinterface = "1.4.2"
androidx-lifecycle = "2.10.0"
androidx-security = "1.1.0"
@@ -13,14 +13,14 @@ androidx-uiautomator = "2.4.0-beta02"
androidx-webkit = "1.15.0"
bcprov = "1.84"
commonmark = "0.28.0"
coroutines = "1.10.2"
dnsjava = "3.6.4"
coroutines = "1.11.0"
dnsjava = "3.6.5"
junit = "4.13.2"
junit-vintage = "6.0.3"
junit-vintage = "6.1.0"
kotest = "6.1.11"
ktlint-gradle = "14.2.0"
kotlin = "2.3.21"
material = "1.13.0"
kotlin = "2.4.0"
material = "1.14.0"
okhttp = "5.3.2"
play-services-code-scanner = "16.1.0"
robolectric = "4.16.1"

Binary file not shown.

View File

@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.

View File

@@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

View File

@@ -2,7 +2,9 @@
## 2026.6.2 - 2026-06-02
Maintenance update for the current OpenClaw release.
OpenClaw is now available on iPhone.
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, share content from iOS, and bring device capabilities like camera, location, screen, and notifications into your private automation workflows.
## 2026.6.1 - 2026-06-01

View File

@@ -1,6 +1,6 @@
# OpenClaw iOS (Super Alpha)
This iOS app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node` on iPhone and iPad.
This iOS app is super-alpha and internal-use only. The first public App Store release targets iPhone and connects to an OpenClaw Gateway as a `role: node`.
## Distribution Status
@@ -34,7 +34,7 @@ open OpenClaw.xcodeproj
3. In Xcode:
- Scheme: `OpenClaw`
- Destination: connected iPhone or iPad (recommended for real behavior)
- Destination: connected iPhone (recommended for real behavior)
- Build configuration: `Debug`
- Run (`Product` -> `Run`)
4. If signing fails on a personal team:
@@ -251,7 +251,7 @@ gateway can only send pushes for iOS devices that paired with that gateway.
## Computer Use Relationship
The iOS app is not a Codex Computer Use backend. Computer Use and `cua-driver mcp` are macOS desktop-control paths; iOS exposes device capabilities as OpenClaw node commands through the gateway. Agents can drive the iPhone or iPad canvas, camera, screen, location, voice, and other node capabilities with `node.invoke`, subject to iOS foreground/background limits.
The iOS app is not a Codex Computer Use backend. Computer Use and `cua-driver mcp` are macOS desktop-control paths; iOS exposes device capabilities as OpenClaw node commands through the gateway. Agents can drive the iPhone canvas, camera, screen, location, voice, and other node capabilities with `node.invoke`, subject to iOS foreground/background limits.
## Location Automation Use Case (Testing)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,253 @@
import Foundation
import OpenClawChatUI
import OpenClawProtocol
enum AppleReviewDemoMode {
static let setupCode = "APPLE-REVIEW-DEMO"
static let gatewayName = "Apple Review Demo Gateway"
static let gatewayAddress = "Local demo mode"
static let gatewayID = "apple-review-demo"
static func isSetupCode(_ value: String) -> Bool {
value.trimmingCharacters(in: .whitespacesAndNewlines)
.localizedCaseInsensitiveCompare(self.setupCode) == .orderedSame
}
static var agents: [AgentSummary] {
[
AgentSummary(
id: "main",
name: "Main",
identity: ["emoji": AnyCodable("OC")],
workspace: "Apple Review Demo",
model: ["provider": AnyCodable("demo"), "model": AnyCodable("local-demo")],
agentruntime: ["kind": AnyCodable("local")],
thinkinglevels: nil,
thinkingoptions: ["auto", "low", "medium"],
thinkingdefault: "auto"),
]
}
}
struct AppleReviewDemoChatTransport: OpenClawChatTransport {
private let store = AppleReviewDemoChatStore()
func createSession(
key: String,
label _: String?,
parentSessionKey _: String?) async throws -> OpenClawChatCreateSessionResponse
{
try await self.store.createSession(key: key)
}
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
try await self.store.history(sessionKey: sessionKey)
}
func listModels() async throws -> [OpenClawChatModelChoice] {
[
OpenClawChatModelChoice(
modelID: "local-demo",
name: "Apple Review Demo",
provider: "demo",
contextWindow: 128_000),
]
}
func sendMessage(
sessionKey: String,
message: String,
thinking _: String,
idempotencyKey: String,
attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
{
try await self.store.sendMessage(
sessionKey: sessionKey,
message: message,
runId: idempotencyKey)
}
func abortRun(sessionKey _: String, runId _: String) async throws {}
func listSessions(limit _: Int?) async throws -> OpenClawChatSessionsListResponse {
try await self.store.sessions()
}
func setSessionModel(sessionKey _: String, model _: String?) async throws {}
func setSessionThinking(sessionKey _: String, thinkingLevel _: String) async throws {}
func requestHealth(timeoutMs _: Int) async throws -> Bool {
true
}
func waitForRunCompletion(runId _: String, timeoutMs _: Int) async -> Bool {
true
}
func events() -> AsyncStream<OpenClawChatTransportEvent> {
AsyncStream { continuation in
continuation.yield(.health(ok: true))
continuation.finish()
}
}
func setActiveSessionKey(_: String) async throws {}
func resetSession(sessionKey _: String) async throws {
await self.store.reset()
}
func compactSession(sessionKey _: String) async throws {}
}
private actor AppleReviewDemoChatStore {
private let sessionKey = "main"
private var messages: [OpenClawChatMessage]
init() {
self.messages = AppleReviewDemoChatStore.seedMessages()
}
func createSession(key: String) throws -> OpenClawChatCreateSessionResponse {
try Self.decode(
CreateSessionPayload(ok: true, key: key, sessionId: "apple-review-demo-\(key)"),
as: OpenClawChatCreateSessionResponse.self)
}
func history(sessionKey: String) throws -> OpenClawChatHistoryPayload {
let normalizedSessionKey = Self.normalizedSessionKey(sessionKey)
return try Self.decode(
HistoryPayload(
sessionKey: normalizedSessionKey,
sessionId: "apple-review-demo-\(normalizedSessionKey)",
messages: self.messages,
thinkingLevel: "auto"),
as: OpenClawChatHistoryPayload.self)
}
func sendMessage(sessionKey _: String, message: String, runId: String) throws -> OpenClawChatSendResponse {
let now = Date().timeIntervalSince1970 * 1000
self.messages.append(Self.message(role: "user", text: message, timestamp: now))
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
let subject = trimmed.isEmpty ? "that request" : "\"\(trimmed)\""
self.messages.append(
Self.message(
role: "assistant",
text: """
Demo mode is active. I can show the review flow locally for \(subject), including chat, agent \
selection, settings, and Gateway-connected UI states. Live automation requires pairing a real \
OpenClaw Gateway.
""",
timestamp: now + 1))
return try Self.decode(
SendPayload(runId: runId, status: "ok"),
as: OpenClawChatSendResponse.self)
}
func sessions() throws -> OpenClawChatSessionsListResponse {
let entry = OpenClawChatSessionEntry(
key: self.sessionKey,
kind: "chat",
displayName: "Apple Review Demo",
surface: "ios",
subject: "Gateway review flow",
room: nil,
space: nil,
updatedAt: Date().timeIntervalSince1970 * 1000,
sessionId: "apple-review-demo-main",
systemSent: true,
abortedLastRun: false,
thinkingLevel: "auto",
verboseLevel: nil,
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "demo",
model: "local-demo",
contextTokens: 128_000,
thinkingLevels: [
OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"),
OpenClawChatThinkingLevelOption(id: "low", label: "Low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"),
],
thinkingOptions: ["auto", "low", "medium"],
thinkingDefault: "auto")
return OpenClawChatSessionsListResponse(
ts: Date().timeIntervalSince1970 * 1000,
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "demo",
model: "local-demo",
contextTokens: 128_000,
thinkingLevels: [
OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"),
OpenClawChatThinkingLevelOption(id: "low", label: "Low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"),
],
thinkingOptions: ["auto", "low", "medium"],
thinkingDefault: "auto",
mainSessionKey: self.sessionKey),
sessions: [entry])
}
func reset() {
self.messages = Self.seedMessages()
}
private static func seedMessages() -> [OpenClawChatMessage] {
let now = Date().timeIntervalSince1970 * 1000
return [
self.message(
role: "assistant",
text: """
Apple Review demo mode is active. This local chat transport lets reviewers inspect the iOS app \
without a private Gateway.
""",
timestamp: now),
]
}
private static func message(role: String, text: String, timestamp: Double) -> OpenClawChatMessage {
OpenClawChatMessage(
role: role,
content: [
OpenClawChatMessageContent(
type: "text",
text: text,
mimeType: nil,
fileName: nil,
content: nil),
],
timestamp: timestamp)
}
private static func normalizedSessionKey(_ value: String) -> String {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "main" : trimmed
}
private static func decode<T: Decodable>(_ value: some Encodable, as type: T.Type) throws -> T {
let data = try JSONEncoder().encode(value)
return try JSONDecoder().decode(type, from: data)
}
private struct HistoryPayload: Encodable {
var sessionKey: String
var sessionId: String?
var messages: [OpenClawChatMessage]?
var thinkingLevel: String?
}
private struct SendPayload: Encodable {
var runId: String
var status: String
}
private struct CreateSessionPayload: Encodable {
var ok: Bool?
var key: String
var sessionId: String?
}
}

View File

@@ -4,6 +4,7 @@ import SwiftUI
struct AgentProDreamingDestination: View {
@Environment(NodeAppModel.self) private var appModel
let headerLeadingAction: OpenClawSidebarHeaderAction?
let overview: AgentOverviewSnapshot?
let gatewayConnected: Bool
let overviewLoading: Bool
@@ -20,6 +21,7 @@ struct AgentProDreamingDestination: View {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.header
self.detailSummaryCard(
icon: "moon",
title: "Dreaming",
@@ -57,6 +59,23 @@ struct AgentProDreamingDestination: View {
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
private var header: some View {
if let headerLeadingAction {
OpenClawAdaptiveHeaderRow(
title: "Dreaming",
subtitle: self.dreamingDetail,
titleFont: .title3.weight(.semibold),
subtitleFont: .callout)
{
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
} accessory: {
EmptyView()
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
private enum DreamAction: String, CaseIterable, Identifiable {
case backfill
case repair

View File

@@ -329,14 +329,24 @@ struct AgentConfigLite: Decodable {
struct ConfigPatchParams: Encodable {
let raw: String
let baseHash: String
let replacePaths: [String]?
init(raw: String, baseHash: String, replacePaths: [String]? = nil) {
self.raw = raw
self.baseHash = baseHash
self.replacePaths = replacePaths
}
}
enum SkillMutationError: LocalizedError {
case liveGatewayUnavailable
case missingConfigHash
case invalidPatchPayload
var errorDescription: String? {
switch self {
case .liveGatewayUnavailable:
"Connect a live gateway to edit agent skills."
case .missingConfigHash:
"Config hash missing; refresh and retry."
case .invalidPatchPayload:

View File

@@ -3,6 +3,7 @@ import SwiftUI
import UIKit
struct AgentProNodesDestination: View {
let headerLeadingAction: OpenClawSidebarHeaderAction?
let overview: AgentOverviewSnapshot?
let gatewayConnected: Bool
let agentCount: Int
@@ -16,6 +17,7 @@ struct AgentProNodesDestination: View {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.header
self.summaryCard
self.totalsCard
self.nodesList
@@ -27,16 +29,33 @@ struct AgentProNodesDestination: View {
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Nodes")
.navigationTitle("Instances")
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
private var header: some View {
if let headerLeadingAction {
OpenClawAdaptiveHeaderRow(
title: "Instances",
subtitle: self.instancesDetail,
titleFont: .title3.weight(.semibold),
subtitleFont: .callout)
{
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
} accessory: {
EmptyView()
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
private var summaryCard: some View {
ProCard {
HStack(spacing: 12) {
ProIconBadge(systemName: "display", color: self.instancesColor)
VStack(alignment: .leading, spacing: 3) {
Text("Nodes")
Text("Instances")
.font(.headline)
Text(self.instancesDetail)
.font(.caption)
@@ -70,16 +89,16 @@ struct AgentProNodesDestination: View {
private var nodesList: some View {
VStack(alignment: .leading, spacing: 8) {
ProSectionHeader(title: "Connected Nodes")
ProSectionHeader(title: "Connected Instances")
ProCard(padding: 0) {
let nodes = self.sortedPresenceEntries
if nodes.isEmpty {
self.emptyRow(
icon: "display",
title: self.gatewayConnected ? "No nodes connected" : "Nodes unavailable",
title: self.gatewayConnected ? "No instances connected" : "Instances unavailable",
detail: self.gatewayConnected
? "The gateway did not report any system presence entries."
: "Connect a gateway to inspect connected nodes.")
: "Connect a gateway to inspect connected instances.")
.padding(14)
} else {
VStack(spacing: 0) {
@@ -114,7 +133,7 @@ struct AgentProNodesDestination: View {
HStack(alignment: .top, spacing: 12) {
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
VStack(alignment: .leading, spacing: 4) {
Text(Self.presenceLabel(entry) ?? "Node")
Text(Self.presenceLabel(entry) ?? "Instance")
.font(.subheadline.weight(.semibold))
.lineLimit(1)
Text(Self.presenceDetail(entry))
@@ -153,7 +172,7 @@ struct AgentProNodesDestination: View {
HStack(spacing: 12) {
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
VStack(alignment: .leading, spacing: 3) {
Text(Self.presenceLabel(entry) ?? "Node")
Text(Self.presenceLabel(entry) ?? "Instance")
.font(.headline)
Text(Self.presenceDetail(entry))
.font(.caption)
@@ -192,7 +211,7 @@ struct AgentProNodesDestination: View {
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle(Self.presenceLabel(entry) ?? "Node")
.navigationTitle(Self.presenceLabel(entry) ?? "Instance")
.navigationBarTitleDisplayMode(.inline)
}

View File

@@ -99,14 +99,14 @@ extension AgentProTab {
} label: {
Label("Run", systemImage: "play.fill")
}
.disabled(busy || !self.gatewayConnected)
.disabled(busy || !self.liveGatewayConnected)
Button {
Task { await self.setCronJob(job, enabled: !job.enabled) }
} label: {
Label(job.enabled ? "Pause" : "Enable", systemImage: job.enabled ? "pause.fill" : "checkmark")
}
.disabled(busy || !self.gatewayConnected)
.disabled(busy || !self.liveGatewayConnected)
}
.buttonStyle(.bordered)
.controlSize(.mini)
@@ -149,7 +149,7 @@ extension AgentProTab {
success: String,
action: () async throws -> Void) async
{
guard self.gatewayConnected else { return }
guard self.liveGatewayConnected else { return }
self.cronActionBusyIDs.insert(job.id)
self.cronActionStatusText = nil
defer { self.cronActionBusyIDs.remove(job.id) }

View File

@@ -6,10 +6,12 @@ extension AgentProTab {
@ViewBuilder
func destination(for route: AgentRoute) -> some View {
switch route {
case .agents:
self.agentsDestination
case .skills:
self.skillsDestination
case .nodes:
self.nodesDestination
case .instances:
self.instancesDestination
case .cron:
self.cronDestination
case .usage:
@@ -19,6 +21,26 @@ extension AgentProTab {
}
}
var agentsDestination: some View {
ZStack {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.rosterHeader
self.agentFilters
self.agentsSection
}
.padding(.vertical, 18)
}
.refreshable {
await self.refreshOverview(force: true)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Agents")
.navigationBarTitleDisplayMode(.inline)
}
var skillsDestination: some View {
ZStack {
OpenClawProBackground()
@@ -46,8 +68,9 @@ extension AgentProTab {
.navigationBarTitleDisplayMode(.inline)
}
var nodesDestination: some View {
var instancesDestination: some View {
AgentProNodesDestination(
headerLeadingAction: self.directHeaderLeadingAction(for: .instances),
overview: self.overview,
gatewayConnected: self.gatewayConnected,
agentCount: self.appModel.gatewayAgents.count,
@@ -64,6 +87,10 @@ extension AgentProTab {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.directHeader(
for: .cron,
title: "Cron Jobs",
subtitle: self.cronDetail)
self.detailSummaryCard(
icon: "clock.arrow.circlepath",
title: "Cron Jobs",
@@ -89,6 +116,10 @@ extension AgentProTab {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.directHeader(
for: .usage,
title: "Usage",
subtitle: self.usageDetail)
self.detailSummaryCard(
icon: "chart.line.uptrend.xyaxis",
title: "Usage",
@@ -111,6 +142,7 @@ extension AgentProTab {
var dreamingDestination: some View {
AgentProDreamingDestination(
headerLeadingAction: self.directHeaderLeadingAction(for: .dreaming),
overview: self.overview,
gatewayConnected: self.gatewayConnected,
overviewLoading: self.overviewLoading,
@@ -122,6 +154,27 @@ extension AgentProTab {
})
}
@ViewBuilder
func directHeader(for route: AgentRoute, title: String, subtitle: String) -> some View {
if let headerLeadingAction = self.directHeaderLeadingAction(for: route) {
OpenClawAdaptiveHeaderRow(
title: title,
subtitle: subtitle,
titleFont: .title3.weight(.semibold),
subtitleFont: .callout)
{
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
} accessory: {
EmptyView()
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
func directHeaderLeadingAction(for route: AgentRoute) -> OpenClawSidebarHeaderAction? {
self.directRoute == route ? self.headerLeadingAction : nil
}
func detailSummaryCard(
icon: String,
title: String,

View File

@@ -58,16 +58,9 @@ extension AgentProTab {
}
func agentRosterState(for agent: AgentSummary) -> AgentRosterState {
guard self.gatewayConnected else { return .idle }
guard self.gatewayConnected else { return .ready }
if agent.id == self.activeAgentID { return .online }
if self.cronJobsContain(agentID: agent.id) { return .busy }
return .idle
}
func cronJobsContain(agentID: String) -> Bool {
self.recentCronJobs.contains { job in
self.normalized(job.agentid) == agentID && job.enabled
}
return .ready
}
func modelLabel(for agent: AgentSummary) -> String? {
@@ -124,7 +117,7 @@ extension AgentProTab {
@MainActor
func refreshOverview(force: Bool) async {
guard self.scenePhase == .active else { return }
guard self.appModel.isOperatorGatewayConnected else {
guard self.liveGatewayConnected else {
self.overview = nil
self.overviewErrorText = nil
self.overviewLoading = false

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